Merge branch 'interfaces' into 'main'
Add new DittoEvent and DittoFilter interfaces See merge request soapbox-pub/ditto!105
This commit is contained in:
commit
0a58233b4e
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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'))));
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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] };
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 };
|
|
@ -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] });
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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[];
|
||||||
|
}
|
|
@ -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) => {
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
26
src/sign.ts
26
src/sign.ts
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
21
src/stats.ts
21
src/stats.ts
|
@ -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(
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'))) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
src/tags.ts
10
src/tags.ts
|
@ -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 };
|
||||||
|
|
12
src/utils.ts
12
src/utils.ts
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
|
@ -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 };
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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([]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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: '',
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue