From c8b378ad10ab8efbc79b21a29738872f2e6822d9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Mar 2024 11:34:04 -0500 Subject: [PATCH] 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 b5fe33b4..a552b644 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 21522089..3e6902a5 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 8d680fbf..f335d855 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 5988504a..c11484eb 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 885b8711..c1c669ee 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 b7f34dd4..751e60ef 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 2e4d5776..79e5b273 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 bcc17191..60467632 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 88758473..fdce5c75 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 c6be412f..85a26cf6 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 744935b5..f488f460 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 38835c66..75e45b42 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 2c029cf4..68883f62 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 2aabbf8d..115f60d5 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 32bdc5d3..089f3462 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 0a3c820e..abdf7cc8 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 5595844e..563cdb79 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));