diff --git a/src/config.ts b/src/config.ts index 246bf5a..8c2626a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -42,6 +42,10 @@ const Conf = { const { protocol, host } = Conf.url; return `${protocol === 'https:' ? 'wss:' : 'ws:'}//${host}/relay`; }, + /** Relay to use for NIP-50 `search` queries. */ + get searchRelay() { + return Deno.env.get('SEARCH_RELAY'); + }, /** Origin of the Ditto server, including the protocol and port. */ get localDomain() { return Deno.env.get('LOCAL_DOMAIN') || 'http://localhost:8000'; diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 1f34b07..2b640d5 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -3,7 +3,7 @@ import { type Event, nip19, z } from '@/deps.ts'; import { type DittoFilter } from '@/filter.ts'; import { booleanParamSchema } from '@/schema.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; -import { eventsDB } from '@/storages.ts'; +import { searchStore } from '@/storages.ts'; import { dedupeEvents } from '@/utils.ts'; import { lookupNip05Cached } from '@/utils/nip05.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts'; @@ -76,7 +76,7 @@ function searchEvents({ q, type, limit, account_id }: SearchQuery): Promise { const filters = await getLookupFilters(query); - const [event] = await eventsDB.getEvents(filters, { limit: 1, signal }); + const [event] = await searchStore.getEvents(filters, { limit: 1, signal }); return event; } diff --git a/src/storages.ts b/src/storages.ts index 15aadc6..045a362 100644 --- a/src/storages.ts +++ b/src/storages.ts @@ -1,6 +1,8 @@ +import { Conf } from '@/config.ts'; import { db } from '@/db.ts'; import { EventsDB } from '@/storages/events-db.ts'; import { Memorelay } from '@/storages/memorelay.ts'; +import { SearchStore } from '@/storages/search-store.ts'; /** SQLite database to store events this Ditto server cares about. */ const eventsDB = new EventsDB(db); @@ -8,4 +10,10 @@ const eventsDB = new EventsDB(db); /** In-memory data store for cached events. */ const memorelay = new Memorelay({ max: 3000 }); -export { eventsDB, memorelay }; +/** Storage to use for remote search. */ +const searchStore = new SearchStore({ + relay: Conf.searchRelay, + fallback: eventsDB, +}); + +export { eventsDB, memorelay, searchStore }; diff --git a/src/storages/search-store.ts b/src/storages/search-store.ts new file mode 100644 index 0000000..7c58a5f --- /dev/null +++ b/src/storages/search-store.ts @@ -0,0 +1,67 @@ +import { NiceRelay } from 'https://gitlab.com/soapbox-pub/nostr-machina/-/raw/5f4fb59c90c092e5aa59c01e6556a4bec264c167/mod.ts'; + +import { Debug, type Event, type Filter } from '@/deps.ts'; +import { type DittoFilter } from '@/filter.ts'; +import { type DittoEvent, type EventStore, type GetEventsOpts, type StoreEventOpts } from '@/storages/types.ts'; +import { EventSet } from '@/utils/event-set.ts'; + +interface SearchStoreOpts { + relay: string | undefined; + fallback: EventStore; +} + +class SearchStore implements EventStore { + #debug = Debug('ditto:storages:search'); + + #fallback: EventStore; + #relay: NiceRelay | undefined; + + supportedNips = [50]; + + constructor(opts: SearchStoreOpts) { + this.#fallback = opts.fallback; + + if (opts.relay) { + this.#relay = new NiceRelay(opts.relay); + } + } + + storeEvent(_event: Event, _opts?: StoreEventOpts | undefined): Promise { + throw new Error('EVENT not implemented.'); + } + + async getEvents( + filters: DittoFilter[], + opts?: GetEventsOpts | undefined, + ): Promise[]> { + this.#debug('REQ', JSON.stringify(filters)); + + if (this.#relay) { + this.#debug(`Searching for "${filters[0]?.search}" at ${this.#relay.socket.url}...`); + + const sub = this.#relay.req(filters, opts); + sub.eoseSignal.onabort = () => sub.close(); + const events = new EventSet>(); + + for await (const event of sub) { + this.#debug('EVENT', JSON.stringify(event)); + events.add(event); + } + + return [...events]; + } else { + this.#debug(`Searching for "${filters[0]?.search}" locally...`); + return this.#fallback.getEvents(filters, opts); + } + } + + countEvents(_filters: Filter[]): Promise { + throw new Error('COUNT not implemented.'); + } + + deleteEvents(_filters: Filter[]): Promise { + throw new Error('DELETE not implemented.'); + } +} + +export { SearchStore };