Remove DittoFilter, use search instead of local

This commit is contained in:
Alex Gleason 2024-03-20 11:34:04 -05:00
parent d17d4c846f
commit c8b378ad10
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
17 changed files with 58 additions and 87 deletions

View File

@ -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": {

View File

@ -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,

View File

@ -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<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';

View File

@ -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<string, string>,
pubkey: string | undefined,
): Promise<DittoFilter | undefined> {
): Promise<NostrFilter | undefined> {
switch (topic) {
case 'public':
return { kinds: [1] };

View File

@ -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

View File

@ -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>
`);

View File

@ -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,
};

View File

@ -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;
}

View File

@ -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'`,

View File

@ -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);
}

View File

@ -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 () => {

View File

@ -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<DittoDB>, filter: DittoFilter): EventQuery {
getFilterQuery(db: Kysely<DittoDB>, 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<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([]);
@ -294,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));
@ -307,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));
@ -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 });

View File

@ -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);

View File

@ -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());

View File

@ -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);

View File

@ -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() {

View File

@ -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));