Merge branch 'nip50-domain' into 'main'
NIP-50 domain search See merge request soapbox-pub/ditto!117
This commit is contained in:
commit
84784cd46b
|
@ -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.1", "~/fixtures/": "./fixtures/" },
|
||||
"lint": {
|
||||
"include": ["src/", "scripts/"],
|
||||
"rules": {
|
||||
|
|
|
@ -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 { DittoFilter } from '@/interfaces/DittoFilter.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
|
||||
const usernameSchema = z
|
||||
|
@ -145,7 +146,7 @@ const accountStatusesController: AppController = async (c) => {
|
|||
}
|
||||
}
|
||||
|
||||
const filter: DittoFilter = {
|
||||
const filter: NostrFilter = {
|
||||
authors: [pubkey],
|
||||
kinds: [1],
|
||||
since,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NostrFilter } from '@soapbox/nspec';
|
||||
import { AppController } from '@/app.ts';
|
||||
import { nip19, type NostrEvent, z } from '@/deps.ts';
|
||||
import { type DittoFilter } 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<NostrEvent[]> {
|
||||
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<Nos
|
|||
}
|
||||
|
||||
/** Get filters to lookup the input value. */
|
||||
async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: AbortSignal): Promise<DittoFilter[]> {
|
||||
const filters: DittoFilter[] = [];
|
||||
async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: AbortSignal): Promise<NostrFilter[]> {
|
||||
const filters: NostrFilter[] = [];
|
||||
|
||||
const accounts = !type || type === 'accounts';
|
||||
const statuses = !type || type === 'statuses';
|
||||
|
|
|
@ -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 { DittoFilter } from '@/interfaces/DittoFilter.ts';
|
||||
import { getAuthor, getFeedPubkeys } from '@/queries.ts';
|
||||
import { Sub } from '@/subs.ts';
|
||||
import { bech32ToPubkey } from '@/utils.ts';
|
||||
|
@ -82,17 +83,19 @@ async function topicToFilter(
|
|||
topic: Stream,
|
||||
query: Record<string, string>,
|
||||
pubkey: string | undefined,
|
||||
): Promise<DittoFilter | undefined> {
|
||||
): Promise<NostrFilter | undefined> {
|
||||
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,
|
||||
|
|
|
@ -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 DittoFilter } 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) => {
|
||||
|
@ -32,7 +41,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
|
||||
|
|
|
@ -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 <https://gitlab.com/soapbox-pub/ditto>
|
||||
`);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { Kysely } from '@/deps.ts';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
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<any>): Promise<void> {
|
||||
await db.schema.dropTable('pubkey_domains').execute();
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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<keyof DittoEvent, keyof NostrEvent>;
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
|
|
@ -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'`,
|
||||
|
|
|
@ -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<void
|
|||
|
||||
await Promise.all([
|
||||
storeEvent(event, signal),
|
||||
parseMetadata(event, signal),
|
||||
processDeletions(event, signal),
|
||||
trackRelays(event),
|
||||
trackHashtags(event),
|
||||
|
@ -74,6 +78,35 @@ async function storeEvent(event: DittoEvent, signal?: AbortSignal): Promise<void
|
|||
}
|
||||
}
|
||||
|
||||
/** Parse kind 0 metadata and track indexes in the database. */
|
||||
async function parseMetadata(event: NostrEvent, signal: AbortSignal): Promise<void> {
|
||||
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<void> {
|
||||
if (event.kind === 5) {
|
||||
|
|
|
@ -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<boolean> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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: 'domain:example.com' }]), []);
|
||||
});
|
||||
|
||||
Deno.test('delete events', async () => {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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';
|
||||
|
@ -143,7 +143,7 @@ class EventsDB implements NStore {
|
|||
}
|
||||
|
||||
/** Build the query for a filter. */
|
||||
getFilterQuery(db: Kysely<DittoDB>, filter: DittoFilter): EventQuery {
|
||||
getFilterQuery(db: Kysely<DittoDB>, filter: NostrFilter): EventQuery {
|
||||
let query = db
|
||||
.selectFrom('events')
|
||||
.select([
|
||||
|
@ -161,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;
|
||||
|
@ -192,23 +192,35 @@ class EventsDB implements NStore {
|
|||
}
|
||||
}
|
||||
|
||||
if (typeof filter.local === 'boolean') {
|
||||
if (filter.search) {
|
||||
const tokens = NIP50.parseInput(filter.search);
|
||||
|
||||
const domain = (tokens.find((t) =>
|
||||
typeof t === 'object' && t.key === 'domain'
|
||||
) as { key: 'domain'; value: string } | undefined)?.value;
|
||||
|
||||
if (domain) {
|
||||
query = query
|
||||
.leftJoin(() => this.usersQuery(), (join) => join.onRef('users.d_tag', '=', 'events.pubkey'))
|
||||
.where('users.d_tag', filter.local ? 'is not' : 'is', null);
|
||||
.innerJoin('pubkey_domains', 'pubkey_domains.pubkey', 'events.pubkey')
|
||||
.where('pubkey_domains.domain', '=', domain);
|
||||
}
|
||||
|
||||
if (filter.search) {
|
||||
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(filter.search));
|
||||
.where('events_fts.content', 'match', JSON.stringify(q));
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/** 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));
|
||||
|
@ -224,7 +236,7 @@ class EventsDB implements NStore {
|
|||
}
|
||||
|
||||
/** Get events for filters from the database. */
|
||||
async query(filters: DittoFilter[], opts: NStoreOpts = {}): Promise<DittoEvent[]> {
|
||||
async query(filters: NostrFilter[], opts: NStoreOpts = {}): Promise<DittoEvent[]> {
|
||||
filters = normalizeFilters(filters); // Improves performance of `{ kinds: [0], authors: ['...'] }` queries.
|
||||
|
||||
if (opts.signal?.aborted) return Promise.resolve([]);
|
||||
|
@ -281,7 +293,7 @@ class EventsDB implements NStore {
|
|||
}
|
||||
|
||||
/** Delete events from each table. Should be run in a transaction! */
|
||||
async deleteEventsTrx(db: Kysely<DittoDB>, filters: DittoFilter[]) {
|
||||
async deleteEventsTrx(db: Kysely<DittoDB>, filters: NostrFilter[]) {
|
||||
if (!filters.length) return Promise.resolve();
|
||||
this.#debug('DELETE', JSON.stringify(filters));
|
||||
|
||||
|
@ -294,7 +306,7 @@ class EventsDB implements NStore {
|
|||
}
|
||||
|
||||
/** Delete events based on filters from the database. */
|
||||
async remove(filters: DittoFilter[], _opts?: NStoreOpts): Promise<void> {
|
||||
async remove(filters: NostrFilter[], _opts?: NStoreOpts): Promise<void> {
|
||||
if (!filters.length) return Promise.resolve();
|
||||
this.#debug('DELETE', JSON.stringify(filters));
|
||||
|
||||
|
@ -302,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 });
|
||||
|
||||
|
|
|
@ -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<DittoEvent[]> {
|
||||
async query(filters: NostrFilter[], opts: NStoreOpts = {}): Promise<DittoEvent[]> {
|
||||
if (opts?.signal?.aborted) return Promise.reject(abortError());
|
||||
|
||||
filters = normalizeFilters(filters);
|
||||
|
|
|
@ -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<DittoEvent[]> {
|
||||
async query(filters: NostrFilter[], opts?: NStoreOpts): Promise<DittoEvent[]> {
|
||||
filters = normalizeFilters(filters);
|
||||
|
||||
if (opts?.signal?.aborted) return Promise.reject(abortError());
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<NostrEvent> {
|
||||
filters: DittoFilter[];
|
||||
filters: NostrFilter[];
|
||||
#machina: Machina<NostrEvent>;
|
||||
|
||||
constructor(filters: DittoFilter[]) {
|
||||
constructor(filters: NostrFilter[]) {
|
||||
this.filters = filters;
|
||||
this.#machina = new Machina();
|
||||
}
|
||||
|
@ -17,7 +16,8 @@ class Subscription implements AsyncIterable<NostrEvent> {
|
|||
}
|
||||
|
||||
matches(event: DittoEvent): boolean {
|
||||
return matchDittoFilters(this.filters, event);
|
||||
// TODO: Match `search` field.
|
||||
return matchFilters(this.filters, event);
|
||||
}
|
||||
|
||||
close() {
|
||||
|
|
15
src/utils.ts
15
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<NostrEvent | undefined> {
|
||||
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,
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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<NostrEvent | undefined> {
|
||||
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 };
|
Loading…
Reference in New Issue