From 78f638e6331dd8f154f82111b5d074160ed08e1c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 14 Aug 2023 14:11:28 -0500 Subject: [PATCH] Add relays to database and start tracking them --- src/db.ts | 5 +++++ src/db/migrations/001_add_relays.ts | 12 ++++++++++++ src/db/relays.ts | 19 +++++++++++++++++++ src/firehose.ts | 28 +++++++++++++++++++++++----- src/schema.ts | 29 +---------------------------- src/utils.ts | 7 +++++++ 6 files changed, 67 insertions(+), 33 deletions(-) create mode 100644 src/db/migrations/001_add_relays.ts create mode 100644 src/db/relays.ts diff --git a/src/db.ts b/src/db.ts index d03bf2f..7da67ed 100644 --- a/src/db.ts +++ b/src/db.ts @@ -8,6 +8,7 @@ interface DittoDB { events: EventRow; tags: TagRow; users: UserRow; + relays: RelayRow; } interface EventRow { @@ -34,6 +35,10 @@ interface UserRow { inserted_at: Date; } +interface RelayRow { + url: string; +} + const db = new Kysely({ dialect: new DenoSqliteDialect({ database: new Sqlite(Conf.dbPath), diff --git a/src/db/migrations/001_add_relays.ts b/src/db/migrations/001_add_relays.ts new file mode 100644 index 0000000..a603c2e --- /dev/null +++ b/src/db/migrations/001_add_relays.ts @@ -0,0 +1,12 @@ +import { Kysely } from '@/deps.ts'; + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('relays') + .addColumn('url', 'text', (col) => col.primaryKey()) + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('relays').execute(); +} diff --git a/src/db/relays.ts b/src/db/relays.ts new file mode 100644 index 0000000..d550162 --- /dev/null +++ b/src/db/relays.ts @@ -0,0 +1,19 @@ +import { db } from '@/db.ts'; + +/** Inserts relays into the database, skipping duplicates. */ +function addRelays(relays: `wss://${string}`[]) { + const values = relays.map((url) => ({ url })); + + return db.insertInto('relays') + .values(values) + .onConflict((oc) => oc.column('url').doNothing()) + .execute(); +} + +/** Get a list of all known good relays. */ +async function getAllRelays(): Promise { + const rows = await db.selectFrom('relays').select('relays.url').execute(); + return rows.map((row) => row.url); +} + +export { addRelays, getAllRelays }; diff --git a/src/firehose.ts b/src/firehose.ts index 17a5155..d1b5f7e 100644 --- a/src/firehose.ts +++ b/src/firehose.ts @@ -1,20 +1,21 @@ -import { Conf } from '@/config.ts'; import { insertEvent, isLocallyFollowed } from '@/db/events.ts'; +import { addRelays, getAllRelays } from '@/db/relays.ts'; import { findUser } from '@/db/users.ts'; import { RelayPool } from '@/deps.ts'; import { trends } from '@/trends.ts'; -import { nostrDate, nostrNow } from '@/utils.ts'; +import { isRelay, nostrDate, nostrNow } from '@/utils.ts'; import type { SignedEvent } from '@/event.ts'; -const relay = new RelayPool([Conf.relay]); +const relays = await getAllRelays(); +const pool = new RelayPool(relays); // This file watches all events on your Ditto relay and triggers // side-effects based on them. This can be used for things like // notifications, trending hashtag tracking, etc. -relay.subscribe( +pool.subscribe( [{ kinds: [1], since: nostrNow() }], - [Conf.relay], + relays, handleEvent, undefined, undefined, @@ -25,6 +26,7 @@ async function handleEvent(event: SignedEvent): Promise { console.info('firehose event:', event.id); trackHashtags(event); + trackRelays(event); if (await findUser({ pubkey: event.pubkey }) || await isLocallyFollowed(event.pubkey)) { insertEvent(event).catch(console.warn); @@ -49,3 +51,19 @@ function trackHashtags(event: SignedEvent): void { // do nothing } } + +/** Tracks nown relays in the database. */ +function trackRelays(event: SignedEvent) { + const relays = new Set<`wss://${string}`>(); + + event.tags.forEach((tag) => { + if (['p', 'e', 'a'].includes(tag[0]) && isRelay(tag[2])) { + relays.add(tag[2]); + } + if (event.kind === 10002 && tag[0] === 'r' && isRelay(tag[1])) { + relays.add(tag[1]); + } + }); + + return addRelays([...relays]); +} diff --git a/src/schema.ts b/src/schema.ts index 09d46a0..e56c1ae 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -20,24 +20,6 @@ const jsonSchema = z.string().transform((value, ctx) => { } }); -/** Alias for `safeParse`, but instead of returning a success object it returns the value (or undefined on fail). */ -function parseValue(schema: z.ZodType, value: unknown): T | undefined { - const result = schema.safeParse(value); - return result.success ? result.data : undefined; -} - -const parseRelay = (relay: string | URL) => parseValue(relaySchema, relay); - -const relaySchema = z.custom((relay) => { - if (typeof relay !== 'string') return false; - try { - const { protocol } = new URL(relay); - return protocol === 'wss:' || protocol === 'ws:'; - } catch (_e) { - return false; - } -}); - const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url()]); /** https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */ @@ -60,13 +42,4 @@ const hashtagSchema = z.string().regex(/^\w{1,30}$/); */ const safeUrlSchema = z.string().max(2048).url(); -export { - decode64Schema, - emojiTagSchema, - filteredArray, - hashtagSchema, - jsonSchema, - parseRelay, - relaySchema, - safeUrlSchema, -}; +export { decode64Schema, emojiTagSchema, filteredArray, hashtagSchema, jsonSchema, safeUrlSchema }; diff --git a/src/utils.ts b/src/utils.ts index ed0a640..a2c2504 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -142,6 +142,12 @@ function activityJson(c: Context, object: T) { return response; } +/** Schema to parse a relay URL. */ +const relaySchema = z.string().max(255).startsWith('wss://').url(); + +/** Check whether the value is a valid relay URL. */ +const isRelay = (relay: string): relay is `wss://${string}` => relaySchema.safeParse(relay).success; + export { activityJson, bech32ToPubkey, @@ -149,6 +155,7 @@ export { eventAge, eventDateComparator, findTag, + isRelay, lookupAccount, type Nip05, nostrDate,