Merge branch 'interfaces' into 'main'

Add new DittoEvent and DittoFilter interfaces

See merge request soapbox-pub/ditto!105
This commit is contained in:
Alex Gleason 2024-01-23 18:39:33 +00:00
commit 0a58233b4e
44 changed files with 316 additions and 466 deletions

View File

@ -5,12 +5,12 @@ import {
type Context, type Context,
cors, cors,
Debug, Debug,
type Event,
type Handler, type Handler,
Hono, Hono,
type HonoEnv, type HonoEnv,
logger, logger,
type MiddlewareHandler, type MiddlewareHandler,
type NostrEvent,
sentryMiddleware, sentryMiddleware,
serveStatic, serveStatic,
} from '@/deps.ts'; } from '@/deps.ts';
@ -90,7 +90,7 @@ interface AppEnv extends HonoEnv {
/** Hex secret key for the current user. Optional, but easiest way to use legacy Mastodon apps. */ /** Hex secret key for the current user. Optional, but easiest way to use legacy Mastodon apps. */
seckey?: string; seckey?: string;
/** NIP-98 signed event proving the pubkey is owned by the user. */ /** NIP-98 signed event proving the pubkey is owned by the user. */
proof?: Event<27235>; proof?: NostrEvent;
/** User associated with the pubkey, if any. */ /** User associated with the pubkey, if any. */
user?: User; user?: User;
}; };

View File

@ -1,13 +1,12 @@
import { type AppController } from '@/app.ts'; import { type AppController } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { insertUser } from '@/db/users.ts'; import { insertUser } from '@/db/users.ts';
import { findReplyTag, nip19, z } from '@/deps.ts'; import { nip19, z } from '@/deps.ts';
import { type DittoFilter } from '@/filter.ts';
import { getAuthor, getFollowedPubkeys } from '@/queries.ts'; import { getAuthor, getFollowedPubkeys } from '@/queries.ts';
import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts';
import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { eventsDB } from '@/storages.ts'; import { eventsDB } from '@/storages.ts';
import { addTag, deleteTag, getTagSet } from '@/tags.ts'; import { addTag, deleteTag, findReplyTag, getTagSet } from '@/tags.ts';
import { uploadFile } from '@/upload.ts'; import { uploadFile } from '@/upload.ts';
import { lookupAccount, nostrNow } from '@/utils.ts'; import { lookupAccount, nostrNow } from '@/utils.ts';
import { createEvent, paginated, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts'; import { createEvent, paginated, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
@ -15,6 +14,7 @@ import { renderAccounts, renderEventAccounts, renderStatuses } from '@/views.ts'
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
import { renderRelationship } from '@/views/mastodon/relationships.ts'; import { renderRelationship } from '@/views/mastodon/relationships.ts';
import { renderStatus } from '@/views/mastodon/statuses.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts';
import { DittoFilter } from '@/interfaces/DittoFilter.ts';
const usernameSchema = z const usernameSchema = z
.string().min(1).max(30) .string().min(1).max(30)
@ -143,7 +143,7 @@ const accountStatusesController: AppController = async (c) => {
} }
} }
const filter: DittoFilter<1> = { const filter: DittoFilter = {
authors: [pubkey], authors: [pubkey],
kinds: [1], kinds: [1],
relations: ['author', 'event_stats', 'author_stats'], relations: ['author', 'event_stats', 'author_stats'],
@ -159,7 +159,7 @@ const accountStatusesController: AppController = async (c) => {
let events = await eventsDB.filter([filter]); let events = await eventsDB.filter([filter]);
if (exclude_replies) { if (exclude_replies) {
events = events.filter((event) => !findReplyTag(event)); events = events.filter((event) => !findReplyTag(event.tags));
} }
const statuses = await Promise.all(events.map((event) => renderStatus(event, c.get('pubkey')))); const statuses = await Promise.all(events.map((event) => renderStatus(event, c.get('pubkey'))));

View File

@ -1,6 +1,6 @@
import { AppController } from '@/app.ts'; import { AppController } from '@/app.ts';
import { type Event, nip19, z } from '@/deps.ts'; import { nip19, type NostrEvent, z } from '@/deps.ts';
import { type DittoFilter } from '@/filter.ts'; import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
import { booleanParamSchema } from '@/schema.ts'; import { booleanParamSchema } from '@/schema.ts';
import { nostrIdSchema } from '@/schemas/nostr.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts';
import { searchStore } from '@/storages.ts'; import { searchStore } from '@/storages.ts';
@ -46,12 +46,12 @@ const searchController: AppController = async (c) => {
const [accounts, statuses] = await Promise.all([ const [accounts, statuses] = await Promise.all([
Promise.all( Promise.all(
results results
.filter((event): event is Event<0> => event.kind === 0) .filter((event): event is NostrEvent => event.kind === 0)
.map((event) => renderAccount(event)), .map((event) => renderAccount(event)),
), ),
Promise.all( Promise.all(
results results
.filter((event): event is Event<1> => event.kind === 1) .filter((event): event is NostrEvent => event.kind === 1)
.map((event) => renderStatus(event, c.get('pubkey'))), .map((event) => renderStatus(event, c.get('pubkey'))),
), ),
]); ]);
@ -64,7 +64,7 @@ const searchController: AppController = async (c) => {
}; };
/** Get events for the search params. */ /** Get events for the search params. */
function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: AbortSignal): Promise<Event[]> { function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: AbortSignal): Promise<NostrEvent[]> {
if (type === 'hashtags') return Promise.resolve([]); if (type === 'hashtags') return Promise.resolve([]);
const filter: DittoFilter = { const filter: DittoFilter = {
@ -94,7 +94,7 @@ function typeToKinds(type: SearchQuery['type']): number[] {
} }
/** Resolve a searched value into an event, if applicable. */ /** Resolve a searched value into an event, if applicable. */
async function lookupEvent(query: SearchQuery, signal: AbortSignal): Promise<Event | undefined> { async function lookupEvent(query: SearchQuery, signal: AbortSignal): Promise<NostrEvent | undefined> {
const filters = await getLookupFilters(query, signal); const filters = await getLookupFilters(query, signal);
const [event] = await searchStore.filter(filters, { limit: 1, signal }); const [event] = await searchStore.filter(filters, { limit: 1, signal });
return event; return event;

View File

@ -1,7 +1,7 @@
import { type AppController } from '@/app.ts'; import { type AppController } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { getUnattachedMediaByIds } from '@/db/unattached-media.ts'; import { getUnattachedMediaByIds } from '@/db/unattached-media.ts';
import { type Event, ISO6391, z } from '@/deps.ts'; import { ISO6391, type NostrEvent, z } from '@/deps.ts';
import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts'; import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts';
import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { addTag, deleteTag } from '@/tags.ts'; import { addTag, deleteTag } from '@/tags.ts';
@ -100,7 +100,7 @@ const contextController: AppController = async (c) => {
const id = c.req.param('id'); const id = c.req.param('id');
const event = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'] }); const event = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'] });
async function renderStatuses(events: Event<1>[]) { async function renderStatuses(events: NostrEvent[]) {
const statuses = await Promise.all(events.map((event) => renderStatus(event, c.get('pubkey')))); const statuses = await Promise.all(events.map((event) => renderStatus(event, c.get('pubkey'))));
return statuses.filter(Boolean); return statuses.filter(Boolean);
} }

View File

@ -1,6 +1,6 @@
import { type AppController } from '@/app.ts'; import { type AppController } from '@/app.ts';
import { Debug, z } from '@/deps.ts'; import { Debug, z } from '@/deps.ts';
import { type DittoFilter } from '@/filter.ts'; import { DittoFilter } from '@/interfaces/DittoFilter.ts';
import { getAuthor, getFeedPubkeys } from '@/queries.ts'; import { getAuthor, getFeedPubkeys } from '@/queries.ts';
import { Sub } from '@/subs.ts'; import { Sub } from '@/subs.ts';
import { bech32ToPubkey } from '@/utils.ts'; import { bech32ToPubkey } from '@/utils.ts';
@ -86,7 +86,7 @@ async function topicToFilter(
topic: Stream, topic: Stream,
pubkey: string, pubkey: string,
query: Record<string, string>, query: Record<string, string>,
): Promise<DittoFilter<1> | undefined> { ): Promise<DittoFilter | undefined> {
switch (topic) { switch (topic) {
case 'public': case 'public':
return { kinds: [1] }; return { kinds: [1] };

View File

@ -1,13 +1,12 @@
import { eventsDB } from '@/storages.ts'; import { type AppContext, type AppController } from '@/app.ts';
import { z } from '@/deps.ts'; import { z } from '@/deps.ts';
import { type DittoFilter } from '@/filter.ts'; import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
import { getFeedPubkeys } from '@/queries.ts'; import { getFeedPubkeys } from '@/queries.ts';
import { booleanParamSchema } from '@/schema.ts'; import { booleanParamSchema } from '@/schema.ts';
import { eventsDB } from '@/storages.ts';
import { paginated, paginationSchema } from '@/utils/api.ts'; import { paginated, paginationSchema } from '@/utils/api.ts';
import { renderStatus } from '@/views/mastodon/statuses.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts';
import type { AppContext, AppController } from '@/app.ts';
const homeTimelineController: AppController = async (c) => { const homeTimelineController: AppController = async (c) => {
const params = paginationSchema.parse(c.req.query()); const params = paginationSchema.parse(c.req.query());
const pubkey = c.get('pubkey')!; const pubkey = c.get('pubkey')!;
@ -32,7 +31,7 @@ const hashtagTimelineController: AppController = (c) => {
}; };
/** Render statuses for timelines. */ /** Render statuses for timelines. */
async function renderStatuses(c: AppContext, filters: DittoFilter<1>[], signal = AbortSignal.timeout(1000)) { async function renderStatuses(c: AppContext, filters: DittoFilter[], signal = AbortSignal.timeout(1000)) {
const events = await eventsDB.filter( const events = await eventsDB.filter(
filters.map((filter) => ({ ...filter, relations: ['author', 'event_stats', 'author_stats'] })), filters.map((filter) => ({ ...filter, relations: ['author', 'event_stats', 'author_stats'] })),
{ signal }, { signal },

View File

@ -13,14 +13,14 @@ import {
import { Sub } from '@/subs.ts'; import { Sub } from '@/subs.ts';
import type { AppController } from '@/app.ts'; import type { AppController } from '@/app.ts';
import type { Event, Filter } from '@/deps.ts'; import type { NostrEvent, NostrFilter } from '@/deps.ts';
/** Limit of initial events returned for a subscription. */ /** Limit of initial events returned for a subscription. */
const FILTER_LIMIT = 100; const FILTER_LIMIT = 100;
/** NIP-01 relay to client message. */ /** NIP-01 relay to client message. */
type RelayMsg = type RelayMsg =
| ['EVENT', string, Event] | ['EVENT', string, NostrEvent]
| ['NOTICE', string] | ['NOTICE', string]
| ['EOSE', string] | ['EOSE', string]
| ['OK', string, boolean, string] | ['OK', string, boolean, string]
@ -109,7 +109,7 @@ function connectStream(socket: WebSocket) {
} }
/** Enforce the filters with certain criteria. */ /** Enforce the filters with certain criteria. */
function prepareFilters(filters: ClientREQ[2][]): Filter[] { function prepareFilters(filters: ClientREQ[2][]): NostrFilter[] {
return filters.map((filter) => ({ return filters.map((filter) => ({
...filter, ...filter,
// Return only local events unless the query is already narrow. // Return only local events unless the query is already narrow.

View File

@ -1,5 +1,5 @@
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { Debug, type Filter } from '@/deps.ts'; import { Debug, type NostrFilter } from '@/deps.ts';
import * as pipeline from '@/pipeline.ts'; import * as pipeline from '@/pipeline.ts';
import { signAdminEvent } from '@/sign.ts'; import { signAdminEvent } from '@/sign.ts';
import { eventsDB } from '@/storages.ts'; import { eventsDB } from '@/storages.ts';
@ -49,7 +49,7 @@ async function insertUser(user: User) {
* ``` * ```
*/ */
async function findUser(user: Partial<User>): Promise<User | undefined> { async function findUser(user: Partial<User>): Promise<User | undefined> {
const filter: Filter = { kinds: [30361], authors: [Conf.pubkey], limit: 1 }; const filter: NostrFilter = { kinds: [30361], authors: [Conf.pubkey], limit: 1 };
for (const [key, value] of Object.entries(user)) { for (const [key, value] of Object.entries(user)) {
switch (key) { switch (key) {

View File

@ -11,9 +11,7 @@ export { cors, logger, serveStatic } from 'https://deno.land/x/hono@v3.10.1/midd
export { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts'; export { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts';
export { RelayPoolWorker } from 'https://dev.jspm.io/nostr-relaypool@0.6.30'; export { RelayPoolWorker } from 'https://dev.jspm.io/nostr-relaypool@0.6.30';
export { export {
type Event,
type EventTemplate, type EventTemplate,
type Filter,
finishEvent, finishEvent,
getEventHash, getEventHash,
getPublicKey, getPublicKey,
@ -29,7 +27,6 @@ export {
type VerifiedEvent, type VerifiedEvent,
verifySignature, verifySignature,
} from 'npm:nostr-tools@^1.17.0'; } from 'npm:nostr-tools@^1.17.0';
export { findReplyTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts';
export { parseFormData } from 'npm:formdata-helper@^0.3.0'; export { parseFormData } from 'npm:formdata-helper@^0.3.0';
// @deno-types="npm:@types/lodash@4.14.194" // @deno-types="npm:@types/lodash@4.14.194"
export { default as lodash } from 'https://esm.sh/lodash@4.17.21'; export { default as lodash } from 'https://esm.sh/lodash@4.17.21';
@ -86,11 +83,14 @@ export { EventEmitter } from 'npm:tseep@^1.1.3';
export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0'; export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0';
// @deno-types="npm:@types/debug@^4.1.12" // @deno-types="npm:@types/debug@^4.1.12"
export { default as Debug } from 'npm:debug@^4.3.4'; export { default as Debug } from 'npm:debug@^4.3.4';
export { NSet } from 'https://gitlab.com/soapbox-pub/nset/-/raw/b3c5601612f9bd277626198c5534e0796e003884/mod.ts';
export { export {
LNURL, LNURL,
type LNURLDetails, type LNURLDetails,
type MapCache, type MapCache,
NIP05, NIP05,
type NostrEvent,
type NostrFilter,
} from 'https://gitlab.com/soapbox-pub/nlib/-/raw/5d711597f3b2a163817cc1fb0f1f3ce8cede7cf7/mod.ts'; } from 'https://gitlab.com/soapbox-pub/nlib/-/raw/5d711597f3b2a163817cc1fb0f1f3ce8cede7cf7/mod.ts';
export type * as TypeFest from 'npm:type-fest@^4.3.0'; export type * as TypeFest from 'npm:type-fest@^4.3.0';

16
src/events.ts Normal file
View File

@ -0,0 +1,16 @@
import { type NostrEvent } from '@/deps.ts';
/** Return a normalized event without any non-standard keys. */
function cleanEvent(event: NostrEvent): NostrEvent {
return {
id: event.id,
pubkey: event.pubkey,
kind: event.kind,
content: event.content,
tags: event.tags,
sig: event.sig,
created_at: event.created_at,
};
}
export { cleanEvent };

View File

@ -1,4 +1,3 @@
import { type Event } from '@/deps.ts';
import { assertEquals } from '@/deps-test.ts'; import { assertEquals } from '@/deps-test.ts';
import event0 from '~/fixtures/events/event-0.json' assert { type: 'json' }; import event0 from '~/fixtures/events/event-0.json' assert { type: 'json' };
@ -7,7 +6,7 @@ import event1 from '~/fixtures/events/event-1.json' assert { type: 'json' };
import { eventToMicroFilter, getFilterId, getFilterLimit, getMicroFilters, isMicrofilter } from './filter.ts'; import { eventToMicroFilter, getFilterId, getFilterLimit, getMicroFilters, isMicrofilter } from './filter.ts';
Deno.test('getMicroFilters', () => { Deno.test('getMicroFilters', () => {
const event = event0 as Event<0>; const event = event0;
const microfilters = getMicroFilters(event); const microfilters = getMicroFilters(event);
assertEquals(microfilters.length, 2); assertEquals(microfilters.length, 2);
assertEquals(microfilters[0], { authors: [event.pubkey], kinds: [0] }); assertEquals(microfilters[0], { authors: [event.pubkey], kinds: [0] });

View File

@ -1,24 +1,14 @@
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { type Event, type Filter, matchFilters, stringifyStable, z } from '@/deps.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 { isReplaceableKind } from '@/kinds.ts'; import { isReplaceableKind } from '@/kinds.ts';
import { nostrIdSchema } from '@/schemas/nostr.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts';
import { type DittoEvent } from '@/storages/types.ts';
/** Additional properties that may be added by Ditto to events. */
type Relation = 'author' | 'author_stats' | 'event_stats';
/** Custom filter interface that extends Nostr filters with extra options for Ditto. */
interface DittoFilter<K extends number = number> extends Filter<K> {
/** Whether the event was authored by a local user. */
local?: boolean;
/** Additional fields to add to the returned event. */
relations?: Relation[];
}
/** Microfilter to get one specific event by ID. */ /** Microfilter to get one specific event by ID. */
type IdMicrofilter = { ids: [Event['id']] }; type IdMicrofilter = { ids: [NostrEvent['id']] };
/** Microfilter to get an author. */ /** Microfilter to get an author. */
type AuthorMicrofilter = { kinds: [0]; authors: [Event['pubkey']] }; type AuthorMicrofilter = { kinds: [0]; authors: [NostrEvent['pubkey']] };
/** Filter to get one specific event. */ /** Filter to get one specific event. */
type MicroFilter = IdMicrofilter | AuthorMicrofilter; type MicroFilter = IdMicrofilter | AuthorMicrofilter;
@ -57,13 +47,13 @@ function getFilterId(filter: MicroFilter): string {
} }
/** Get a microfilter from a Nostr event. */ /** Get a microfilter from a Nostr event. */
function eventToMicroFilter(event: Event): MicroFilter { function eventToMicroFilter(event: NostrEvent): MicroFilter {
const [microfilter] = getMicroFilters(event); const [microfilter] = getMicroFilters(event);
return microfilter; return microfilter;
} }
/** Get all the microfilters for an event, in order of priority. */ /** Get all the microfilters for an event, in order of priority. */
function getMicroFilters(event: Event): MicroFilter[] { function getMicroFilters(event: NostrEvent): MicroFilter[] {
const microfilters: MicroFilter[] = []; const microfilters: MicroFilter[] = [];
if (event.kind === 0) { if (event.kind === 0) {
microfilters.push({ kinds: [0], authors: [event.pubkey] }); microfilters.push({ kinds: [0], authors: [event.pubkey] });
@ -79,12 +69,12 @@ const microFilterSchema = z.union([
]); ]);
/** Checks whether the filter is a microfilter. */ /** Checks whether the filter is a microfilter. */
function isMicrofilter(filter: Filter): filter is MicroFilter { function isMicrofilter(filter: NostrFilter): filter is MicroFilter {
return microFilterSchema.safeParse(filter).success; return microFilterSchema.safeParse(filter).success;
} }
/** Calculate the intrinsic limit of a filter. */ /** Calculate the intrinsic limit of a filter. */
function getFilterLimit(filter: Filter): number { function getFilterLimit(filter: NostrFilter): number {
if (filter.ids && !filter.ids.length) return 0; if (filter.ids && !filter.ids.length) return 0;
if (filter.kinds && !filter.kinds.length) return 0; if (filter.kinds && !filter.kinds.length) return 0;
if (filter.authors && !filter.authors.length) return 0; if (filter.authors && !filter.authors.length) return 0;
@ -100,12 +90,12 @@ function getFilterLimit(filter: Filter): number {
} }
/** Returns true if the filter could potentially return any stored events at all. */ /** Returns true if the filter could potentially return any stored events at all. */
function canFilter(filter: Filter): boolean { function canFilter(filter: NostrFilter): boolean {
return getFilterLimit(filter) > 0; return getFilterLimit(filter) > 0;
} }
/** Normalize the `limit` of each filter, and remove filters that can't produce any events. */ /** Normalize the `limit` of each filter, and remove filters that can't produce any events. */
function normalizeFilters<F extends Filter>(filters: F[]): F[] { function normalizeFilters<F extends NostrFilter>(filters: F[]): F[] {
return filters.reduce<F[]>((acc, filter) => { return filters.reduce<F[]>((acc, filter) => {
const limit = getFilterLimit(filter); const limit = getFilterLimit(filter);
if (limit > 0) { if (limit > 0) {
@ -118,7 +108,6 @@ function normalizeFilters<F extends Filter>(filters: F[]): F[] {
export { export {
type AuthorMicrofilter, type AuthorMicrofilter,
canFilter, canFilter,
type DittoFilter,
eventToMicroFilter, eventToMicroFilter,
getFilterId, getFilterId,
getFilterLimit, getFilterLimit,
@ -128,5 +117,4 @@ export {
matchDittoFilters, matchDittoFilters,
type MicroFilter, type MicroFilter,
normalizeFilters, normalizeFilters,
type Relation,
}; };

View File

@ -1,4 +1,4 @@
import { Debug, type Event } from '@/deps.ts'; import { Debug, type NostrEvent } from '@/deps.ts';
import { activeRelays, pool } from '@/pool.ts'; import { activeRelays, pool } from '@/pool.ts';
import { nostrNow } from '@/utils.ts'; import { nostrNow } from '@/utils.ts';
@ -18,8 +18,8 @@ pool.subscribe(
); );
/** Handle events through the firehose pipeline. */ /** Handle events through the firehose pipeline. */
function handleEvent(event: Event): Promise<void> { function handleEvent(event: NostrEvent): Promise<void> {
debug(`Event<${event.kind}> ${event.id}`); debug(`NostrEvent<${event.kind}> ${event.id}`);
return pipeline return pipeline
.handleEvent(event) .handleEvent(event)

View File

@ -0,0 +1,24 @@
import { type NostrEvent } from '@/deps.ts';
/** Ditto internal stats for the event's author. */
export interface AuthorStats {
followers_count: number;
following_count: number;
notes_count: number;
}
/** Ditto internal stats for the event. */
export interface EventStats {
replies_count: number;
reposts_count: number;
reactions_count: number;
}
/** Internal Event representation used by Ditto, including extra keys. */
export interface DittoEvent extends NostrEvent {
author?: DittoEvent;
author_stats?: AuthorStats;
event_stats?: EventStats;
d_author?: DittoEvent;
user?: DittoEvent;
}

View File

@ -0,0 +1,14 @@
import { type NostrEvent, type NostrFilter } 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;
/** Additional fields to add to the returned event. */
relations?: DittoRelation[];
}

View File

@ -1,5 +1,5 @@
import { type AppContext, type AppMiddleware } from '@/app.ts'; import { type AppContext, type AppMiddleware } from '@/app.ts';
import { type Event, HTTPException } from '@/deps.ts'; import { HTTPException, type NostrEvent } from '@/deps.ts';
import { import {
buildAuthEventTemplate, buildAuthEventTemplate,
parseAuthRequest, parseAuthRequest,
@ -65,7 +65,7 @@ function matchesRole(user: User, role: UserRole): boolean {
/** HOC to obtain proof in middleware. */ /** HOC to obtain proof in middleware. */
function withProof( function withProof(
handler: (c: AppContext, proof: Event<27235>, next: () => Promise<void>) => Promise<void>, handler: (c: AppContext, proof: NostrEvent, next: () => Promise<void>) => Promise<void>,
opts?: ParseAuthRequestOpts, opts?: ParseAuthRequestOpts,
): AppMiddleware { ): AppMiddleware {
return async (c, next) => { return async (c, next) => {

View File

@ -2,7 +2,8 @@ import { Conf } from '@/config.ts';
import { encryptAdmin } from '@/crypto.ts'; import { encryptAdmin } from '@/crypto.ts';
import { addRelays } from '@/db/relays.ts'; import { addRelays } from '@/db/relays.ts';
import { deleteAttachedMedia } from '@/db/unattached-media.ts'; import { deleteAttachedMedia } from '@/db/unattached-media.ts';
import { Debug, type Event, LNURL } from '@/deps.ts'; import { Debug, LNURL, type NostrEvent } from '@/deps.ts';
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
import { isEphemeralKind } from '@/kinds.ts'; import { isEphemeralKind } from '@/kinds.ts';
import { isLocallyFollowed } from '@/queries.ts'; import { isLocallyFollowed } from '@/queries.ts';
import { updateStats } from '@/stats.ts'; import { updateStats } from '@/stats.ts';
@ -15,7 +16,6 @@ import { TrendsWorker } from '@/workers/trends.ts';
import { verifySignatureWorker } from '@/workers/verify.ts'; import { verifySignatureWorker } from '@/workers/verify.ts';
import { signAdminEvent } from '@/sign.ts'; import { signAdminEvent } from '@/sign.ts';
import { lnurlCache } from '@/utils/lnurl.ts'; import { lnurlCache } from '@/utils/lnurl.ts';
import { DittoEvent } from '@/storages/types.ts';
const debug = Debug('ditto:pipeline'); const debug = Debug('ditto:pipeline');
@ -28,7 +28,7 @@ async function handleEvent(event: DittoEvent): Promise<void> {
if (!(await verifySignatureWorker(event))) return; if (!(await verifySignatureWorker(event))) return;
const wanted = reqmeister.isWanted(event); const wanted = reqmeister.isWanted(event);
if (await encounterEvent(event)) return; if (await encounterEvent(event)) return;
debug(`Event<${event.kind}> ${event.id}`); debug(`NostrEvent<${event.kind}> ${event.id}`);
await hydrateEvent(event); await hydrateEvent(event);
await Promise.all([ await Promise.all([
@ -45,7 +45,7 @@ async function handleEvent(event: DittoEvent): Promise<void> {
} }
/** Encounter the event, and return whether it has already been encountered. */ /** Encounter the event, and return whether it has already been encountered. */
async function encounterEvent(event: Event): Promise<boolean> { async function encounterEvent(event: NostrEvent): Promise<boolean> {
const preexisting = (await memorelay.count([{ ids: [event.id] }])) > 0; const preexisting = (await memorelay.count([{ ids: [event.id] }])) > 0;
memorelay.add(event); memorelay.add(event);
reqmeister.add(event); reqmeister.add(event);
@ -59,7 +59,7 @@ async function hydrateEvent(event: DittoEvent): Promise<void> {
} }
/** Check if the pubkey is the `DITTO_NSEC` pubkey. */ /** Check if the pubkey is the `DITTO_NSEC` pubkey. */
const isAdminEvent = ({ pubkey }: Event): boolean => pubkey === Conf.pubkey; const isAdminEvent = ({ pubkey }: NostrEvent): boolean => pubkey === Conf.pubkey;
interface StoreEventOpts { interface StoreEventOpts {
force?: boolean; force?: boolean;
@ -89,7 +89,7 @@ async function storeEvent(event: DittoEvent, opts: StoreEventOpts = {}): Promise
} }
/** Query to-be-deleted events, ensure their pubkey matches, then delete them from the database. */ /** Query to-be-deleted events, ensure their pubkey matches, then delete them from the database. */
async function processDeletions(event: Event): Promise<void> { async function processDeletions(event: NostrEvent): Promise<void> {
if (event.kind === 5) { if (event.kind === 5) {
const ids = getTagSet(event.tags, 'e'); const ids = getTagSet(event.tags, 'e');
@ -108,7 +108,7 @@ async function processDeletions(event: Event): Promise<void> {
} }
/** Track whenever a hashtag is used, for processing trending tags. */ /** Track whenever a hashtag is used, for processing trending tags. */
async function trackHashtags(event: Event): Promise<void> { async function trackHashtags(event: NostrEvent): Promise<void> {
const date = nostrDate(event.created_at); const date = nostrDate(event.created_at);
const tags = event.tags const tags = event.tags
@ -127,7 +127,7 @@ async function trackHashtags(event: Event): Promise<void> {
} }
/** Tracks known relays in the database. */ /** Tracks known relays in the database. */
function trackRelays(event: Event) { function trackRelays(event: NostrEvent) {
const relays = new Set<`wss://${string}`>(); const relays = new Set<`wss://${string}`>();
event.tags.forEach((tag) => { event.tags.forEach((tag) => {
@ -208,10 +208,10 @@ async function payZap(event: DittoEvent, signal: AbortSignal) {
} }
/** Determine if the event is being received in a timely manner. */ /** Determine if the event is being received in a timely manner. */
const isFresh = (event: Event): boolean => eventAge(event) < Time.seconds(10); const isFresh = (event: NostrEvent): boolean => eventAge(event) < Time.seconds(10);
/** Distribute the event through active subscriptions. */ /** Distribute the event through active subscriptions. */
function streamOut(event: Event) { function streamOut(event: NostrEvent) {
if (!isFresh(event)) return; if (!isFresh(event)) return;
for (const sub of Sub.matches(event)) { for (const sub of Sub.matches(event)) {

View File

@ -1,37 +1,38 @@
import { eventsDB, memorelay, reqmeister } from '@/storages.ts'; import { eventsDB, memorelay, reqmeister } from '@/storages.ts';
import { Debug, type Event, findReplyTag } from '@/deps.ts'; import { Debug, type NostrEvent } from '@/deps.ts';
import { type AuthorMicrofilter, type DittoFilter, type IdMicrofilter, type Relation } from '@/filter.ts'; import { type AuthorMicrofilter, type IdMicrofilter } from '@/filter.ts';
import { type DittoEvent } from '@/storages/types.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { getTagSet } from '@/tags.ts'; import { type DittoFilter, type DittoRelation } from '@/interfaces/DittoFilter.ts';
import { findReplyTag, getTagSet } from '@/tags.ts';
const debug = Debug('ditto:queries'); const debug = Debug('ditto:queries');
interface GetEventOpts<K extends number> { interface GetEventOpts {
/** Signal to abort the request. */ /** Signal to abort the request. */
signal?: AbortSignal; signal?: AbortSignal;
/** Event kind. */ /** Event kind. */
kind?: K; kind?: number;
/** Relations to include on the event. */ /** Relations to include on the event. */
relations?: Relation[]; relations?: DittoRelation[];
} }
/** Get a Nostr event by its ID. */ /** Get a Nostr event by its ID. */
const getEvent = async <K extends number = number>( const getEvent = async (
id: string, id: string,
opts: GetEventOpts<K> = {}, opts: GetEventOpts = {},
): Promise<DittoEvent<K> | undefined> => { ): Promise<DittoEvent | undefined> => {
debug(`getEvent: ${id}`); debug(`getEvent: ${id}`);
const { kind, relations, signal = AbortSignal.timeout(1000) } = opts; const { kind, relations, signal = AbortSignal.timeout(1000) } = opts;
const microfilter: IdMicrofilter = { ids: [id] }; const microfilter: IdMicrofilter = { ids: [id] };
const [memoryEvent] = await memorelay.filter([microfilter], opts) as DittoEvent<K>[]; const [memoryEvent] = await memorelay.filter([microfilter], opts) as DittoEvent[];
if (memoryEvent && !relations) { if (memoryEvent && !relations) {
debug(`getEvent: ${id.slice(0, 8)} found in memory`); debug(`getEvent: ${id.slice(0, 8)} found in memory`);
return memoryEvent; return memoryEvent;
} }
const filter: DittoFilter<K> = { ids: [id], relations, limit: 1 }; const filter: DittoFilter = { ids: [id], relations, limit: 1 };
if (kind) { if (kind) {
filter.kinds = [kind]; filter.kinds = [kind];
} }
@ -61,7 +62,7 @@ const getEvent = async <K extends number = number>(
return memoryEvent; return memoryEvent;
} }
const reqEvent = await reqmeister.req(microfilter, opts).catch(() => undefined) as Event<K> | undefined; const reqEvent = await reqmeister.req(microfilter, opts).catch(() => undefined);
if (reqEvent) { if (reqEvent) {
debug(`getEvent: ${id.slice(0, 8)} found by reqmeister`); debug(`getEvent: ${id.slice(0, 8)} found by reqmeister`);
@ -72,7 +73,7 @@ const getEvent = async <K extends number = number>(
}; };
/** Get a Nostr `set_medatadata` event for a user's pubkey. */ /** Get a Nostr `set_medatadata` event for a user's pubkey. */
const getAuthor = async (pubkey: string, opts: GetEventOpts<0> = {}): Promise<Event<0> | undefined> => { const getAuthor = async (pubkey: string, opts: GetEventOpts = {}): Promise<NostrEvent | undefined> => {
const { relations, signal = AbortSignal.timeout(1000) } = opts; const { relations, signal = AbortSignal.timeout(1000) } = opts;
const microfilter: AuthorMicrofilter = { kinds: [0], authors: [pubkey] }; const microfilter: AuthorMicrofilter = { kinds: [0], authors: [pubkey] };
@ -94,7 +95,7 @@ const getAuthor = async (pubkey: string, opts: GetEventOpts<0> = {}): Promise<Ev
}; };
/** Get users the given pubkey follows. */ /** Get users the given pubkey follows. */
const getFollows = async (pubkey: string, signal?: AbortSignal): Promise<Event<3> | undefined> => { const getFollows = async (pubkey: string, signal?: AbortSignal): Promise<NostrEvent | undefined> => {
const [event] = await eventsDB.filter([{ authors: [pubkey], kinds: [3], limit: 1 }], { limit: 1, signal }); const [event] = await eventsDB.filter([{ authors: [pubkey], kinds: [3], limit: 1 }], { limit: 1, signal });
return event; return event;
}; };
@ -112,9 +113,9 @@ async function getFeedPubkeys(pubkey: string): Promise<string[]> {
return [...authors, pubkey]; return [...authors, pubkey];
} }
async function getAncestors(event: Event<1>, result = [] as Event<1>[]): Promise<Event<1>[]> { async function getAncestors(event: NostrEvent, result: NostrEvent[] = []): Promise<NostrEvent[]> {
if (result.length < 100) { if (result.length < 100) {
const replyTag = findReplyTag(event); const replyTag = findReplyTag(event.tags);
const inReplyTo = replyTag ? replyTag[1] : undefined; const inReplyTo = replyTag ? replyTag[1] : undefined;
if (inReplyTo) { if (inReplyTo) {
@ -130,7 +131,7 @@ async function getAncestors(event: Event<1>, result = [] as Event<1>[]): Promise
return result.reverse(); return result.reverse();
} }
function getDescendants(eventId: string, signal = AbortSignal.timeout(2000)): Promise<Event<1>[]> { function getDescendants(eventId: string, signal = AbortSignal.timeout(2000)): Promise<NostrEvent[]> {
return eventsDB.filter( return eventsDB.filter(
[{ kinds: [1], '#e': [eventId], relations: ['author', 'event_stats', 'author_stats'] }], [{ kinds: [1], '#e': [eventId], relations: ['author', 'event_stats', 'author_stats'] }],
{ limit: 200, signal }, { limit: 200, signal },

View File

@ -1,7 +1,7 @@
import { type AppContext } from '@/app.ts'; import { type AppContext } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { decryptAdmin, encryptAdmin } from '@/crypto.ts'; import { decryptAdmin, encryptAdmin } from '@/crypto.ts';
import { Debug, type Event, type EventTemplate, finishEvent, HTTPException } from '@/deps.ts'; import { Debug, type EventTemplate, finishEvent, HTTPException, type NostrEvent } from '@/deps.ts';
import { connectResponseSchema } from '@/schemas/nostr.ts'; import { connectResponseSchema } from '@/schemas/nostr.ts';
import { jsonSchema } from '@/schema.ts'; import { jsonSchema } from '@/schema.ts';
import { Sub } from '@/subs.ts'; import { Sub } from '@/subs.ts';
@ -21,11 +21,11 @@ interface SignEventOpts {
* - If a secret key is provided, it will be used to sign the event. * - If a secret key is provided, it will be used to sign the event.
* - If `X-Nostr-Sign` is passed, it will use NIP-46 to sign the event. * - If `X-Nostr-Sign` is passed, it will use NIP-46 to sign the event.
*/ */
async function signEvent<K extends number = number>( async function signEvent(
event: EventTemplate<K>, event: EventTemplate,
c: AppContext, c: AppContext,
opts: SignEventOpts = {}, opts: SignEventOpts = {},
): Promise<Event<K>> { ): Promise<NostrEvent> {
const seckey = c.get('seckey'); const seckey = c.get('seckey');
const header = c.req.header('x-nostr-sign'); const header = c.req.header('x-nostr-sign');
@ -45,11 +45,11 @@ async function signEvent<K extends number = number>(
} }
/** Sign event with NIP-46, waiting in the background for the signed event. */ /** Sign event with NIP-46, waiting in the background for the signed event. */
async function signNostrConnect<K extends number = number>( async function signNostrConnect(
event: EventTemplate<K>, event: EventTemplate,
c: AppContext, c: AppContext,
opts: SignEventOpts = {}, opts: SignEventOpts = {},
): Promise<Event<K>> { ): Promise<NostrEvent> {
const pubkey = c.get('pubkey'); const pubkey = c.get('pubkey');
if (!pubkey) { if (!pubkey) {
@ -73,16 +73,16 @@ async function signNostrConnect<K extends number = number>(
tags: [['p', pubkey]], tags: [['p', pubkey]],
}, c); }, c);
return awaitSignedEvent<K>(pubkey, messageId, event, c); return awaitSignedEvent(pubkey, messageId, event, c);
} }
/** Wait for signed event to be sent through Nostr relay. */ /** Wait for signed event to be sent through Nostr relay. */
async function awaitSignedEvent<K extends number = number>( async function awaitSignedEvent(
pubkey: string, pubkey: string,
messageId: string, messageId: string,
template: EventTemplate<K>, template: EventTemplate,
c: AppContext, c: AppContext,
): Promise<Event<K>> { ): Promise<NostrEvent> {
const sub = Sub.sub(messageId, '1', [{ kinds: [24133], authors: [pubkey], '#p': [Conf.pubkey] }]); const sub = Sub.sub(messageId, '1', [{ kinds: [24133], authors: [pubkey], '#p': [Conf.pubkey] }]);
function close(): void { function close(): void {
@ -103,7 +103,7 @@ async function awaitSignedEvent<K extends number = number>(
if (result.success) { if (result.success) {
close(); close();
clearTimeout(timeout); clearTimeout(timeout);
return result.data.result as Event<K>; return result.data.result;
} }
} }
@ -114,7 +114,7 @@ async function awaitSignedEvent<K extends number = number>(
/** Sign event as the Ditto server. */ /** Sign event as the Ditto server. */
// deno-lint-ignore require-await // deno-lint-ignore require-await
async function signAdminEvent<K extends number = number>(event: EventTemplate<K>): Promise<Event<K>> { async function signAdminEvent(event: EventTemplate): Promise<NostrEvent> {
return finishEvent(event, Conf.seckey); return finishEvent(event, Conf.seckey);
} }

View File

@ -1,6 +1,7 @@
import { type AuthorStatsRow, db, type DittoDB, type EventStatsRow } from '@/db.ts'; import { type AuthorStatsRow, db, type DittoDB, type EventStatsRow } from '@/db.ts';
import { Debug, type Event, findReplyTag, type InsertQueryBuilder } from '@/deps.ts'; import { Debug, type InsertQueryBuilder, type NostrEvent } from '@/deps.ts';
import { eventsDB } from '@/storages.ts'; import { eventsDB } from '@/storages.ts';
import { findReplyTag } from '@/tags.ts';
type AuthorStat = keyof Omit<AuthorStatsRow, 'pubkey'>; type AuthorStat = keyof Omit<AuthorStatsRow, 'pubkey'>;
type EventStat = keyof Omit<EventStatsRow, 'event_id'>; type EventStat = keyof Omit<EventStatsRow, 'event_id'>;
@ -12,15 +13,15 @@ type StatDiff = AuthorStatDiff | EventStatDiff;
const debug = Debug('ditto:stats'); const debug = Debug('ditto:stats');
/** Store stats for the event in LMDB. */ /** Store stats for the event in LMDB. */
async function updateStats<K extends number>(event: Event<K>) { async function updateStats(event: NostrEvent) {
let prev: Event<K> | undefined; let prev: NostrEvent | undefined;
const queries: InsertQueryBuilder<DittoDB, any, unknown>[] = []; const queries: InsertQueryBuilder<DittoDB, any, unknown>[] = [];
// Kind 3 is a special case - replace the count with the new list. // Kind 3 is a special case - replace the count with the new list.
if (event.kind === 3) { if (event.kind === 3) {
prev = await maybeGetPrev(event); prev = await maybeGetPrev(event);
if (!prev || event.created_at >= prev.created_at) { if (!prev || event.created_at >= prev.created_at) {
queries.push(updateFollowingCountQuery(event as Event<3>)); queries.push(updateFollowingCountQuery(event));
} }
} }
@ -41,11 +42,11 @@ async function updateStats<K extends number>(event: Event<K>) {
} }
/** Calculate stats changes ahead of time so we can build an efficient query. */ /** Calculate stats changes ahead of time so we can build an efficient query. */
function getStatsDiff<K extends number>(event: Event<K>, prev: Event<K> | undefined): StatDiff[] { function getStatsDiff(event: NostrEvent, prev: NostrEvent | undefined): StatDiff[] {
const statDiffs: StatDiff[] = []; const statDiffs: StatDiff[] = [];
const firstTaggedId = event.tags.find(([name]) => name === 'e')?.[1]; const firstTaggedId = event.tags.find(([name]) => name === 'e')?.[1];
const inReplyToId = findReplyTag(event as Event<1>)?.[1]; const inReplyToId = findReplyTag(event.tags)?.[1];
switch (event.kind) { switch (event.kind) {
case 1: case 1:
@ -55,7 +56,7 @@ function getStatsDiff<K extends number>(event: Event<K>, prev: Event<K> | undefi
} }
break; break;
case 3: case 3:
statDiffs.push(...getFollowDiff(event as Event<3>, prev as Event<3> | undefined)); statDiffs.push(...getFollowDiff(event, prev));
break; break;
case 6: case 6:
if (firstTaggedId) { if (firstTaggedId) {
@ -124,7 +125,7 @@ function eventStatsQuery(diffs: EventStatDiff[]) {
} }
/** Get the last version of the event, if any. */ /** Get the last version of the event, if any. */
async function maybeGetPrev<K extends number>(event: Event<K>): Promise<Event<K>> { async function maybeGetPrev(event: NostrEvent): Promise<NostrEvent> {
const [prev] = await eventsDB.filter([ const [prev] = await eventsDB.filter([
{ kinds: [event.kind], authors: [event.pubkey], limit: 1 }, { kinds: [event.kind], authors: [event.pubkey], limit: 1 },
]); ]);
@ -133,7 +134,7 @@ async function maybeGetPrev<K extends number>(event: Event<K>): Promise<Event<K>
} }
/** Set the following count to the total number of unique "p" tags in the follow list. */ /** Set the following count to the total number of unique "p" tags in the follow list. */
function updateFollowingCountQuery({ pubkey, tags }: Event<3>) { function updateFollowingCountQuery({ pubkey, tags }: NostrEvent) {
const following_count = new Set( const following_count = new Set(
tags tags
.filter(([name]) => name === 'p') .filter(([name]) => name === 'p')
@ -155,7 +156,7 @@ function updateFollowingCountQuery({ pubkey, tags }: Event<3>) {
} }
/** Compare the old and new follow events (if any), and return a diff array. */ /** Compare the old and new follow events (if any), and return a diff array. */
function getFollowDiff(event: Event<3>, prev?: Event<3>): AuthorStatDiff[] { function getFollowDiff(event: NostrEvent, prev?: NostrEvent): AuthorStatDiff[] {
const prevTags = prev?.tags ?? []; const prevTags = prev?.tags ?? [];
const prevPubkeys = new Set( const prevPubkeys = new Set(

View File

@ -1,12 +1,15 @@
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { type DittoDB } from '@/db.ts'; import { type DittoDB } from '@/db.ts';
import { Debug, type Event, Kysely, type SelectQueryBuilder } from '@/deps.ts'; import { Debug, Kysely, type NostrEvent, type SelectQueryBuilder } from '@/deps.ts';
import { type DittoFilter, normalizeFilters } from '@/filter.ts'; import { cleanEvent } from '@/events.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 { isDittoInternalKind, isParameterizedReplaceableKind, isReplaceableKind } from '@/kinds.ts';
import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { isNostrId, isURL } from '@/utils.ts'; import { isNostrId, isURL } from '@/utils.ts';
import { type DittoEvent, EventStore, type GetEventsOpts } from './types.ts'; import { type EventStore, type GetEventsOpts } from './types.ts';
/** Function to decide whether or not to index a tag. */ /** Function to decide whether or not to index a tag. */
type TagCondition = ({ event, count, value }: { type TagCondition = ({ event, count, value }: {
@ -65,7 +68,8 @@ class EventsDB implements EventStore {
} }
/** Insert an event (and its tags) into the database. */ /** Insert an event (and its tags) into the database. */
async add(event: DittoEvent): Promise<void> { async add(event: NostrEvent): Promise<void> {
event = cleanEvent(event);
this.#debug('EVENT', JSON.stringify(event)); this.#debug('EVENT', JSON.stringify(event));
if (isDittoInternalKind(event.kind) && event.pubkey !== Conf.pubkey) { if (isDittoInternalKind(event.kind) && event.pubkey !== Conf.pubkey) {
@ -264,7 +268,7 @@ class EventsDB implements EventStore {
} }
/** Get events for filters from the database. */ /** Get events for filters from the database. */
async filter<K extends number>(filters: DittoFilter<K>[], opts: GetEventsOpts = {}): Promise<DittoEvent<K>[]> { async filter(filters: DittoFilter[], opts: GetEventsOpts = {}): Promise<DittoEvent[]> {
filters = normalizeFilters(filters); // Improves performance of `{ kinds: [0], authors: ['...'] }` queries. filters = normalizeFilters(filters); // Improves performance of `{ kinds: [0], authors: ['...'] }` queries.
if (opts.signal?.aborted) return Promise.resolve([]); if (opts.signal?.aborted) return Promise.resolve([]);
@ -278,9 +282,9 @@ class EventsDB implements EventStore {
} }
return (await query.execute()).map((row) => { return (await query.execute()).map((row) => {
const event: DittoEvent<K> = { const event: DittoEvent = {
id: row.id, id: row.id,
kind: row.kind as K, kind: row.kind,
pubkey: row.pubkey, pubkey: row.pubkey,
content: row.content, content: row.content,
created_at: row.created_at, created_at: row.created_at,
@ -337,7 +341,7 @@ class EventsDB implements EventStore {
} }
/** Delete events based on filters from the database. */ /** Delete events based on filters from the database. */
async deleteFilters<K extends number>(filters: DittoFilter<K>[]): Promise<void> { async deleteFilters(filters: DittoFilter[]): Promise<void> {
if (!filters.length) return Promise.resolve(); if (!filters.length) return Promise.resolve();
this.#debug('DELETE', JSON.stringify(filters)); this.#debug('DELETE', JSON.stringify(filters));
@ -345,7 +349,7 @@ class EventsDB implements EventStore {
} }
/** Get number of events that would be returned by filters. */ /** Get number of events that would be returned by filters. */
async count<K extends number>(filters: DittoFilter<K>[]): Promise<number> { async count(filters: DittoFilter[]): Promise<number> {
if (!filters.length) return Promise.resolve(0); if (!filters.length) return Promise.resolve(0);
this.#debug('COUNT', JSON.stringify(filters)); this.#debug('COUNT', JSON.stringify(filters));
const query = this.getEventsQuery(filters); const query = this.getEventsQuery(filters);
@ -393,10 +397,10 @@ function filterIndexableTags(event: DittoEvent): string[][] {
} }
/** Build a search index from the event. */ /** Build a search index from the event. */
function buildSearchContent(event: Event): string { function buildSearchContent(event: NostrEvent): string {
switch (event.kind) { switch (event.kind) {
case 0: case 0:
return buildUserSearchContent(event as Event<0>); return buildUserSearchContent(event);
case 1: case 1:
return event.content; return event.content;
case 30009: case 30009:
@ -407,7 +411,7 @@ function buildSearchContent(event: Event): string {
} }
/** Build search content for a user. */ /** Build search content for a user. */
function buildUserSearchContent(event: Event<0>): string { function buildUserSearchContent(event: NostrEvent): string {
const { name, nip05, about } = jsonMetaContentSchema.parse(event.content); const { name, nip05, about } = jsonMetaContentSchema.parse(event.content);
return [name, nip05, about].filter(Boolean).join('\n'); return [name, nip05, about].filter(Boolean).join('\n');
} }

View File

@ -1,15 +1,16 @@
import { type DittoFilter } from '@/filter.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { type DittoEvent, type EventStore } from '@/storages/types.ts'; import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
import { type EventStore } from '@/storages/types.ts';
interface HydrateEventOpts<K extends number> { interface HydrateEventOpts {
events: DittoEvent<K>[]; events: DittoEvent[];
filters: DittoFilter<K>[]; filters: DittoFilter[];
storage: EventStore; storage: EventStore;
signal?: AbortSignal; signal?: AbortSignal;
} }
/** Hydrate event relationships using the provided storage. */ /** Hydrate event relationships using the provided storage. */
async function hydrateEvents<K extends number>(opts: HydrateEventOpts<K>): Promise<DittoEvent<K>[]> { async function hydrateEvents(opts: HydrateEventOpts): Promise<DittoEvent[]> {
const { events, filters, storage, signal } = opts; const { events, filters, storage, signal } = opts;
if (filters.some((filter) => filter.relations?.includes('author'))) { if (filters.some((filter) => filter.relations?.includes('author'))) {

View File

@ -1,23 +1,22 @@
import { Debug, type Event, type Filter, LRUCache, matchFilter } from '@/deps.ts'; import { Debug, LRUCache, matchFilter, type NostrEvent, type NostrFilter, NSet } from '@/deps.ts';
import { normalizeFilters } from '@/filter.ts'; import { normalizeFilters } from '@/filter.ts';
import { EventSet } from '@/utils/event-set.ts';
import { type EventStore, type GetEventsOpts } from './types.ts'; import { type EventStore, type GetEventsOpts } from './types.ts';
/** In-memory data store for events. */ /** In-memory data store for events. */
class Memorelay implements EventStore { class Memorelay implements EventStore {
#debug = Debug('ditto:memorelay'); #debug = Debug('ditto:memorelay');
#cache: LRUCache<string, Event>; #cache: LRUCache<string, NostrEvent>;
/** NIPs supported by this storage method. */ /** NIPs supported by this storage method. */
supportedNips = [1, 45]; supportedNips = [1, 45];
constructor(...args: ConstructorParameters<typeof LRUCache<string, Event>>) { constructor(...args: ConstructorParameters<typeof LRUCache<string, NostrEvent>>) {
this.#cache = new LRUCache<string, Event>(...args); this.#cache = new LRUCache<string, NostrEvent>(...args);
} }
/** Iterate stored events. */ /** Iterate stored events. */
*#events(): Generator<Event> { *#events(): Generator<NostrEvent> {
for (const event of this.#cache.values()) { for (const event of this.#cache.values()) {
if (event && !(event instanceof Promise)) { if (event && !(event instanceof Promise)) {
yield event; yield event;
@ -26,7 +25,7 @@ class Memorelay implements EventStore {
} }
/** Get events from memory. */ /** Get events from memory. */
filter<K extends number>(filters: Filter<K>[], opts: GetEventsOpts = {}): Promise<Event<K>[]> { filter(filters: NostrFilter[], opts: GetEventsOpts = {}): Promise<NostrEvent[]> {
filters = normalizeFilters(filters); filters = normalizeFilters(filters);
if (opts.signal?.aborted) return Promise.resolve([]); if (opts.signal?.aborted) return Promise.resolve([]);
@ -35,7 +34,7 @@ class Memorelay implements EventStore {
this.#debug('REQ', JSON.stringify(filters)); this.#debug('REQ', JSON.stringify(filters));
/** Event results to return. */ /** Event results to return. */
const results = new EventSet<Event<K>>(); const results = new NSet<NostrEvent>();
/** Number of times an event has been added to results for each filter. */ /** Number of times an event has been added to results for each filter. */
const filterUsages: number[] = []; const filterUsages: number[] = [];
@ -52,7 +51,7 @@ class Memorelay implements EventStore {
for (const id of filter.ids) { for (const id of filter.ids) {
const event = this.#cache.get(id); const event = this.#cache.get(id);
if (event && matchFilter(filter, event)) { if (event && matchFilter(filter, event)) {
results.add(event as Event<K>); results.add(event);
} }
} }
filterUsages[index] = Infinity; filterUsages[index] = Infinity;
@ -73,7 +72,7 @@ class Memorelay implements EventStore {
if (usage >= limit) { if (usage >= limit) {
return; return;
} else if (matchFilter(filter, event)) { } else if (matchFilter(filter, event)) {
results.add(event as Event<K>); results.add(event);
this.#cache.get(event.id); this.#cache.get(event.id);
filterUsages[index] = usage + 1; filterUsages[index] = usage + 1;
} }
@ -91,19 +90,19 @@ class Memorelay implements EventStore {
} }
/** Insert an event into memory. */ /** Insert an event into memory. */
add(event: Event): Promise<void> { add(event: NostrEvent): Promise<void> {
this.#cache.set(event.id, event); this.#cache.set(event.id, event);
return Promise.resolve(); return Promise.resolve();
} }
/** Count events in memory for the filters. */ /** Count events in memory for the filters. */
async count(filters: Filter[]): Promise<number> { async count(filters: NostrFilter[]): Promise<number> {
const events = await this.filter(filters); const events = await this.filter(filters);
return events.length; return events.length;
} }
/** Delete events from memory. */ /** Delete events from memory. */
async deleteFilters(filters: Filter[]): Promise<void> { async deleteFilters(filters: NostrFilter[]): Promise<void> {
for (const event of await this.filter(filters)) { for (const event of await this.filter(filters)) {
this.#cache.delete(event.id); this.#cache.delete(event.id);
} }

View File

@ -1,8 +1,9 @@
import { Debug } from '@/deps.ts'; import { Debug, NSet } from '@/deps.ts';
import { type DittoFilter, normalizeFilters } from '@/filter.ts'; import { normalizeFilters } from '@/filter.ts';
import { EventSet } from '@/utils/event-set.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
import { type DittoEvent, type EventStore, type GetEventsOpts, type StoreEventOpts } from './types.ts'; import { type EventStore, type GetEventsOpts, type StoreEventOpts } from './types.ts';
interface OptimizerOpts { interface OptimizerOpts {
db: EventStore; db: EventStore;
@ -25,17 +26,17 @@ class Optimizer implements EventStore {
this.#client = opts.client; this.#client = opts.client;
} }
async add(event: DittoEvent<number>, opts?: StoreEventOpts | undefined): Promise<void> { async add(event: DittoEvent, opts?: StoreEventOpts | undefined): Promise<void> {
await Promise.all([ await Promise.all([
this.#db.add(event, opts), this.#db.add(event, opts),
this.#cache.add(event, opts), this.#cache.add(event, opts),
]); ]);
} }
async filter<K extends number>( async filter(
filters: DittoFilter<K>[], filters: DittoFilter[],
opts: GetEventsOpts | undefined = {}, opts: GetEventsOpts | undefined = {},
): Promise<DittoEvent<K>[]> { ): Promise<DittoEvent[]> {
this.#debug('REQ', JSON.stringify(filters)); this.#debug('REQ', JSON.stringify(filters));
const { limit = Infinity } = opts; const { limit = Infinity } = opts;
@ -44,7 +45,7 @@ class Optimizer implements EventStore {
if (opts?.signal?.aborted) return Promise.resolve([]); if (opts?.signal?.aborted) return Promise.resolve([]);
if (!filters.length) return Promise.resolve([]); if (!filters.length) return Promise.resolve([]);
const results = new EventSet<DittoEvent<K>>(); const results = new NSet<DittoEvent>();
// Filters with IDs are immutable, so we can take them straight from the cache if we have them. // Filters with IDs are immutable, so we can take them straight from the cache if we have them.
for (let i = 0; i < filters.length; i++) { for (let i = 0; i < filters.length; i++) {
@ -99,11 +100,11 @@ class Optimizer implements EventStore {
return getResults(); return getResults();
} }
countEvents<K extends number>(_filters: DittoFilter<K>[]): Promise<number> { countEvents(_filters: DittoFilter[]): Promise<number> {
throw new Error('COUNT not implemented.'); throw new Error('COUNT not implemented.');
} }
deleteEvents<K extends number>(_filters: DittoFilter<K>[]): Promise<void> { deleteEvents(_filters: DittoFilter[]): Promise<void> {
throw new Error('DELETE not implemented.'); throw new Error('DELETE not implemented.');
} }
} }

View File

@ -1,13 +1,13 @@
import { Debug, type Event, type Filter, matchFilters, type RelayPoolWorker } from '@/deps.ts'; import { Debug, matchFilters, type NostrEvent, type NostrFilter, NSet, type RelayPoolWorker } from '@/deps.ts';
import { cleanEvent } from '@/events.ts';
import { normalizeFilters } from '@/filter.ts'; import { normalizeFilters } from '@/filter.ts';
import { type EventStore, type GetEventsOpts, type StoreEventOpts } from '@/storages/types.ts'; import { type EventStore, type GetEventsOpts, type StoreEventOpts } from '@/storages/types.ts';
import { EventSet } from '@/utils/event-set.ts';
interface PoolStoreOpts { interface PoolStoreOpts {
pool: InstanceType<typeof RelayPoolWorker>; pool: InstanceType<typeof RelayPoolWorker>;
relays: WebSocket['url'][]; relays: WebSocket['url'][];
publisher: { publisher: {
handleEvent(event: Event): Promise<void>; handleEvent(event: NostrEvent): Promise<void>;
}; };
} }
@ -16,7 +16,7 @@ class PoolStore implements EventStore {
#pool: InstanceType<typeof RelayPoolWorker>; #pool: InstanceType<typeof RelayPoolWorker>;
#relays: WebSocket['url'][]; #relays: WebSocket['url'][];
#publisher: { #publisher: {
handleEvent(event: Event): Promise<void>; handleEvent(event: NostrEvent): Promise<void>;
}; };
supportedNips = [1]; supportedNips = [1];
@ -27,14 +27,15 @@ class PoolStore implements EventStore {
this.#publisher = opts.publisher; this.#publisher = opts.publisher;
} }
add(event: Event, opts: StoreEventOpts = {}): Promise<void> { add(event: NostrEvent, opts: StoreEventOpts = {}): Promise<void> {
const { relays = this.#relays } = opts; const { relays = this.#relays } = opts;
event = cleanEvent(event);
this.#debug('EVENT', event); this.#debug('EVENT', event);
this.#pool.publish(event, relays); this.#pool.publish(event, relays);
return Promise.resolve(); return Promise.resolve();
} }
filter<K extends number>(filters: Filter<K>[], opts: GetEventsOpts = {}): Promise<Event<K>[]> { filter(filters: NostrFilter[], opts: GetEventsOpts = {}): Promise<NostrEvent[]> {
filters = normalizeFilters(filters); filters = normalizeFilters(filters);
if (opts.signal?.aborted) return Promise.resolve([]); if (opts.signal?.aborted) return Promise.resolve([]);
@ -43,17 +44,17 @@ class PoolStore implements EventStore {
this.#debug('REQ', JSON.stringify(filters)); this.#debug('REQ', JSON.stringify(filters));
return new Promise((resolve) => { return new Promise((resolve) => {
const results = new EventSet<Event<K>>(); const results = new NSet<NostrEvent>();
const unsub = this.#pool.subscribe( const unsub = this.#pool.subscribe(
filters, filters,
opts.relays ?? this.#relays, opts.relays ?? this.#relays,
(event: Event | null) => { (event: NostrEvent | null) => {
if (event && matchFilters(filters, event)) { if (event && matchFilters(filters, event)) {
this.#publisher.handleEvent(event).catch(() => {}); this.#publisher.handleEvent(event).catch(() => {});
results.add({ results.add({
id: event.id, id: event.id,
kind: event.kind as K, kind: event.kind,
pubkey: event.pubkey, pubkey: event.pubkey,
content: event.content, content: event.content,
tags: event.tags, tags: event.tags,

View File

@ -1,12 +1,5 @@
import { Debug, type Event, EventEmitter, type Filter } from '@/deps.ts'; import { Debug, EventEmitter, type NostrEvent, type NostrFilter } from '@/deps.ts';
import { import { eventToMicroFilter, getFilterId, isMicrofilter, type MicroFilter } from '@/filter.ts';
AuthorMicrofilter,
eventToMicroFilter,
getFilterId,
IdMicrofilter,
isMicrofilter,
type MicroFilter,
} from '@/filter.ts';
import { type EventStore, GetEventsOpts } from '@/storages/types.ts'; import { type EventStore, GetEventsOpts } from '@/storages/types.ts';
import { Time } from '@/utils/time.ts'; import { Time } from '@/utils/time.ts';
@ -24,7 +17,7 @@ interface ReqmeisterReqOpts {
type ReqmeisterQueueItem = [string, MicroFilter, WebSocket['url'][]]; type ReqmeisterQueueItem = [string, MicroFilter, WebSocket['url'][]];
/** Batches requests to Nostr relays using microfilters. */ /** Batches requests to Nostr relays using microfilters. */
class Reqmeister extends EventEmitter<{ [filterId: string]: (event: Event) => any }> implements EventStore { class Reqmeister extends EventEmitter<{ [filterId: string]: (event: NostrEvent) => any }> implements EventStore {
#debug = Debug('ditto:reqmeister'); #debug = Debug('ditto:reqmeister');
#opts: ReqmeisterOpts; #opts: ReqmeisterOpts;
@ -55,8 +48,8 @@ class Reqmeister extends EventEmitter<{ [filterId: string]: (event: Event) => an
const queue = this.#queue; const queue = this.#queue;
this.#queue = []; this.#queue = [];
const wantedEvents = new Set<Event['id']>(); const wantedEvents = new Set<NostrEvent['id']>();
const wantedAuthors = new Set<Event['pubkey']>(); const wantedAuthors = new Set<NostrEvent['pubkey']>();
// TODO: batch by relays. // TODO: batch by relays.
for (const [_filterId, filter, _relays] of queue) { for (const [_filterId, filter, _relays] of queue) {
@ -67,7 +60,7 @@ class Reqmeister extends EventEmitter<{ [filterId: string]: (event: Event) => an
} }
} }
const filters: Filter[] = []; const filters: NostrFilter[] = [];
if (wantedEvents.size) filters.push({ ids: [...wantedEvents] }); if (wantedEvents.size) filters.push({ ids: [...wantedEvents] });
if (wantedAuthors.size) filters.push({ kinds: [0], authors: [...wantedAuthors] }); if (wantedAuthors.size) filters.push({ kinds: [0], authors: [...wantedAuthors] });
@ -85,10 +78,7 @@ class Reqmeister extends EventEmitter<{ [filterId: string]: (event: Event) => an
this.#perform(); this.#perform();
} }
req(filter: IdMicrofilter, opts?: ReqmeisterReqOpts): Promise<Event>; req(filter: MicroFilter, opts: ReqmeisterReqOpts = {}): Promise<NostrEvent> {
req(filter: AuthorMicrofilter, opts?: ReqmeisterReqOpts): Promise<Event<0>>;
req(filter: MicroFilter, opts?: ReqmeisterReqOpts): Promise<Event>;
req(filter: MicroFilter, opts: ReqmeisterReqOpts = {}): Promise<Event> {
const { const {
relays = [], relays = [],
signal = AbortSignal.timeout(this.#opts.timeout ?? 1000), signal = AbortSignal.timeout(this.#opts.timeout ?? 1000),
@ -102,8 +92,8 @@ class Reqmeister extends EventEmitter<{ [filterId: string]: (event: Event) => an
this.#queue.push([filterId, filter, relays]); this.#queue.push([filterId, filter, relays]);
return new Promise<Event>((resolve, reject) => { return new Promise<NostrEvent>((resolve, reject) => {
const handleEvent = (event: Event) => { const handleEvent = (event: NostrEvent) => {
resolve(event); resolve(event);
this.removeListener(filterId, handleEvent); this.removeListener(filterId, handleEvent);
}; };
@ -119,25 +109,25 @@ class Reqmeister extends EventEmitter<{ [filterId: string]: (event: Event) => an
}); });
} }
add(event: Event): Promise<void> { add(event: NostrEvent): Promise<void> {
const filterId = getFilterId(eventToMicroFilter(event)); const filterId = getFilterId(eventToMicroFilter(event));
this.#queue = this.#queue.filter(([id]) => id !== filterId); this.#queue = this.#queue.filter(([id]) => id !== filterId);
this.emit(filterId, event); this.emit(filterId, event);
return Promise.resolve(); return Promise.resolve();
} }
isWanted(event: Event): boolean { isWanted(event: NostrEvent): boolean {
const filterId = getFilterId(eventToMicroFilter(event)); const filterId = getFilterId(eventToMicroFilter(event));
return this.#queue.some(([id]) => id === filterId); return this.#queue.some(([id]) => id === filterId);
} }
filter<K extends number>(filters: Filter<K>[], opts?: GetEventsOpts | undefined): Promise<Event<K>[]> { filter(filters: NostrFilter[], opts?: GetEventsOpts | undefined): Promise<NostrEvent[]> {
if (opts?.signal?.aborted) return Promise.resolve([]); if (opts?.signal?.aborted) return Promise.resolve([]);
if (!filters.length) return Promise.resolve([]); if (!filters.length) return Promise.resolve([]);
const promises = filters.reduce<Promise<Event<K>>[]>((result, filter) => { const promises = filters.reduce<Promise<NostrEvent>[]>((result, filter) => {
if (isMicrofilter(filter)) { if (isMicrofilter(filter)) {
result.push(this.req(filter) as Promise<Event<K>>); result.push(this.req(filter) as Promise<NostrEvent>);
} }
return result; return result;
}, []); }, []);
@ -145,11 +135,11 @@ class Reqmeister extends EventEmitter<{ [filterId: string]: (event: Event) => an
return Promise.all(promises); return Promise.all(promises);
} }
count(_filters: Filter[]): Promise<number> { count(_filters: NostrFilter[]): Promise<number> {
throw new Error('COUNT not implemented.'); throw new Error('COUNT not implemented.');
} }
deleteFilters(_filters: Filter[]): Promise<void> { deleteFilters(_filters: NostrFilter[]): Promise<void> {
throw new Error('DELETE not implemented.'); throw new Error('DELETE not implemented.');
} }
} }

View File

@ -1,10 +1,11 @@
import { NiceRelay } from 'https://gitlab.com/soapbox-pub/nostr-machina/-/raw/5f4fb59c90c092e5aa59c01e6556a4bec264c167/mod.ts'; import { NiceRelay } from 'https://gitlab.com/soapbox-pub/nostr-machina/-/raw/5f4fb59c90c092e5aa59c01e6556a4bec264c167/mod.ts';
import { Debug, type Event, type Filter } from '@/deps.ts'; import { Debug, type NostrEvent, type NostrFilter, NSet } from '@/deps.ts';
import { type DittoFilter, normalizeFilters } from '@/filter.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 { hydrateEvents } from '@/storages/hydrate.ts';
import { type DittoEvent, type EventStore, type GetEventsOpts, type StoreEventOpts } from '@/storages/types.ts'; import { type EventStore, type GetEventsOpts, type StoreEventOpts } from '@/storages/types.ts';
import { EventSet } from '@/utils/event-set.ts';
interface SearchStoreOpts { interface SearchStoreOpts {
relay: string | undefined; relay: string | undefined;
@ -30,14 +31,14 @@ class SearchStore implements EventStore {
} }
} }
add(_event: Event, _opts?: StoreEventOpts | undefined): Promise<void> { add(_event: NostrEvent, _opts?: StoreEventOpts | undefined): Promise<void> {
throw new Error('EVENT not implemented.'); throw new Error('EVENT not implemented.');
} }
async filter<K extends number>( async filter(
filters: DittoFilter<K>[], filters: DittoFilter[],
opts?: GetEventsOpts | undefined, opts?: GetEventsOpts | undefined,
): Promise<DittoEvent<K>[]> { ): Promise<DittoEvent[]> {
filters = normalizeFilters(filters); filters = normalizeFilters(filters);
if (opts?.signal?.aborted) return Promise.resolve([]); if (opts?.signal?.aborted) return Promise.resolve([]);
@ -60,7 +61,7 @@ class SearchStore implements EventStore {
opts?.signal?.addEventListener('abort', close, { once: true }); opts?.signal?.addEventListener('abort', close, { once: true });
sub.eoseSignal.addEventListener('abort', close, { once: true }); sub.eoseSignal.addEventListener('abort', close, { once: true });
const events = new EventSet<DittoEvent<K>>(); const events = new NSet<DittoEvent>();
for await (const event of sub) { for await (const event of sub) {
events.add(event); events.add(event);
@ -73,11 +74,11 @@ class SearchStore implements EventStore {
} }
} }
count<K extends number>(_filters: Filter<K>[]): Promise<number> { count(_filters: NostrFilter[]): Promise<number> {
throw new Error('COUNT not implemented.'); throw new Error('COUNT not implemented.');
} }
deleteFilters<K extends number>(_filters: Filter<K>[]): Promise<void> { deleteFilters(_filters: NostrFilter[]): Promise<void> {
throw new Error('DELETE not implemented.'); throw new Error('DELETE not implemented.');
} }
} }

View File

@ -1,6 +1,5 @@
import { type DittoDB } from '@/db.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { type Event } from '@/deps.ts'; import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
import { type DittoFilter } from '@/filter.ts';
/** Additional options to apply to the whole subscription. */ /** Additional options to apply to the whole subscription. */
interface GetEventsOpts { interface GetEventsOpts {
@ -18,30 +17,18 @@ interface StoreEventOpts {
relays?: WebSocket['url'][]; relays?: WebSocket['url'][];
} }
type AuthorStats = Omit<DittoDB['author_stats'], 'pubkey'>;
type EventStats = Omit<DittoDB['event_stats'], 'event_id'>;
/** Internal Event representation used by Ditto, including extra keys. */
interface DittoEvent<K extends number = number> extends Event<K> {
author?: DittoEvent<0>;
author_stats?: AuthorStats;
event_stats?: EventStats;
d_author?: DittoEvent<0>;
user?: DittoEvent<30361>;
}
/** Storage interface for Nostr events. */ /** Storage interface for Nostr events. */
interface EventStore { interface EventStore {
/** Indicates NIPs supported by this data store, similar to NIP-11. For example, `50` would indicate support for `search` filters. */ /** Indicates NIPs supported by this data store, similar to NIP-11. For example, `50` would indicate support for `search` filters. */
supportedNips: readonly number[]; supportedNips: readonly number[];
/** Add an event to the store. */ /** Add an event to the store. */
add(event: Event, opts?: StoreEventOpts): Promise<void>; add(event: DittoEvent, opts?: StoreEventOpts): Promise<void>;
/** Get events from filters. */ /** Get events from filters. */
filter<K extends number>(filters: DittoFilter<K>[], opts?: GetEventsOpts): Promise<DittoEvent<K>[]>; filter(filters: DittoFilter[], opts?: GetEventsOpts): Promise<DittoEvent[]>;
/** Get the number of events from filters. */ /** Get the number of events from filters. */
count?<K extends number>(filters: DittoFilter<K>[]): Promise<number>; count?(filters: DittoFilter[]): Promise<number>;
/** Delete events from filters. */ /** Delete events from filters. */
deleteFilters?<K extends number>(filters: DittoFilter<K>[]): Promise<void>; deleteFilters?(filters: DittoFilter[]): Promise<void>;
} }
export type { DittoEvent, EventStore, GetEventsOpts, StoreEventOpts }; export type { EventStore, GetEventsOpts, StoreEventOpts };

View File

@ -1,6 +1,6 @@
import { Debug } from '@/deps.ts'; import { Debug } from '@/deps.ts';
import { type DittoFilter } from '@/filter.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { type DittoEvent } from '@/storages/types.ts'; import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
import { Subscription } from '@/subscription.ts'; import { Subscription } from '@/subscription.ts';
const debug = Debug('ditto:subs'); const debug = Debug('ditto:subs');
@ -21,7 +21,7 @@ class SubscriptionStore {
* } * }
* ``` * ```
*/ */
sub<K extends number>(socket: unknown, id: string, filters: DittoFilter<K>[]): Subscription<K> { sub(socket: unknown, id: string, filters: DittoFilter[]): Subscription {
debug('sub', id, JSON.stringify(filters)); debug('sub', id, JSON.stringify(filters));
let subs = this.#store.get(socket); let subs = this.#store.get(socket);

View File

@ -1,17 +1,18 @@
import { type Event, Machina } from '@/deps.ts'; import { Machina, type NostrEvent } from '@/deps.ts';
import { type DittoFilter, matchDittoFilters } from '@/filter.ts'; import { matchDittoFilters } from '@/filter.ts';
import { type DittoEvent } from '@/storages/types.ts'; import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
class Subscription<K extends number = number> implements AsyncIterable<Event<K>> { class Subscription implements AsyncIterable<NostrEvent> {
filters: DittoFilter<K>[]; filters: DittoFilter[];
#machina: Machina<Event<K>>; #machina: Machina<NostrEvent>;
constructor(filters: DittoFilter<K>[]) { constructor(filters: DittoFilter[]) {
this.filters = filters; this.filters = filters;
this.#machina = new Machina(); this.#machina = new Machina();
} }
stream(event: Event<K>): void { stream(event: NostrEvent): void {
this.#machina.push(event); this.#machina.push(event);
} }

View File

@ -31,4 +31,12 @@ function addTag(tags: readonly string[][], tag: string[]): string[][] {
} }
} }
export { addTag, deleteTag, getTagSet, hasTag }; const isReplyTag = (tag: string[]) => tag[0] === 'e' && tag[3] === 'reply';
const isRootTag = (tag: string[]) => tag[0] === 'e' && tag[3] === 'root';
const isLegacyReplyTag = (tag: string[]) => tag[0] === 'e' && !tag[3];
function findReplyTag(tags: string[][]) {
return tags.find(isReplyTag) || tags.find(isRootTag) || tags.findLast(isLegacyReplyTag);
}
export { addTag, deleteTag, findReplyTag, getTagSet, hasTag };

View File

@ -1,4 +1,4 @@
import { type Event, type EventTemplate, getEventHash, nip19, z } from '@/deps.ts'; import { type EventTemplate, getEventHash, nip19, type NostrEvent, z } from '@/deps.ts';
import { getAuthor } from '@/queries.ts'; import { getAuthor } from '@/queries.ts';
import { nip05Cache } from '@/utils/nip05.ts'; import { nip05Cache } from '@/utils/nip05.ts';
import { nostrIdSchema } from '@/schemas/nostr.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts';
@ -9,7 +9,7 @@ const nostrNow = (): number => Math.floor(Date.now() / 1000);
const nostrDate = (seconds: number): Date => new Date(seconds * 1000); const nostrDate = (seconds: number): Date => new Date(seconds * 1000);
/** Pass to sort() to sort events by date. */ /** Pass to sort() to sort events by date. */
const eventDateComparator = (a: Event, b: Event): number => b.created_at - a.created_at; const eventDateComparator = (a: NostrEvent, b: NostrEvent): number => b.created_at - a.created_at;
/** Get pubkey from bech32 string, if applicable. */ /** Get pubkey from bech32 string, if applicable. */
function bech32ToPubkey(bech32: string): string | undefined { function bech32ToPubkey(bech32: string): string | undefined {
@ -56,7 +56,7 @@ function parseNip05(value: string): Nip05 {
} }
/** Resolve a bech32 or NIP-05 identifier to an account. */ /** Resolve a bech32 or NIP-05 identifier to an account. */
async function lookupAccount(value: string, signal = AbortSignal.timeout(3000)): Promise<Event<0> | undefined> { async function lookupAccount(value: string, signal = AbortSignal.timeout(3000)): Promise<NostrEvent | undefined> {
console.log(`Looking up ${value}`); console.log(`Looking up ${value}`);
const pubkey = bech32ToPubkey(value) || const pubkey = bech32ToPubkey(value) ||
@ -68,7 +68,7 @@ async function lookupAccount(value: string, signal = AbortSignal.timeout(3000)):
} }
/** Return the event's age in milliseconds. */ /** Return the event's age in milliseconds. */
function eventAge(event: Event): number { function eventAge(event: NostrEvent): number {
return Date.now() - nostrDate(event.created_at).getTime(); return Date.now() - nostrDate(event.created_at).getTime();
} }
@ -97,7 +97,7 @@ const relaySchema = z.string().max(255).startsWith('wss://').url();
const isRelay = (relay: string): relay is `wss://${string}` => relaySchema.safeParse(relay).success; const isRelay = (relay: string): relay is `wss://${string}` => relaySchema.safeParse(relay).success;
/** Deduplicate events by ID. */ /** Deduplicate events by ID. */
function dedupeEvents<K extends number>(events: Event<K>[]): Event<K>[] { function dedupeEvents(events: NostrEvent[]): NostrEvent[] {
return [...new Map(events.map((event) => [event.id, event])).values()]; return [...new Map(events.map((event) => [event.id, event])).values()];
} }
@ -111,7 +111,7 @@ function stripTags<E extends EventTemplate>(event: E, tags: string[] = []): E {
} }
/** Ensure the template and event match on their shared keys. */ /** Ensure the template and event match on their shared keys. */
function eventMatchesTemplate(event: Event, template: EventTemplate): boolean { function eventMatchesTemplate(event: NostrEvent, template: EventTemplate): boolean {
const whitelist = ['nonce']; const whitelist = ['nonce'];
event = stripTags(event, whitelist); event = stripTags(event, whitelist);

View File

@ -3,10 +3,10 @@ import { Conf } from '@/config.ts';
import { import {
type Context, type Context,
Debug, Debug,
type Event,
EventTemplate, EventTemplate,
Filter,
HTTPException, HTTPException,
type NostrEvent,
NostrFilter,
parseFormData, parseFormData,
type TypeFest, type TypeFest,
z, z,
@ -19,10 +19,10 @@ import { nostrNow } from '@/utils.ts';
const debug = Debug('ditto:api'); const debug = Debug('ditto:api');
/** EventTemplate with defaults. */ /** EventTemplate with defaults. */
type EventStub<K extends number = number> = TypeFest.SetOptional<EventTemplate<K>, 'content' | 'created_at' | 'tags'>; type EventStub = TypeFest.SetOptional<EventTemplate, 'content' | 'created_at' | 'tags'>;
/** Publish an event through the pipeline. */ /** Publish an event through the pipeline. */
async function createEvent<K extends number>(t: EventStub<K>, c: AppContext): Promise<Event<K>> { async function createEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
const pubkey = c.get('pubkey'); const pubkey = c.get('pubkey');
if (!pubkey) { if (!pubkey) {
@ -40,27 +40,27 @@ async function createEvent<K extends number>(t: EventStub<K>, c: AppContext): Pr
} }
/** Filter for fetching an existing event to update. */ /** Filter for fetching an existing event to update. */
interface UpdateEventFilter<K extends number> extends Filter<K> { interface UpdateEventFilter extends NostrFilter {
kinds: [K]; kinds: [number];
limit?: 1; limit?: 1;
} }
/** Fetch existing event, update it, then publish the new event. */ /** Fetch existing event, update it, then publish the new event. */
async function updateEvent<K extends number, E extends EventStub<K>>( async function updateEvent<K extends number, E extends EventStub>(
filter: UpdateEventFilter<K>, filter: UpdateEventFilter,
fn: (prev: Event<K> | undefined) => E, fn: (prev: NostrEvent | undefined) => E,
c: AppContext, c: AppContext,
): Promise<Event<K>> { ): Promise<NostrEvent> {
const [prev] = await eventsDB.filter([filter], { limit: 1 }); const [prev] = await eventsDB.filter([filter], { limit: 1 });
return createEvent(fn(prev), c); return createEvent(fn(prev), c);
} }
/** Fetch existing event, update its tags, then publish the new event. */ /** Fetch existing event, update its tags, then publish the new event. */
function updateListEvent<K extends number>( function updateListEvent(
filter: UpdateEventFilter<K>, filter: UpdateEventFilter,
fn: (tags: string[][]) => string[][], fn: (tags: string[][]) => string[][],
c: AppContext, c: AppContext,
): Promise<Event<K>> { ): Promise<NostrEvent> {
return updateEvent(filter, (prev) => ({ return updateEvent(filter, (prev) => ({
kind: filter.kinds[0], kind: filter.kinds[0],
content: prev?.content ?? '', content: prev?.content ?? '',
@ -69,7 +69,7 @@ function updateListEvent<K extends number>(
} }
/** Publish an admin event through the pipeline. */ /** Publish an admin event through the pipeline. */
async function createAdminEvent<K extends number>(t: EventStub<K>, c: AppContext): Promise<Event<K>> { async function createAdminEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
const event = await signAdminEvent({ const event = await signAdminEvent({
content: '', content: '',
created_at: nostrNow(), created_at: nostrNow(),
@ -81,7 +81,7 @@ async function createAdminEvent<K extends number>(t: EventStub<K>, c: AppContext
} }
/** Push the event through the pipeline, rethrowing any RelayError. */ /** Push the event through the pipeline, rethrowing any RelayError. */
async function publishEvent<K extends number>(event: Event<K>, c: AppContext): Promise<Event<K>> { async function publishEvent(event: NostrEvent, c: AppContext): Promise<NostrEvent> {
debug('EVENT', event); debug('EVENT', event);
try { try {
await pipeline.handleEvent(event); await pipeline.handleEvent(event);
@ -118,7 +118,7 @@ const paginationSchema = z.object({
type PaginationParams = z.infer<typeof paginationSchema>; type PaginationParams = z.infer<typeof paginationSchema>;
/** Build HTTP Link header for Mastodon API pagination. */ /** Build HTTP Link header for Mastodon API pagination. */
function buildLinkHeader(url: string, events: Event[]): string | undefined { function buildLinkHeader(url: string, events: NostrEvent[]): string | undefined {
if (events.length <= 1) return; if (events.length <= 1) return;
const firstEvent = events[0]; const firstEvent = events[0];
const lastEvent = events[events.length - 1]; const lastEvent = events[events.length - 1];
@ -138,7 +138,7 @@ type Entity = { id: string };
type HeaderRecord = Record<string, string | string[]>; type HeaderRecord = Record<string, string | string[]>;
/** Return results with pagination headers. Assumes chronological sorting of events. */ /** Return results with pagination headers. Assumes chronological sorting of events. */
function paginated(c: AppContext, events: Event[], entities: (Entity | undefined)[], headers: HeaderRecord = {}) { function paginated(c: AppContext, events: NostrEvent[], entities: (Entity | undefined)[], headers: HeaderRecord = {}) {
const link = buildLinkHeader(c.req.url, events); const link = buildLinkHeader(c.req.url, events);
if (link) { if (link) {

View File

@ -1,109 +0,0 @@
import { assertEquals } from '@/deps-test.ts';
import { EventSet } from './event-set.ts';
Deno.test('EventSet', () => {
const set = new EventSet();
assertEquals(set.size, 0);
const event = { id: '1', kind: 0, pubkey: 'abc', content: '', created_at: 0, sig: '', tags: [] };
set.add(event);
assertEquals(set.size, 1);
assertEquals(set.has(event), true);
set.add(event);
assertEquals(set.size, 1);
assertEquals(set.has(event), true);
set.delete(event);
assertEquals(set.size, 0);
assertEquals(set.has(event), false);
set.delete(event);
assertEquals(set.size, 0);
assertEquals(set.has(event), false);
set.add(event);
assertEquals(set.size, 1);
assertEquals(set.has(event), true);
set.clear();
assertEquals(set.size, 0);
assertEquals(set.has(event), false);
});
Deno.test('EventSet.add (replaceable)', () => {
const event0 = { id: '1', kind: 0, pubkey: 'abc', content: '', created_at: 0, sig: '', tags: [] };
const event1 = { id: '2', kind: 0, pubkey: 'abc', content: '', created_at: 1, sig: '', tags: [] };
const event2 = { id: '3', kind: 0, pubkey: 'abc', content: '', created_at: 2, sig: '', tags: [] };
const set = new EventSet();
set.add(event0);
assertEquals(set.size, 1);
assertEquals(set.has(event0), true);
set.add(event1);
assertEquals(set.size, 1);
assertEquals(set.has(event0), false);
assertEquals(set.has(event1), true);
set.add(event2);
assertEquals(set.size, 1);
assertEquals(set.has(event0), false);
assertEquals(set.has(event1), false);
assertEquals(set.has(event2), true);
});
Deno.test('EventSet.add (parameterized)', () => {
const event0 = { id: '1', kind: 30000, pubkey: 'abc', content: '', created_at: 0, sig: '', tags: [['d', 'a']] };
const event1 = { id: '2', kind: 30000, pubkey: 'abc', content: '', created_at: 1, sig: '', tags: [['d', 'a']] };
const event2 = { id: '3', kind: 30000, pubkey: 'abc', content: '', created_at: 2, sig: '', tags: [['d', 'a']] };
const set = new EventSet();
set.add(event0);
assertEquals(set.size, 1);
assertEquals(set.has(event0), true);
set.add(event1);
assertEquals(set.size, 1);
assertEquals(set.has(event0), false);
assertEquals(set.has(event1), true);
set.add(event2);
assertEquals(set.size, 1);
assertEquals(set.has(event0), false);
assertEquals(set.has(event1), false);
assertEquals(set.has(event2), true);
});
Deno.test('EventSet.eventReplaces', () => {
const event0 = { id: '1', kind: 0, pubkey: 'abc', content: '', created_at: 0, sig: '', tags: [] };
const event1 = { id: '2', kind: 0, pubkey: 'abc', content: '', created_at: 1, sig: '', tags: [] };
const event2 = { id: '3', kind: 0, pubkey: 'abc', content: '', created_at: 2, sig: '', tags: [] };
const event3 = { id: '4', kind: 0, pubkey: 'def', content: '', created_at: 0, sig: '', tags: [] };
assertEquals(EventSet.eventReplaces(event1, event0), true);
assertEquals(EventSet.eventReplaces(event2, event0), true);
assertEquals(EventSet.eventReplaces(event2, event1), true);
assertEquals(EventSet.eventReplaces(event0, event1), false);
assertEquals(EventSet.eventReplaces(event0, event2), false);
assertEquals(EventSet.eventReplaces(event1, event2), false);
assertEquals(EventSet.eventReplaces(event3, event1), false);
assertEquals(EventSet.eventReplaces(event1, event3), false);
});
Deno.test('EventSet.eventReplaces (parameterized)', () => {
const event0 = { id: '1', kind: 30000, pubkey: 'abc', content: '', created_at: 0, sig: '', tags: [['d', 'a']] };
const event1 = { id: '2', kind: 30000, pubkey: 'abc', content: '', created_at: 1, sig: '', tags: [['d', 'a']] };
const event2 = { id: '3', kind: 30000, pubkey: 'abc', content: '', created_at: 2, sig: '', tags: [['d', 'a']] };
assertEquals(EventSet.eventReplaces(event1, event0), true);
assertEquals(EventSet.eventReplaces(event2, event0), true);
assertEquals(EventSet.eventReplaces(event2, event1), true);
assertEquals(EventSet.eventReplaces(event0, event1), false);
assertEquals(EventSet.eventReplaces(event0, event2), false);
assertEquals(EventSet.eventReplaces(event1, event2), false);
});

View File

@ -1,77 +0,0 @@
import { type Event } from '@/deps.ts';
import { isParameterizedReplaceableKind, isReplaceableKind } from '@/kinds.ts';
/** In-memory store for Nostr events with replaceable event functionality. */
class EventSet<E extends Event = Event> implements Set<E> {
#map = new Map<string, E>();
get size() {
return this.#map.size;
}
add(event: E): this {
if (isReplaceableKind(event.kind) || isParameterizedReplaceableKind(event.kind)) {
for (const e of this.values()) {
if (EventSet.eventReplaces(event, e)) {
this.delete(e);
}
}
}
this.#map.set(event.id, event);
return this;
}
clear(): void {
this.#map.clear();
}
delete(event: E): boolean {
return this.#map.delete(event.id);
}
forEach(callbackfn: (event: E, key: E, set: Set<E>) => void, thisArg?: any): void {
return this.#map.forEach((event, _id) => callbackfn(event, event, this), thisArg);
}
has(event: E): boolean {
return this.#map.has(event.id);
}
*entries(): IterableIterator<[E, E]> {
for (const event of this.#map.values()) {
yield [event, event];
}
}
keys(): IterableIterator<E> {
return this.#map.values();
}
values(): IterableIterator<E> {
return this.#map.values();
}
[Symbol.iterator](): IterableIterator<E> {
return this.#map.values();
}
[Symbol.toStringTag]: string = 'EventSet';
/** Returns true if both events are replaceable, belong to the same kind and pubkey (and `d` tag, for parameterized events), and the first event is newer than the second one. */
static eventReplaces(event: Event, target: Event): boolean {
if (isReplaceableKind(event.kind)) {
return event.kind === target.kind && event.pubkey === target.pubkey && event.created_at > target.created_at;
} else if (isParameterizedReplaceableKind(event.kind)) {
const d = event.tags.find(([name]) => name === 'd')?.[1] || '';
const d2 = target.tags.find(([name]) => name === 'd')?.[1] || '';
return event.kind === target.kind &&
event.pubkey === target.pubkey &&
d === d2 &&
event.created_at > target.created_at;
}
return false;
}
}
export { EventSet };

View File

@ -1,4 +1,4 @@
import { type Event, type EventTemplate, nip13, type VerifiedEvent } from '@/deps.ts'; import { type EventTemplate, nip13, type NostrEvent } from '@/deps.ts';
import { decode64Schema, jsonSchema } from '@/schema.ts'; import { decode64Schema, jsonSchema } from '@/schema.ts';
import { signedEventSchema } from '@/schemas/nostr.ts'; import { signedEventSchema } from '@/schemas/nostr.ts';
import { eventAge, findTag, nostrNow, sha256 } from '@/utils.ts'; import { eventAge, findTag, nostrNow, sha256 } from '@/utils.ts';
@ -28,18 +28,18 @@ async function parseAuthRequest(req: Request, opts: ParseAuthRequestOpts = {}) {
} }
/** Compare the auth event with the request, returning a zod SafeParse type. */ /** Compare the auth event with the request, returning a zod SafeParse type. */
function validateAuthEvent(req: Request, event: Event, opts: ParseAuthRequestOpts = {}) { function validateAuthEvent(req: Request, event: NostrEvent, opts: ParseAuthRequestOpts = {}) {
const { maxAge = Time.minutes(1), validatePayload = true, pow = 0 } = opts; const { maxAge = Time.minutes(1), validatePayload = true, pow = 0 } = opts;
const schema = signedEventSchema const schema = signedEventSchema
.refine((event): event is VerifiedEvent<27235> => event.kind === 27235, 'Event must be kind 27235') .refine((event) => event.kind === 27235, 'Event must be kind 27235')
.refine((event) => eventAge(event) < maxAge, 'Event expired') .refine((event) => eventAge(event) < maxAge, 'Event expired')
.refine((event) => tagValue(event, 'method') === req.method, 'Event method does not match HTTP request method') .refine((event) => tagValue(event, 'method') === req.method, 'Event method does not match HTTP request method')
.refine((event) => tagValue(event, 'u') === req.url, 'Event URL does not match request URL') .refine((event) => tagValue(event, 'u') === req.url, 'Event URL does not match request URL')
.refine((event) => pow ? nip13.getPow(event.id) >= pow : true, 'Insufficient proof of work') .refine((event) => pow ? nip13.getPow(event.id) >= pow : true, 'Insufficient proof of work')
.refine(validateBody, 'Event payload does not match request body'); .refine(validateBody, 'Event payload does not match request body');
function validateBody(event: Event<27235>) { function validateBody(event: NostrEvent) {
if (!validatePayload) return true; if (!validatePayload) return true;
return req.clone().text() return req.clone().text()
.then(sha256) .then(sha256)
@ -73,7 +73,7 @@ async function buildAuthEventTemplate(req: Request, opts: ParseAuthRequestOpts =
} }
/** Get the value for the first matching tag name in the event. */ /** Get the value for the first matching tag name in the event. */
function tagValue(event: Event, tagName: string): string | undefined { function tagValue(event: NostrEvent, tagName: string): string | undefined {
return findTag(event.tags, tagName)?.[1]; return findTag(event.tags, tagName)?.[1];
} }

View File

@ -1,12 +1,12 @@
import { AppContext } from '@/app.ts'; import { AppContext } from '@/app.ts';
import { type Filter } from '@/deps.ts'; import { type NostrFilter } from '@/deps.ts';
import { eventsDB } from '@/storages.ts'; import { eventsDB } from '@/storages.ts';
import { renderAccount } from '@/views/mastodon/accounts.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts';
import { renderStatus } from '@/views/mastodon/statuses.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts';
import { paginated, paginationSchema } from '@/utils/api.ts'; import { paginated, paginationSchema } from '@/utils/api.ts';
/** Render account objects for the author of each event. */ /** Render account objects for the author of each event. */
async function renderEventAccounts(c: AppContext, filters: Filter[], signal = AbortSignal.timeout(1000)) { async function renderEventAccounts(c: AppContext, filters: NostrFilter[], signal = AbortSignal.timeout(1000)) {
if (!filters.length) { if (!filters.length) {
return c.json([]); return c.json([]);
} }

View File

@ -2,11 +2,11 @@ import { Conf } from '@/config.ts';
import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { getPublicKeyPem } from '@/utils/rsa.ts'; import { getPublicKeyPem } from '@/utils/rsa.ts';
import type { Event } from '@/deps.ts'; import type { NostrEvent } from '@/deps.ts';
import type { Actor } from '@/schemas/activitypub.ts'; import type { Actor } from '@/schemas/activitypub.ts';
/** Nostr metadata event to ActivityPub actor. */ /** Nostr metadata event to ActivityPub actor. */
async function renderActor(event: Event<0>, username: string): Promise<Actor | undefined> { async function renderActor(event: NostrEvent, username: string): Promise<Actor | undefined> {
const content = jsonMetaContentSchema.parse(event.content); const content = jsonMetaContentSchema.parse(event.content);
return { return {

View File

@ -1,8 +1,8 @@
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { findUser } from '@/db/users.ts'; import { findUser } from '@/db/users.ts';
import { lodash, nip19, type UnsignedEvent } from '@/deps.ts'; import { lodash, nip19, type UnsignedEvent } from '@/deps.ts';
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { type DittoEvent } from '@/storages/types.ts';
import { getLnurl } from '@/utils/lnurl.ts'; import { getLnurl } from '@/utils/lnurl.ts';
import { nip05Cache } from '@/utils/nip05.ts'; import { nip05Cache } from '@/utils/nip05.ts';
import { Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts'; import { Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts';
@ -13,7 +13,7 @@ interface ToAccountOpts {
} }
async function renderAccount( async function renderAccount(
event: Omit<DittoEvent<0>, 'id' | 'sig'>, event: Omit<DittoEvent, 'id' | 'sig'>,
opts: ToAccountOpts = {}, opts: ToAccountOpts = {},
) { ) {
const { withSource = false } = opts; const { withSource = false } = opts;
@ -81,7 +81,7 @@ async function renderAccount(
} }
function accountFromPubkey(pubkey: string, opts: ToAccountOpts = {}) { function accountFromPubkey(pubkey: string, opts: ToAccountOpts = {}) {
const event: UnsignedEvent<0> = { const event: UnsignedEvent = {
kind: 0, kind: 0,
pubkey, pubkey,
content: '', content: '',

View File

@ -1,9 +1,9 @@
import { DittoEvent } from '@/storages/types.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { nostrDate } from '@/utils.ts'; import { nostrDate } from '@/utils.ts';
import { accountFromPubkey, renderAccount } from './accounts.ts'; import { accountFromPubkey, renderAccount } from './accounts.ts';
async function renderAdminAccount(event: DittoEvent<30361>) { async function renderAdminAccount(event: DittoEvent) {
const d = event.tags.find(([name]) => name === 'd')?.[1]!; const d = event.tags.find(([name]) => name === 'd')?.[1]!;
const account = event.d_author ? await renderAccount({ ...event.d_author, user: event }) : await accountFromPubkey(d); const account = event.d_author ? await renderAccount({ ...event.d_author, user: event }) : await accountFromPubkey(d);

View File

@ -1,17 +1,17 @@
import { type Event } from '@/deps.ts'; import { type NostrEvent } from '@/deps.ts';
import { getAuthor } from '@/queries.ts'; import { getAuthor } from '@/queries.ts';
import { nostrDate } from '@/utils.ts'; import { nostrDate } from '@/utils.ts';
import { accountFromPubkey } from '@/views/mastodon/accounts.ts'; import { accountFromPubkey } from '@/views/mastodon/accounts.ts';
import { renderStatus } from '@/views/mastodon/statuses.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts';
function renderNotification(event: Event, viewerPubkey?: string) { function renderNotification(event: NostrEvent, viewerPubkey?: string) {
switch (event.kind) { switch (event.kind) {
case 1: case 1:
return renderNotificationMention(event as Event<1>, viewerPubkey); return renderNotificationMention(event, viewerPubkey);
} }
} }
async function renderNotificationMention(event: Event<1>, viewerPubkey?: string) { async function renderNotificationMention(event: NostrEvent, viewerPubkey?: string) {
const author = await getAuthor(event.pubkey); const author = await getAuthor(event.pubkey);
const status = await renderStatus({ ...event, author }, viewerPubkey); const status = await renderStatus({ ...event, author }, viewerPubkey);
if (!status) return; if (!status) return;

View File

@ -1,24 +1,25 @@
import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts'; import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { findReplyTag, nip19 } from '@/deps.ts'; import { nip19 } from '@/deps.ts';
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { getMediaLinks, parseNoteContent } from '@/note.ts'; import { getMediaLinks, parseNoteContent } from '@/note.ts';
import { getAuthor } from '@/queries.ts'; import { getAuthor } from '@/queries.ts';
import { jsonMediaDataSchema } from '@/schemas/nostr.ts'; import { jsonMediaDataSchema } from '@/schemas/nostr.ts';
import { eventsDB } from '@/storages.ts'; import { eventsDB } from '@/storages.ts';
import { type DittoEvent } from '@/storages/types.ts'; import { findReplyTag } from '@/tags.ts';
import { nostrDate } from '@/utils.ts'; import { nostrDate } from '@/utils.ts';
import { unfurlCardCached } from '@/utils/unfurl.ts'; import { unfurlCardCached } from '@/utils/unfurl.ts';
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts';
import { renderEmojis } from '@/views/mastodon/emojis.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts';
async function renderStatus(event: DittoEvent<1>, viewerPubkey?: string) { async function renderStatus(event: DittoEvent, viewerPubkey?: string) {
const account = event.author const account = event.author
? await renderAccount({ ...event.author, author_stats: event.author_stats }) ? await renderAccount({ ...event.author, author_stats: event.author_stats })
: await accountFromPubkey(event.pubkey); : await accountFromPubkey(event.pubkey);
const replyTag = findReplyTag(event); const replyTag = findReplyTag(event.tags);
const mentionedPubkeys = [ const mentionedPubkeys = [
...new Set( ...new Set(

View File

@ -1,4 +1,4 @@
import { Comlink, type Event } from '@/deps.ts'; import { Comlink, type NostrEvent } from '@/deps.ts';
import type { VerifyWorker } from './verify.worker.ts'; import type { VerifyWorker } from './verify.worker.ts';
@ -6,7 +6,7 @@ const worker = Comlink.wrap<typeof VerifyWorker>(
new Worker(new URL('./verify.worker.ts', import.meta.url), { type: 'module' }), new Worker(new URL('./verify.worker.ts', import.meta.url), { type: 'module' }),
); );
function verifySignatureWorker<K extends number>(event: Event<K>): Promise<boolean> { function verifySignatureWorker(event: NostrEvent): Promise<boolean> {
return worker.verifySignature(event); return worker.verifySignature(event);
} }

View File

@ -1,7 +1,7 @@
import { Comlink, type Event, type VerifiedEvent, verifySignature } from '@/deps.ts'; import { Comlink, type NostrEvent, type VerifiedEvent, verifySignature } from '@/deps.ts';
export const VerifyWorker = { export const VerifyWorker = {
verifySignature<K extends number>(event: Event<K>): event is VerifiedEvent<K> { verifySignature(event: NostrEvent): event is VerifiedEvent {
return verifySignature(event); return verifySignature(event);
}, },
}; };