From e2cc6132be822e31e63b6b20455dd43ec8aae8e3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 19 Mar 2024 17:45:19 -0500 Subject: [PATCH 1/7] Track pubkey domains --- src/db.ts | 6 ++++ src/db/migrations/015_add_pubkey_domains.ts | 21 +++++++++++++ src/pipeline.ts | 33 +++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 src/db/migrations/015_add_pubkey_domains.ts diff --git a/src/db.ts b/src/db.ts index e1039dc..2d6b4d7 100644 --- a/src/db.ts +++ b/src/db.ts @@ -14,6 +14,7 @@ interface DittoDB { unattached_media: UnattachedMediaRow; author_stats: AuthorStatsRow; event_stats: EventStatsRow; + pubkey_domains: PubkeyDomainRow; } interface AuthorStatsRow { @@ -66,6 +67,11 @@ interface UnattachedMediaRow { uploaded_at: Date; } +interface PubkeyDomainRow { + pubkey: string; + domain: string; +} + const sqliteWorker = new SqliteWorker(); await sqliteWorker.open(Conf.dbPath); diff --git a/src/db/migrations/015_add_pubkey_domains.ts b/src/db/migrations/015_add_pubkey_domains.ts new file mode 100644 index 0000000..33bcc6c --- /dev/null +++ b/src/db/migrations/015_add_pubkey_domains.ts @@ -0,0 +1,21 @@ +import { Kysely } from '@/deps.ts'; + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('pubkey_domains') + .ifNotExists() + .addColumn('pubkey', 'text', (col) => col.primaryKey()) + .addColumn('domain', 'text') + .execute(); + + await db.schema + .createIndex('pubkey_domains_domain_index') + .on('pubkey_domains') + .column('domain') + .ifNotExists() + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('pubkey_domains').execute(); +} diff --git a/src/pipeline.ts b/src/pipeline.ts index 95c1a87..9d65b13 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -1,4 +1,6 @@ +import { NSchema as n } from '@soapbox/nspec'; import { Conf } from '@/config.ts'; +import { db } from '@/db.ts'; import { addRelays } from '@/db/relays.ts'; import { deleteAttachedMedia } from '@/db/unattached-media.ts'; import { Debug, LNURL, type NostrEvent } from '@/deps.ts'; @@ -15,6 +17,7 @@ import { TrendsWorker } from '@/workers/trends.ts'; import { verifyEventWorker } from '@/workers/verify.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { lnurlCache } from '@/utils/lnurl.ts'; +import { nip05Cache } from '@/utils/nip05.ts'; const debug = Debug('ditto:pipeline'); @@ -30,6 +33,7 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { + if (event.kind !== 0) return; + + // Parse metadata. + const metadata = n.json().pipe(n.metadata()).safeParse(event.content); + if (!metadata.success) return; + + // Get nip05. + const { nip05 } = metadata.data; + if (!nip05) return; + + // Fetch nip05. + const result = await nip05Cache.fetch(nip05, { signal }).catch(() => undefined); + if (!result) return; + + // Ensure pubkey matches event. + const { pubkey } = result; + if (pubkey !== event.pubkey) return; + + // Track pubkey domain. + const [, domain] = nip05.split('@'); + await db + .insertInto('pubkey_domains') + .values({ pubkey, domain }) + .execute() + .catch(debug); +} + /** Query to-be-deleted events, ensure their pubkey matches, then delete them from the database. */ async function processDeletions(event: NostrEvent, signal: AbortSignal): Promise { if (event.kind === 5) { From 245cb43771d13f2e078562f06467862c224f6e45 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 19 Mar 2024 22:46:54 -0500 Subject: [PATCH 2/7] EventsDB: parse nip50 extensions --- deno.json | 2 +- src/storages/events-db.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/deno.json b/deno.json index ccb54ad..b5fe33b 100644 --- a/deno.json +++ b/deno.json @@ -10,7 +10,7 @@ "relays:sync": "deno run -A --unstable-ffi scripts/relays.ts sync" }, "exclude": ["./public"], - "imports": { "@/": "./src/", "@soapbox/nspec": "jsr:@soapbox/nspec@^0.7.0", "~/fixtures/": "./fixtures/" }, + "imports": { "@/": "./src/", "@soapbox/nspec": "jsr:@soapbox/nspec@^0.8.0", "~/fixtures/": "./fixtures/" }, "lint": { "include": ["src/", "scripts/"], "rules": { diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index 0253ef3..b77f309 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -1,3 +1,4 @@ +import { NIP50 } from '@soapbox/nspec'; import { Conf } from '@/config.ts'; import { type DittoDB } from '@/db.ts'; import { Debug, Kysely, type NostrEvent, type NStore, type NStoreOpts, type SelectQueryBuilder } from '@/deps.ts'; @@ -199,9 +200,12 @@ class EventsDB implements NStore { } if (filter.search) { + const tokens = NIP50.parseInput(filter.search); + const q = tokens.filter((t) => typeof t === 'string').join(' '); + query = query .innerJoin('events_fts', 'events_fts.id', 'events.id') - .where('events_fts.content', 'match', JSON.stringify(filter.search)); + .where('events_fts.content', 'match', JSON.stringify(q)); } return query; From d17d4c846fa6ae9a31b55b3de3fbd7202236802d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 19 Mar 2024 22:50:47 -0500 Subject: [PATCH 3/7] EventsDB: allow searching by domain --- src/storages/events-db.ts | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index b77f309..38835c6 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -193,19 +193,28 @@ class EventsDB implements NStore { } } - if (typeof filter.local === 'boolean') { - query = query - .leftJoin(() => this.usersQuery(), (join) => join.onRef('users.d_tag', '=', 'events.pubkey')) - .where('users.d_tag', filter.local ? 'is not' : 'is', null); - } - if (filter.search) { const tokens = NIP50.parseInput(filter.search); - const q = tokens.filter((t) => typeof t === 'string').join(' '); - query = query - .innerJoin('events_fts', 'events_fts.id', 'events.id') - .where('events_fts.content', 'match', JSON.stringify(q)); + const domain = (tokens.find((t) => + typeof t === 'object' && t.key === 'domain' + ) as { key: 'domain'; value: string } | undefined)?.value; + + if (domain) { + query = query + .innerJoin('pubkey_domains', 'pubkey_domains.pubkey', 'events.pubkey') + .where('pubkey_domains.domain', '=', domain); + } + + const q = tokens.filter((t) => + typeof t === 'string' + ).join(' '); + + if (q) { + query = query + .innerJoin('events_fts', 'events_fts.id', 'events.id') + .where('events_fts.content', 'match', JSON.stringify(q)); + } } return query; From c8b378ad10ab8efbc79b21a29738872f2e6822d9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Mar 2024 11:34:04 -0500 Subject: [PATCH 4/7] Remove DittoFilter, use search instead of local --- deno.json | 2 +- src/controllers/api/accounts.ts | 4 ++-- src/controllers/api/search.ts | 8 ++++---- src/controllers/api/streaming.ts | 4 ++-- src/controllers/api/timelines.ts | 4 ++-- src/controllers/site.ts | 4 +++- src/filter.ts | 28 +--------------------------- src/interfaces/DittoFilter.ts | 8 +------- src/middleware/csp.ts | 4 ++-- src/queries.ts | 9 ++++++++- src/storages/events-db.test.ts | 22 +++++++++------------- src/storages/events-db.ts | 17 ++++++++--------- src/storages/optimizer.ts | 4 ++-- src/storages/search-store.ts | 5 ++--- src/subs.ts | 4 ++-- src/subscription.ts | 12 ++++++------ src/utils/api.ts | 6 +++--- 17 files changed, 58 insertions(+), 87 deletions(-) diff --git a/deno.json b/deno.json index b5fe33b..a552b64 100644 --- a/deno.json +++ b/deno.json @@ -10,7 +10,7 @@ "relays:sync": "deno run -A --unstable-ffi scripts/relays.ts sync" }, "exclude": ["./public"], - "imports": { "@/": "./src/", "@soapbox/nspec": "jsr:@soapbox/nspec@^0.8.0", "~/fixtures/": "./fixtures/" }, + "imports": { "@/": "./src/", "@soapbox/nspec": "jsr:@soapbox/nspec@^0.8.1", "~/fixtures/": "./fixtures/" }, "lint": { "include": ["src/", "scripts/"], "rules": { diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 2152208..3e6902a 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -14,7 +14,7 @@ import { renderAccounts, renderEventAccounts, renderStatuses } from '@/views.ts' import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { renderRelationship } from '@/views/mastodon/relationships.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; -import { DittoFilter } from '@/interfaces/DittoFilter.ts'; +import { NostrFilter } from '@/interfaces/DittoFilter.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; const usernameSchema = z @@ -145,7 +145,7 @@ const accountStatusesController: AppController = async (c) => { } } - const filter: DittoFilter = { + const filter: NostrFilter = { authors: [pubkey], kinds: [1], since, diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 8d680fb..f335d85 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -1,6 +1,6 @@ import { AppController } from '@/app.ts'; import { nip19, type NostrEvent, z } from '@/deps.ts'; -import { type DittoFilter } from '@/interfaces/DittoFilter.ts'; +import { type NostrFilter } from '@/interfaces/DittoFilter.ts'; import { booleanParamSchema } from '@/schema.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; import { searchStore } from '@/storages.ts'; @@ -67,7 +67,7 @@ const searchController: AppController = async (c) => { function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: AbortSignal): Promise { if (type === 'hashtags') return Promise.resolve([]); - const filter: DittoFilter = { + const filter: NostrFilter = { kinds: typeToKinds(type), search: q, limit, @@ -107,8 +107,8 @@ async function lookupEvent(query: SearchQuery, signal: AbortSignal): Promise { - const filters: DittoFilter[] = []; +async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: AbortSignal): Promise { + const filters: NostrFilter[] = []; const accounts = !type || type === 'accounts'; const statuses = !type || type === 'statuses'; diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 5988504..c11484e 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -1,6 +1,6 @@ import { type AppController } from '@/app.ts'; import { Debug, z } from '@/deps.ts'; -import { DittoFilter } from '@/interfaces/DittoFilter.ts'; +import { NostrFilter } from '@/interfaces/DittoFilter.ts'; import { getAuthor, getFeedPubkeys } from '@/queries.ts'; import { Sub } from '@/subs.ts'; import { bech32ToPubkey } from '@/utils.ts'; @@ -82,7 +82,7 @@ async function topicToFilter( topic: Stream, query: Record, pubkey: string | undefined, -): Promise { +): Promise { switch (topic) { case 'public': return { kinds: [1] }; diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 885b871..c1c669e 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -1,6 +1,6 @@ import { type AppContext, type AppController } from '@/app.ts'; import { z } from '@/deps.ts'; -import { type DittoFilter } from '@/interfaces/DittoFilter.ts'; +import { type NostrFilter } from '@/interfaces/DittoFilter.ts'; import { getFeedPubkeys } from '@/queries.ts'; import { booleanParamSchema } from '@/schema.ts'; import { eventsDB } from '@/storages.ts'; @@ -32,7 +32,7 @@ const hashtagTimelineController: AppController = (c) => { }; /** Render statuses for timelines. */ -async function renderStatuses(c: AppContext, filters: DittoFilter[]) { +async function renderStatuses(c: AppContext, filters: NostrFilter[]) { const { signal } = c.req.raw; const events = await eventsDB diff --git a/src/controllers/site.ts b/src/controllers/site.ts index b7f34dd..751e60e 100644 --- a/src/controllers/site.ts +++ b/src/controllers/site.ts @@ -4,9 +4,11 @@ import type { AppController } from '@/app.ts'; /** Landing page controller. */ const indexController: AppController = (c) => { + const { origin } = Conf.url; + return c.text(`Please connect with a Mastodon client: - ${Conf.localDomain} + ${origin} Ditto `); diff --git a/src/filter.ts b/src/filter.ts index 2e4d577..79e5b27 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -1,7 +1,4 @@ -import { Conf } from '@/config.ts'; -import { matchFilters, type NostrEvent, type NostrFilter, stringifyStable, z } from '@/deps.ts'; -import { DittoEvent } from '@/interfaces/DittoEvent.ts'; -import { type DittoFilter } from '@/interfaces/DittoFilter.ts'; +import { type NostrEvent, type NostrFilter, stringifyStable, z } from '@/deps.ts'; import { isReplaceableKind } from '@/kinds.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; @@ -12,28 +9,6 @@ type AuthorMicrofilter = { kinds: [0]; authors: [NostrEvent['pubkey']] }; /** Filter to get one specific event. */ type MicroFilter = IdMicrofilter | AuthorMicrofilter; -function matchDittoFilter(filter: DittoFilter, event: DittoEvent): boolean { - if (filter.local && !(event.user || event.pubkey === Conf.pubkey)) { - return false; - } - - return matchFilters([filter], event); -} - -/** - * Similar to nostr-tools `matchFilters`, but supports Ditto's custom keys. - * Database calls are needed to look up the extra data, so it's passed in as an argument. - */ -function matchDittoFilters(filters: DittoFilter[], event: DittoEvent): boolean { - for (const filter of filters) { - if (matchDittoFilter(filter, event)) { - return true; - } - } - - return false; -} - /** Get deterministic ID for a microfilter. */ function getFilterId(filter: MicroFilter): string { if ('ids' in filter) { @@ -114,7 +89,6 @@ export { getMicroFilters, type IdMicrofilter, isMicrofilter, - matchDittoFilters, type MicroFilter, normalizeFilters, }; diff --git a/src/interfaces/DittoFilter.ts b/src/interfaces/DittoFilter.ts index bcc1719..6046763 100644 --- a/src/interfaces/DittoFilter.ts +++ b/src/interfaces/DittoFilter.ts @@ -1,12 +1,6 @@ -import { type NostrEvent, type NostrFilter } from '@/deps.ts'; +import { type NostrEvent } from '@/deps.ts'; import { type DittoEvent } from './DittoEvent.ts'; /** Additional properties that may be added by Ditto to events. */ export type DittoRelation = Exclude; - -/** Custom filter interface that extends Nostr filters with extra options for Ditto. */ -export interface DittoFilter extends NostrFilter { - /** Whether the event was authored by a local user. */ - local?: boolean; -} diff --git a/src/middleware/csp.ts b/src/middleware/csp.ts index 8875847..fdce5c7 100644 --- a/src/middleware/csp.ts +++ b/src/middleware/csp.ts @@ -3,13 +3,13 @@ import { Conf } from '@/config.ts'; const csp = (): AppMiddleware => { return async (c, next) => { - const { host, protocol } = Conf.url; + const { host, protocol, origin } = Conf.url; const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; const policies = [ 'upgrade-insecure-requests', `script-src 'self'`, - `connect-src 'self' blob: ${Conf.localDomain} ${wsProtocol}//${host}`, + `connect-src 'self' blob: ${origin} ${wsProtocol}//${host}`, `media-src 'self' https:`, `img-src 'self' data: blob: https:`, `default-src 'none'`, diff --git a/src/queries.ts b/src/queries.ts index c6be412..85a26cf 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -1,3 +1,4 @@ +import { Conf } from '@/config.ts'; import { eventsDB, optimizer } from '@/storages.ts'; import { Debug, type NostrEvent, type NostrFilter } from '@/deps.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; @@ -89,7 +90,13 @@ function getDescendants(eventId: string, signal = AbortSignal.timeout(2000)): Pr /** Returns whether the pubkey is followed by a local user. */ async function isLocallyFollowed(pubkey: string): Promise { - const [event] = await eventsDB.query([{ kinds: [3], '#p': [pubkey], local: true, limit: 1 }], { limit: 1 }); + const { host } = Conf.url; + + const [event] = await eventsDB.query( + [{ kinds: [3], '#p': [pubkey], search: `domain:${host}`, limit: 1 }], + { limit: 1 }, + ); + return Boolean(event); } diff --git a/src/storages/events-db.test.ts b/src/storages/events-db.test.ts index 744935b..f488f46 100644 --- a/src/storages/events-db.test.ts +++ b/src/storages/events-db.test.ts @@ -1,5 +1,4 @@ import { db } from '@/db.ts'; -import { buildUserEvent } from '@/db/users.ts'; import { assertEquals, assertRejects } from '@/deps-test.ts'; import event0 from '~/fixtures/events/event-0.json' with { type: 'json' }; @@ -28,23 +27,20 @@ Deno.test('insert and filter events', async () => { ); }); -Deno.test('query events with local filter', async () => { +Deno.test('query events with domain search filter', async () => { await eventsDB.event(event1); assertEquals(await eventsDB.query([{}]), [event1]); - assertEquals(await eventsDB.query([{ local: true }]), []); - assertEquals(await eventsDB.query([{ local: false }]), [event1]); + assertEquals(await eventsDB.query([{ search: 'domain:localhost:8000' }]), []); + assertEquals(await eventsDB.query([{ search: '' }]), [event1]); - const userEvent = await buildUserEvent({ - username: 'alex', - pubkey: event1.pubkey, - inserted_at: new Date(), - admin: false, - }); - await eventsDB.event(userEvent); + await db + .insertInto('pubkey_domains') + .values({ pubkey: event1.pubkey, domain: 'localhost:8000' }) + .execute(); - assertEquals(await eventsDB.query([{ kinds: [1], local: true }]), [event1]); - assertEquals(await eventsDB.query([{ kinds: [1], local: false }]), []); + assertEquals(await eventsDB.query([{ kinds: [1], search: 'domain:localhost:8000' }]), [event1]); + assertEquals(await eventsDB.query([{ kinds: [1], search: '' }]), []); }); Deno.test('delete events', async () => { diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index 38835c6..75e45b4 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -1,10 +1,9 @@ -import { NIP50 } from '@soapbox/nspec'; +import { NIP50, NostrFilter } from '@soapbox/nspec'; import { Conf } from '@/config.ts'; import { type DittoDB } from '@/db.ts'; import { Debug, Kysely, type NostrEvent, type NStore, type NStoreOpts, type SelectQueryBuilder } from '@/deps.ts'; import { normalizeFilters } from '@/filter.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; -import { type DittoFilter } from '@/interfaces/DittoFilter.ts'; import { isDittoInternalKind, isParameterizedReplaceableKind, isReplaceableKind } from '@/kinds.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; @@ -144,7 +143,7 @@ class EventsDB implements NStore { } /** Build the query for a filter. */ - getFilterQuery(db: Kysely, filter: DittoFilter): EventQuery { + getFilterQuery(db: Kysely, filter: NostrFilter): EventQuery { let query = db .selectFrom('events') .select([ @@ -162,7 +161,7 @@ class EventsDB implements NStore { for (const [key, value] of Object.entries(filter)) { if (value === undefined) continue; - switch (key as keyof DittoFilter) { + switch (key as keyof NostrFilter) { case 'ids': query = query.where('events.id', 'in', filter.ids!); break; @@ -221,7 +220,7 @@ class EventsDB implements NStore { } /** Combine filter queries into a single union query. */ - getEventsQuery(filters: DittoFilter[]) { + getEventsQuery(filters: NostrFilter[]) { return filters .map((filter) => this.#db.selectFrom(() => this.getFilterQuery(this.#db, filter).as('events')).selectAll()) .reduce((result, query) => result.unionAll(query)); @@ -237,7 +236,7 @@ class EventsDB implements NStore { } /** Get events for filters from the database. */ - async query(filters: DittoFilter[], opts: NStoreOpts = {}): Promise { + async query(filters: NostrFilter[], opts: NStoreOpts = {}): Promise { filters = normalizeFilters(filters); // Improves performance of `{ kinds: [0], authors: ['...'] }` queries. if (opts.signal?.aborted) return Promise.resolve([]); @@ -294,7 +293,7 @@ class EventsDB implements NStore { } /** Delete events from each table. Should be run in a transaction! */ - async deleteEventsTrx(db: Kysely, filters: DittoFilter[]) { + async deleteEventsTrx(db: Kysely, filters: NostrFilter[]) { if (!filters.length) return Promise.resolve(); this.#debug('DELETE', JSON.stringify(filters)); @@ -307,7 +306,7 @@ class EventsDB implements NStore { } /** Delete events based on filters from the database. */ - async remove(filters: DittoFilter[], _opts?: NStoreOpts): Promise { + async remove(filters: NostrFilter[], _opts?: NStoreOpts): Promise { if (!filters.length) return Promise.resolve(); this.#debug('DELETE', JSON.stringify(filters)); @@ -315,7 +314,7 @@ class EventsDB implements NStore { } /** Get number of events that would be returned by filters. */ - async count(filters: DittoFilter[], opts: NStoreOpts = {}): Promise<{ count: number; approximate: boolean }> { + async count(filters: NostrFilter[], opts: NStoreOpts = {}): Promise<{ count: number; approximate: boolean }> { if (opts.signal?.aborted) return Promise.reject(abortError()); if (!filters.length) return Promise.resolve({ count: 0, approximate: false }); diff --git a/src/storages/optimizer.ts b/src/storages/optimizer.ts index 2c029cf..68883f6 100644 --- a/src/storages/optimizer.ts +++ b/src/storages/optimizer.ts @@ -1,7 +1,7 @@ +import { NostrFilter } from '@soapbox/nspec'; import { Debug, NSet, type NStore, type NStoreOpts } from '@/deps.ts'; import { normalizeFilters } from '@/filter.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; -import { type DittoFilter } from '@/interfaces/DittoFilter.ts'; import { abortError } from '@/utils/abort.ts'; interface OptimizerOpts { @@ -32,7 +32,7 @@ class Optimizer implements NStore { ]); } - async query(filters: DittoFilter[], opts: NStoreOpts = {}): Promise { + async query(filters: NostrFilter[], opts: NStoreOpts = {}): Promise { if (opts?.signal?.aborted) return Promise.reject(abortError()); filters = normalizeFilters(filters); diff --git a/src/storages/search-store.ts b/src/storages/search-store.ts index 2aabbf8..115f60d 100644 --- a/src/storages/search-store.ts +++ b/src/storages/search-store.ts @@ -1,8 +1,7 @@ -import { NRelay1 } from '@soapbox/nspec'; +import { NostrFilter, NRelay1 } from '@soapbox/nspec'; import { Debug, type NostrEvent, type NStore, type NStoreOpts } from '@/deps.ts'; import { normalizeFilters } from '@/filter.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; -import { type DittoFilter } from '@/interfaces/DittoFilter.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { abortError } from '@/utils/abort.ts'; @@ -32,7 +31,7 @@ class SearchStore implements NStore { return Promise.reject(new Error('EVENT not implemented.')); } - async query(filters: DittoFilter[], opts?: NStoreOpts): Promise { + async query(filters: NostrFilter[], opts?: NStoreOpts): Promise { filters = normalizeFilters(filters); if (opts?.signal?.aborted) return Promise.reject(abortError()); diff --git a/src/subs.ts b/src/subs.ts index 32bdc5d..089f346 100644 --- a/src/subs.ts +++ b/src/subs.ts @@ -1,6 +1,6 @@ +import { NostrFilter } from '@soapbox/nspec'; import { Debug } from '@/deps.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; -import { type DittoFilter } from '@/interfaces/DittoFilter.ts'; import { Subscription } from '@/subscription.ts'; const debug = Debug('ditto:subs'); @@ -21,7 +21,7 @@ class SubscriptionStore { * } * ``` */ - sub(socket: unknown, id: string, filters: DittoFilter[]): Subscription { + sub(socket: unknown, id: string, filters: NostrFilter[]): Subscription { debug('sub', id, JSON.stringify(filters)); let subs = this.#store.get(socket); diff --git a/src/subscription.ts b/src/subscription.ts index 0a3c820..abdf7cc 100644 --- a/src/subscription.ts +++ b/src/subscription.ts @@ -1,13 +1,12 @@ -import { Machina, type NostrEvent } from '@/deps.ts'; -import { matchDittoFilters } from '@/filter.ts'; -import { type DittoFilter } from '@/interfaces/DittoFilter.ts'; +import { NostrFilter } from '@soapbox/nspec'; +import { Machina, matchFilters, type NostrEvent } from '@/deps.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; class Subscription implements AsyncIterable { - filters: DittoFilter[]; + filters: NostrFilter[]; #machina: Machina; - constructor(filters: DittoFilter[]) { + constructor(filters: NostrFilter[]) { this.filters = filters; this.#machina = new Machina(); } @@ -17,7 +16,8 @@ class Subscription implements AsyncIterable { } matches(event: DittoEvent): boolean { - return matchDittoFilters(this.filters, event); + // TODO: Match `search` field. + return matchFilters(this.filters, event); } close() { diff --git a/src/utils/api.ts b/src/utils/api.ts index 5595844..563cdb7 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -128,10 +128,10 @@ function buildLinkHeader(url: string, events: NostrEvent[]): string | undefined const firstEvent = events[0]; const lastEvent = events[events.length - 1]; - const { localDomain } = Conf; + const { origin } = Conf.url; const { pathname, search } = new URL(url); - const next = new URL(pathname + search, localDomain); - const prev = new URL(pathname + search, localDomain); + const next = new URL(pathname + search, origin); + const prev = new URL(pathname + search, origin); next.searchParams.set('until', String(lastEvent.created_at)); prev.searchParams.set('since', String(firstEvent.created_at)); From ad2261a37eed3e92073c46db4def04b88785eb46 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Mar 2024 11:55:21 -0500 Subject: [PATCH 5/7] Move accountLookup function into a separate module (to prevent circular dependencies) --- src/controllers/api/accounts.ts | 5 +++-- src/utils.ts | 15 --------------- src/utils/lookup.ts | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 src/utils/lookup.ts diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 3e6902a..d8aadb1 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -1,3 +1,4 @@ +import { NostrFilter } from '@soapbox/nspec'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { insertUser } from '@/db/users.ts'; @@ -8,13 +9,13 @@ import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { eventsDB } from '@/storages.ts'; import { addTag, deleteTag, findReplyTag, getTagSet } from '@/tags.ts'; import { uploadFile } from '@/upload.ts'; -import { lookupAccount, nostrNow } from '@/utils.ts'; +import { nostrNow } from '@/utils.ts'; import { createEvent, paginated, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts'; +import { lookupAccount } from '@/utils/lookup.ts'; import { renderAccounts, renderEventAccounts, renderStatuses } from '@/views.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { renderRelationship } from '@/views/mastodon/relationships.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; -import { NostrFilter } from '@/interfaces/DittoFilter.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; const usernameSchema = z diff --git a/src/utils.ts b/src/utils.ts index 0fcd920..d714ce7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,4 @@ import { type EventTemplate, getEventHash, nip19, type NostrEvent, z } from '@/deps.ts'; -import { getAuthor } from '@/queries.ts'; -import { nip05Cache } from '@/utils/nip05.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; /** Get the current time in Nostr format. */ @@ -55,18 +53,6 @@ function parseNip05(value: string): Nip05 { }; } -/** Resolve a bech32 or NIP-05 identifier to an account. */ -async function lookupAccount(value: string, signal = AbortSignal.timeout(3000)): Promise { - console.log(`Looking up ${value}`); - - const pubkey = bech32ToPubkey(value) || - await nip05Cache.fetch(value, { signal }).then(({ pubkey }) => pubkey).catch(() => undefined); - - if (pubkey) { - return getAuthor(pubkey); - } -} - /** Return the event's age in milliseconds. */ function eventAge(event: NostrEvent): number { return Date.now() - nostrDate(event.created_at).getTime(); @@ -153,7 +139,6 @@ export { isNostrId, isRelay, isURL, - lookupAccount, type Nip05, nostrDate, nostrNow, diff --git a/src/utils/lookup.ts b/src/utils/lookup.ts new file mode 100644 index 0000000..c0f52ec --- /dev/null +++ b/src/utils/lookup.ts @@ -0,0 +1,18 @@ +import { type NostrEvent } from '@/deps.ts'; +import { getAuthor } from '@/queries.ts'; +import { bech32ToPubkey } from '@/utils.ts'; +import { nip05Cache } from '@/utils/nip05.ts'; + +/** Resolve a bech32 or NIP-05 identifier to an account. */ +async function lookupAccount(value: string, signal = AbortSignal.timeout(3000)): Promise { + console.log(`Looking up ${value}`); + + const pubkey = bech32ToPubkey(value) || + await nip05Cache.fetch(value, { signal }).then(({ pubkey }) => pubkey).catch(() => undefined); + + if (pubkey) { + return getAuthor(pubkey); + } +} + +export { lookupAccount }; From dcd0728b191bedbb9ad284246ceba68bb205b95e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Mar 2024 11:56:30 -0500 Subject: [PATCH 6/7] EventsDB: fix domain test --- src/storages/events-db.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storages/events-db.test.ts b/src/storages/events-db.test.ts index f488f46..713c94f 100644 --- a/src/storages/events-db.test.ts +++ b/src/storages/events-db.test.ts @@ -40,7 +40,7 @@ Deno.test('query events with domain search filter', async () => { .execute(); assertEquals(await eventsDB.query([{ kinds: [1], search: 'domain:localhost:8000' }]), [event1]); - assertEquals(await eventsDB.query([{ kinds: [1], search: '' }]), []); + assertEquals(await eventsDB.query([{ kinds: [1], search: 'domain:example.com' }]), []); }); Deno.test('delete events', async () => { From b2bc46ac5769c4d6222bb8863d68967fe9ab2e9d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Mar 2024 12:01:38 -0500 Subject: [PATCH 7/7] Removed DittoFilter usages I missed earlier --- src/controllers/api/search.ts | 2 +- src/controllers/api/streaming.ts | 9 ++++++--- src/controllers/api/timelines.ts | 13 +++++++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index f335d85..ec85192 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -1,6 +1,6 @@ +import { NostrFilter } from '@soapbox/nspec'; import { AppController } from '@/app.ts'; import { nip19, type NostrEvent, z } from '@/deps.ts'; -import { type NostrFilter } from '@/interfaces/DittoFilter.ts'; import { booleanParamSchema } from '@/schema.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; import { searchStore } from '@/storages.ts'; diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index c11484e..e3486bc 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -1,6 +1,7 @@ +import { NostrFilter } from '@soapbox/nspec'; import { type AppController } from '@/app.ts'; +import { Conf } from '@/config.ts'; import { Debug, z } from '@/deps.ts'; -import { NostrFilter } from '@/interfaces/DittoFilter.ts'; import { getAuthor, getFeedPubkeys } from '@/queries.ts'; import { Sub } from '@/subs.ts'; import { bech32ToPubkey } from '@/utils.ts'; @@ -83,16 +84,18 @@ async function topicToFilter( query: Record, pubkey: string | undefined, ): Promise { + const { host } = Conf.url; + switch (topic) { case 'public': return { kinds: [1] }; case 'public:local': - return { kinds: [1], local: true }; + return { kinds: [1], search: `domain:${host}` }; case 'hashtag': if (query.tag) return { kinds: [1], '#t': [query.tag] }; break; case 'hashtag:local': - if (query.tag) return { kinds: [1], '#t': [query.tag], local: true }; + if (query.tag) return { kinds: [1], '#t': [query.tag], search: `domain:${host}` }; break; case 'user': // HACK: this puts the user's entire contacts list into RAM, diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index c1c669e..ebea935 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -1,6 +1,7 @@ +import { NostrFilter } from '@soapbox/nspec'; import { type AppContext, type AppController } from '@/app.ts'; +import { Conf } from '@/config.ts'; import { z } from '@/deps.ts'; -import { type NostrFilter } from '@/interfaces/DittoFilter.ts'; import { getFeedPubkeys } from '@/queries.ts'; import { booleanParamSchema } from '@/schema.ts'; import { eventsDB } from '@/storages.ts'; @@ -22,7 +23,15 @@ const publicQuerySchema = z.object({ const publicTimelineController: AppController = (c) => { const params = paginationSchema.parse(c.req.query()); const { local } = publicQuerySchema.parse(c.req.query()); - return renderStatuses(c, [{ kinds: [1], local, ...params }]); + const { host } = Conf.url; + + const filter: NostrFilter = { kinds: [1], ...params }; + + if (local) { + filter.search = `domain:${host}`; + } + + return renderStatuses(c, [filter]); }; const hashtagTimelineController: AppController = (c) => {