Remove DittoFilter, use search instead of local
This commit is contained in:
parent
d17d4c846f
commit
c8b378ad10
|
@ -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": {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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] };
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
`);
|
||||
|
|
|
@ -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,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: '' }]), []);
|
||||
});
|
||||
|
||||
Deno.test('delete events', async () => {
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Reference in New Issue