-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.ts
135 lines (115 loc) · 3.28 KB
/
main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import "dotenv/config";
import "websocket-polyfill";
import * as R from "rambda";
import Fastify, { FastifyRequest } from "fastify";
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
import { MeiliSearch, MultiSearchResult } from "meilisearch";
import { Event, nip19, SimplePool } from "nostr-tools";
import { RecommendationParams } from "./types.js";
import {
buildDefaultWeights,
isNostrHexKey,
isTopLevelPost,
nostrRelays,
} from "./nostr.js";
import { unprocessableHandler } from "./errors.js";
import { MEILI_INDEX_USER_WEIGHTS } from "./constants.js";
import { match } from "ts-pattern";
import { buildRecommendQuery } from "./meili.js";
let indexingWorker: ChildProcessWithoutNullStreams;
if (process.env.NODE_ENV === "production") {
indexingWorker = spawn("node", ["./build/indexing.js"]);
} else {
indexingWorker = spawn("ts-node-esm", ["indexing.ts"]);
}
indexingWorker.stdout.on("data", (data) => {
console.log(`${data}`);
});
indexingWorker.stderr.on("data", (data) => {
console.error(`${data}`);
});
/**
* MeiliSearch
*/
const env = process.env;
const MEILI_HOST_URL = env["MEILI_HOST_URL"];
const MEILI_MASTER_KEY = env["MEILI_MASTER_KEY"];
const MEILI_ADMIN_API_KEY = env["MEILI_ADMIN_API_KEY"];
const client = new MeiliSearch({
host: MEILI_HOST_URL,
apiKey: MEILI_MASTER_KEY,
});
/**
* Fastify start
*/
const fastify = Fastify({
logger: true,
});
fastify.route({
method: "GET",
url: "/",
handler: async (_, reply) => {
reply
.type("text/html")
.send(
"This is a naive classifier based recommendation system for Nostr.",
);
},
});
fastify.route({
method: "GET",
url: "/recommend/:pubkey",
schema: {
querystring: {
limit: { type: "integer", default: 20 },
offset: { type: "integer", default: 0 },
},
},
handler: async (request: FastifyRequest<RecommendationParams>, reply) => {
const { pubkey } = request.params;
const { limit, offset } = request.query as any;
if (!isNostrHexKey(pubkey) && !pubkey.startsWith("npub1")) {
return unprocessableHandler(
new Error("not a valid hex or npub nostr key"),
reply,
);
}
let hexPubKey: string;
if (!isNostrHexKey(pubkey)) {
try {
hexPubKey = nip19.decode(pubkey).data as string;
} catch (e) {
return unprocessableHandler(e, reply);
}
} else {
hexPubKey = pubkey;
}
const res = await client.index(MEILI_INDEX_USER_WEIGHTS).search("", {
filter: `pubkey=${hexPubKey}`,
});
let weights;
match(res.hits.length)
.with(0, () => {
weights = Object.fromEntries(buildDefaultWeights());
})
.otherwise(() => {
weights = res.hits[0].weight;
});
const multiQuery = buildRecommendQuery(weights);
const searchRes = await client.multiSearch({ queries: multiQuery });
return R.compose(
R.slice(offset * limit, (offset + 1) * limit),
R.filter((ev: Event) => isTopLevelPost(ev)),
R.sort((x: any, y: any) => x.created_at > y.created_at ? -1 : 1),
R.flatten,
R.map((r: MultiSearchResult<Record<string, any>>) => r.hits),
)(searchRes.results);
},
});
// Run the server!
try {
await fastify.listen({ port: 3000 });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}