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,
|
||||
cors,
|
||||
Debug,
|
||||
type Event,
|
||||
type Handler,
|
||||
Hono,
|
||||
type HonoEnv,
|
||||
logger,
|
||||
type MiddlewareHandler,
|
||||
type NostrEvent,
|
||||
sentryMiddleware,
|
||||
serveStatic,
|
||||
} 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. */
|
||||
seckey?: string;
|
||||
/** 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?: User;
|
||||
};
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { type AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { insertUser } from '@/db/users.ts';
|
||||
import { findReplyTag, nip19, z } from '@/deps.ts';
|
||||
import { type DittoFilter } from '@/filter.ts';
|
||||
import { nip19, z } from '@/deps.ts';
|
||||
import { getAuthor, getFollowedPubkeys } from '@/queries.ts';
|
||||
import { booleanParamSchema, fileSchema } from '@/schema.ts';
|
||||
import { jsonMetaContentSchema } from '@/schemas/nostr.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 { lookupAccount, nostrNow } from '@/utils.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 { renderRelationship } from '@/views/mastodon/relationships.ts';
|
||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||
import { DittoFilter } from '@/interfaces/DittoFilter.ts';
|
||||
|
||||
const usernameSchema = z
|
||||
.string().min(1).max(30)
|
||||
|
@ -143,7 +143,7 @@ const accountStatusesController: AppController = async (c) => {
|
|||
}
|
||||
}
|
||||
|
||||
const filter: DittoFilter<1> = {
|
||||
const filter: DittoFilter = {
|
||||
authors: [pubkey],
|
||||
kinds: [1],
|
||||
relations: ['author', 'event_stats', 'author_stats'],
|
||||
|
@ -159,7 +159,7 @@ const accountStatusesController: AppController = async (c) => {
|
|||
let events = await eventsDB.filter([filter]);
|
||||
|
||||
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'))));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AppController } from '@/app.ts';
|
||||
import { type Event, nip19, z } from '@/deps.ts';
|
||||
import { type DittoFilter } from '@/filter.ts';
|
||||
import { nip19, type NostrEvent, z } from '@/deps.ts';
|
||||
import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
|
||||
import { booleanParamSchema } from '@/schema.ts';
|
||||
import { nostrIdSchema } from '@/schemas/nostr.ts';
|
||||
import { searchStore } from '@/storages.ts';
|
||||
|
@ -46,12 +46,12 @@ const searchController: AppController = async (c) => {
|
|||
const [accounts, statuses] = await Promise.all([
|
||||
Promise.all(
|
||||
results
|
||||
.filter((event): event is Event<0> => event.kind === 0)
|
||||
.filter((event): event is NostrEvent => event.kind === 0)
|
||||
.map((event) => renderAccount(event)),
|
||||
),
|
||||
Promise.all(
|
||||
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'))),
|
||||
),
|
||||
]);
|
||||
|
@ -64,7 +64,7 @@ const searchController: AppController = async (c) => {
|
|||
};
|
||||
|
||||
/** 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([]);
|
||||
|
||||
const filter: DittoFilter = {
|
||||
|
@ -94,7 +94,7 @@ function typeToKinds(type: SearchQuery['type']): number[] {
|
|||
}
|
||||
|
||||
/** 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 [event] = await searchStore.filter(filters, { limit: 1, signal });
|
||||
return event;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { type AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.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 { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
||||
import { addTag, deleteTag } from '@/tags.ts';
|
||||
|
@ -100,7 +100,7 @@ const contextController: AppController = async (c) => {
|
|||
const id = c.req.param('id');
|
||||
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'))));
|
||||
return statuses.filter(Boolean);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { type AppController } from '@/app.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 { Sub } from '@/subs.ts';
|
||||
import { bech32ToPubkey } from '@/utils.ts';
|
||||
|
@ -86,7 +86,7 @@ async function topicToFilter(
|
|||
topic: Stream,
|
||||
pubkey: string,
|
||||
query: Record<string, string>,
|
||||
): Promise<DittoFilter<1> | undefined> {
|
||||
): Promise<DittoFilter | undefined> {
|
||||
switch (topic) {
|
||||
case 'public':
|
||||
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 { type DittoFilter } from '@/filter.ts';
|
||||
import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
|
||||
import { getFeedPubkeys } from '@/queries.ts';
|
||||
import { booleanParamSchema } from '@/schema.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { paginated, paginationSchema } from '@/utils/api.ts';
|
||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||
|
||||
import type { AppContext, AppController } from '@/app.ts';
|
||||
|
||||
const homeTimelineController: AppController = async (c) => {
|
||||
const params = paginationSchema.parse(c.req.query());
|
||||
const pubkey = c.get('pubkey')!;
|
||||
|
@ -32,7 +31,7 @@ const hashtagTimelineController: AppController = (c) => {
|
|||
};
|
||||
|
||||
/** 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(
|
||||
filters.map((filter) => ({ ...filter, relations: ['author', 'event_stats', 'author_stats'] })),
|
||||
{ signal },
|
||||
|
|
|
@ -13,14 +13,14 @@ import {
|
|||
import { Sub } from '@/subs.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. */
|
||||
const FILTER_LIMIT = 100;
|
||||
|
||||
/** NIP-01 relay to client message. */
|
||||
type RelayMsg =
|
||||
| ['EVENT', string, Event]
|
||||
| ['EVENT', string, NostrEvent]
|
||||
| ['NOTICE', string]
|
||||
| ['EOSE', string]
|
||||
| ['OK', string, boolean, string]
|
||||
|
@ -109,7 +109,7 @@ function connectStream(socket: WebSocket) {
|
|||
}
|
||||
|
||||
/** Enforce the filters with certain criteria. */
|
||||
function prepareFilters(filters: ClientREQ[2][]): Filter[] {
|
||||
function prepareFilters(filters: ClientREQ[2][]): NostrFilter[] {
|
||||
return filters.map((filter) => ({
|
||||
...filter,
|
||||
// Return only local events unless the query is already narrow.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { signAdminEvent } from '@/sign.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
|
@ -49,7 +49,7 @@ async function insertUser(user: User) {
|
|||
* ```
|
||||
*/
|
||||
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)) {
|
||||
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 { RelayPoolWorker } from 'https://dev.jspm.io/nostr-relaypool@0.6.30';
|
||||
export {
|
||||
type Event,
|
||||
type EventTemplate,
|
||||
type Filter,
|
||||
finishEvent,
|
||||
getEventHash,
|
||||
getPublicKey,
|
||||
|
@ -29,7 +27,6 @@ export {
|
|||
type VerifiedEvent,
|
||||
verifySignature,
|
||||
} 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';
|
||||
// @deno-types="npm:@types/lodash@4.14.194"
|
||||
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';
|
||||
// @deno-types="npm:@types/debug@^4.1.12"
|
||||
export { default as Debug } from 'npm:debug@^4.3.4';
|
||||
export { NSet } from 'https://gitlab.com/soapbox-pub/nset/-/raw/b3c5601612f9bd277626198c5534e0796e003884/mod.ts';
|
||||
export {
|
||||
LNURL,
|
||||
type LNURLDetails,
|
||||
type MapCache,
|
||||
NIP05,
|
||||
type NostrEvent,
|
||||
type NostrFilter,
|
||||
} from 'https://gitlab.com/soapbox-pub/nlib/-/raw/5d711597f3b2a163817cc1fb0f1f3ce8cede7cf7/mod.ts';
|
||||
|
||||
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 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';
|
||||
|
||||
Deno.test('getMicroFilters', () => {
|
||||
const event = event0 as Event<0>;
|
||||
const event = event0;
|
||||
const microfilters = getMicroFilters(event);
|
||||
assertEquals(microfilters.length, 2);
|
||||
assertEquals(microfilters[0], { authors: [event.pubkey], kinds: [0] });
|
||||
|
|
|
@ -1,24 +1,14 @@
|
|||
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 { 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. */
|
||||
type IdMicrofilter = { ids: [Event['id']] };
|
||||
type IdMicrofilter = { ids: [NostrEvent['id']] };
|
||||
/** 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. */
|
||||
type MicroFilter = IdMicrofilter | AuthorMicrofilter;
|
||||
|
||||
|
@ -57,13 +47,13 @@ function getFilterId(filter: MicroFilter): string {
|
|||
}
|
||||
|
||||
/** Get a microfilter from a Nostr event. */
|
||||
function eventToMicroFilter(event: Event): MicroFilter {
|
||||
function eventToMicroFilter(event: NostrEvent): MicroFilter {
|
||||
const [microfilter] = getMicroFilters(event);
|
||||
return microfilter;
|
||||
}
|
||||
|
||||
/** Get all the microfilters for an event, in order of priority. */
|
||||
function getMicroFilters(event: Event): MicroFilter[] {
|
||||
function getMicroFilters(event: NostrEvent): MicroFilter[] {
|
||||
const microfilters: MicroFilter[] = [];
|
||||
if (event.kind === 0) {
|
||||
microfilters.push({ kinds: [0], authors: [event.pubkey] });
|
||||
|
@ -79,12 +69,12 @@ const microFilterSchema = z.union([
|
|||
]);
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
/** 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.kinds && !filter.kinds.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. */
|
||||
function canFilter(filter: Filter): boolean {
|
||||
function canFilter(filter: NostrFilter): boolean {
|
||||
return getFilterLimit(filter) > 0;
|
||||
}
|
||||
|
||||
/** 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) => {
|
||||
const limit = getFilterLimit(filter);
|
||||
if (limit > 0) {
|
||||
|
@ -118,7 +108,6 @@ function normalizeFilters<F extends Filter>(filters: F[]): F[] {
|
|||
export {
|
||||
type AuthorMicrofilter,
|
||||
canFilter,
|
||||
type DittoFilter,
|
||||
eventToMicroFilter,
|
||||
getFilterId,
|
||||
getFilterLimit,
|
||||
|
@ -128,5 +117,4 @@ export {
|
|||
matchDittoFilters,
|
||||
type MicroFilter,
|
||||
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 { nostrNow } from '@/utils.ts';
|
||||
|
||||
|
@ -18,8 +18,8 @@ pool.subscribe(
|
|||
);
|
||||
|
||||
/** Handle events through the firehose pipeline. */
|
||||
function handleEvent(event: Event): Promise<void> {
|
||||
debug(`Event<${event.kind}> ${event.id}`);
|
||||
function handleEvent(event: NostrEvent): Promise<void> {
|
||||
debug(`NostrEvent<${event.kind}> ${event.id}`);
|
||||
|
||||
return pipeline
|
||||
.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 Event, HTTPException } from '@/deps.ts';
|
||||
import { HTTPException, type NostrEvent } from '@/deps.ts';
|
||||
import {
|
||||
buildAuthEventTemplate,
|
||||
parseAuthRequest,
|
||||
|
@ -65,7 +65,7 @@ function matchesRole(user: User, role: UserRole): boolean {
|
|||
|
||||
/** HOC to obtain proof in middleware. */
|
||||
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,
|
||||
): AppMiddleware {
|
||||
return async (c, next) => {
|
||||
|
|
|
@ -2,7 +2,8 @@ import { Conf } from '@/config.ts';
|
|||
import { encryptAdmin } from '@/crypto.ts';
|
||||
import { addRelays } from '@/db/relays.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 { isLocallyFollowed } from '@/queries.ts';
|
||||
import { updateStats } from '@/stats.ts';
|
||||
|
@ -15,7 +16,6 @@ import { TrendsWorker } from '@/workers/trends.ts';
|
|||
import { verifySignatureWorker } from '@/workers/verify.ts';
|
||||
import { signAdminEvent } from '@/sign.ts';
|
||||
import { lnurlCache } from '@/utils/lnurl.ts';
|
||||
import { DittoEvent } from '@/storages/types.ts';
|
||||
|
||||
const debug = Debug('ditto:pipeline');
|
||||
|
||||
|
@ -28,7 +28,7 @@ async function handleEvent(event: DittoEvent): Promise<void> {
|
|||
if (!(await verifySignatureWorker(event))) return;
|
||||
const wanted = reqmeister.isWanted(event);
|
||||
if (await encounterEvent(event)) return;
|
||||
debug(`Event<${event.kind}> ${event.id}`);
|
||||
debug(`NostrEvent<${event.kind}> ${event.id}`);
|
||||
await hydrateEvent(event);
|
||||
|
||||
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. */
|
||||
async function encounterEvent(event: Event): Promise<boolean> {
|
||||
async function encounterEvent(event: NostrEvent): Promise<boolean> {
|
||||
const preexisting = (await memorelay.count([{ ids: [event.id] }])) > 0;
|
||||
memorelay.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. */
|
||||
const isAdminEvent = ({ pubkey }: Event): boolean => pubkey === Conf.pubkey;
|
||||
const isAdminEvent = ({ pubkey }: NostrEvent): boolean => pubkey === Conf.pubkey;
|
||||
|
||||
interface StoreEventOpts {
|
||||
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. */
|
||||
async function processDeletions(event: Event): Promise<void> {
|
||||
async function processDeletions(event: NostrEvent): Promise<void> {
|
||||
if (event.kind === 5) {
|
||||
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. */
|
||||
async function trackHashtags(event: Event): Promise<void> {
|
||||
async function trackHashtags(event: NostrEvent): Promise<void> {
|
||||
const date = nostrDate(event.created_at);
|
||||
|
||||
const tags = event.tags
|
||||
|
@ -127,7 +127,7 @@ async function trackHashtags(event: Event): Promise<void> {
|
|||
}
|
||||
|
||||
/** Tracks known relays in the database. */
|
||||
function trackRelays(event: Event) {
|
||||
function trackRelays(event: NostrEvent) {
|
||||
const relays = new Set<`wss://${string}`>();
|
||||
|
||||
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. */
|
||||
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. */
|
||||
function streamOut(event: Event) {
|
||||
function streamOut(event: NostrEvent) {
|
||||
if (!isFresh(event)) return;
|
||||
|
||||
for (const sub of Sub.matches(event)) {
|
||||
|
|
|
@ -1,37 +1,38 @@
|
|||
import { eventsDB, memorelay, reqmeister } from '@/storages.ts';
|
||||
import { Debug, type Event, findReplyTag } from '@/deps.ts';
|
||||
import { type AuthorMicrofilter, type DittoFilter, type IdMicrofilter, type Relation } from '@/filter.ts';
|
||||
import { type DittoEvent } from '@/storages/types.ts';
|
||||
import { getTagSet } from '@/tags.ts';
|
||||
import { Debug, type NostrEvent } from '@/deps.ts';
|
||||
import { type AuthorMicrofilter, type IdMicrofilter } from '@/filter.ts';
|
||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||
import { type DittoFilter, type DittoRelation } from '@/interfaces/DittoFilter.ts';
|
||||
import { findReplyTag, getTagSet } from '@/tags.ts';
|
||||
|
||||
const debug = Debug('ditto:queries');
|
||||
|
||||
interface GetEventOpts<K extends number> {
|
||||
interface GetEventOpts {
|
||||
/** Signal to abort the request. */
|
||||
signal?: AbortSignal;
|
||||
/** Event kind. */
|
||||
kind?: K;
|
||||
kind?: number;
|
||||
/** Relations to include on the event. */
|
||||
relations?: Relation[];
|
||||
relations?: DittoRelation[];
|
||||
}
|
||||
|
||||
/** Get a Nostr event by its ID. */
|
||||
const getEvent = async <K extends number = number>(
|
||||
const getEvent = async (
|
||||
id: string,
|
||||
opts: GetEventOpts<K> = {},
|
||||
): Promise<DittoEvent<K> | undefined> => {
|
||||
opts: GetEventOpts = {},
|
||||
): Promise<DittoEvent | undefined> => {
|
||||
debug(`getEvent: ${id}`);
|
||||
const { kind, relations, signal = AbortSignal.timeout(1000) } = opts;
|
||||
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) {
|
||||
debug(`getEvent: ${id.slice(0, 8)} found in memory`);
|
||||
return memoryEvent;
|
||||
}
|
||||
|
||||
const filter: DittoFilter<K> = { ids: [id], relations, limit: 1 };
|
||||
const filter: DittoFilter = { ids: [id], relations, limit: 1 };
|
||||
if (kind) {
|
||||
filter.kinds = [kind];
|
||||
}
|
||||
|
@ -61,7 +62,7 @@ const getEvent = async <K extends number = number>(
|
|||
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) {
|
||||
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. */
|
||||
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 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. */
|
||||
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 });
|
||||
return event;
|
||||
};
|
||||
|
@ -112,9 +113,9 @@ async function getFeedPubkeys(pubkey: string): Promise<string[]> {
|
|||
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) {
|
||||
const replyTag = findReplyTag(event);
|
||||
const replyTag = findReplyTag(event.tags);
|
||||
const inReplyTo = replyTag ? replyTag[1] : undefined;
|
||||
|
||||
if (inReplyTo) {
|
||||
|
@ -130,7 +131,7 @@ async function getAncestors(event: Event<1>, result = [] as Event<1>[]): Promise
|
|||
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(
|
||||
[{ kinds: [1], '#e': [eventId], relations: ['author', 'event_stats', 'author_stats'] }],
|
||||
{ limit: 200, signal },
|
||||
|
|
26
src/sign.ts
26
src/sign.ts
|
@ -1,7 +1,7 @@
|
|||
import { type AppContext } from '@/app.ts';
|
||||
import { Conf } from '@/config.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 { jsonSchema } from '@/schema.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 `X-Nostr-Sign` is passed, it will use NIP-46 to sign the event.
|
||||
*/
|
||||
async function signEvent<K extends number = number>(
|
||||
event: EventTemplate<K>,
|
||||
async function signEvent(
|
||||
event: EventTemplate,
|
||||
c: AppContext,
|
||||
opts: SignEventOpts = {},
|
||||
): Promise<Event<K>> {
|
||||
): Promise<NostrEvent> {
|
||||
const seckey = c.get('seckey');
|
||||
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. */
|
||||
async function signNostrConnect<K extends number = number>(
|
||||
event: EventTemplate<K>,
|
||||
async function signNostrConnect(
|
||||
event: EventTemplate,
|
||||
c: AppContext,
|
||||
opts: SignEventOpts = {},
|
||||
): Promise<Event<K>> {
|
||||
): Promise<NostrEvent> {
|
||||
const pubkey = c.get('pubkey');
|
||||
|
||||
if (!pubkey) {
|
||||
|
@ -73,16 +73,16 @@ async function signNostrConnect<K extends number = number>(
|
|||
tags: [['p', pubkey]],
|
||||
}, c);
|
||||
|
||||
return awaitSignedEvent<K>(pubkey, messageId, event, c);
|
||||
return awaitSignedEvent(pubkey, messageId, event, c);
|
||||
}
|
||||
|
||||
/** Wait for signed event to be sent through Nostr relay. */
|
||||
async function awaitSignedEvent<K extends number = number>(
|
||||
async function awaitSignedEvent(
|
||||
pubkey: string,
|
||||
messageId: string,
|
||||
template: EventTemplate<K>,
|
||||
template: EventTemplate,
|
||||
c: AppContext,
|
||||
): Promise<Event<K>> {
|
||||
): Promise<NostrEvent> {
|
||||
const sub = Sub.sub(messageId, '1', [{ kinds: [24133], authors: [pubkey], '#p': [Conf.pubkey] }]);
|
||||
|
||||
function close(): void {
|
||||
|
@ -103,7 +103,7 @@ async function awaitSignedEvent<K extends number = number>(
|
|||
if (result.success) {
|
||||
close();
|
||||
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. */
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
|
21
src/stats.ts
21
src/stats.ts
|
@ -1,6 +1,7 @@
|
|||
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 { findReplyTag } from '@/tags.ts';
|
||||
|
||||
type AuthorStat = keyof Omit<AuthorStatsRow, 'pubkey'>;
|
||||
type EventStat = keyof Omit<EventStatsRow, 'event_id'>;
|
||||
|
@ -12,15 +13,15 @@ type StatDiff = AuthorStatDiff | EventStatDiff;
|
|||
const debug = Debug('ditto:stats');
|
||||
|
||||
/** Store stats for the event in LMDB. */
|
||||
async function updateStats<K extends number>(event: Event<K>) {
|
||||
let prev: Event<K> | undefined;
|
||||
async function updateStats(event: NostrEvent) {
|
||||
let prev: NostrEvent | undefined;
|
||||
const queries: InsertQueryBuilder<DittoDB, any, unknown>[] = [];
|
||||
|
||||
// Kind 3 is a special case - replace the count with the new list.
|
||||
if (event.kind === 3) {
|
||||
prev = await maybeGetPrev(event);
|
||||
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. */
|
||||
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 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) {
|
||||
case 1:
|
||||
|
@ -55,7 +56,7 @@ function getStatsDiff<K extends number>(event: Event<K>, prev: Event<K> | undefi
|
|||
}
|
||||
break;
|
||||
case 3:
|
||||
statDiffs.push(...getFollowDiff(event as Event<3>, prev as Event<3> | undefined));
|
||||
statDiffs.push(...getFollowDiff(event, prev));
|
||||
break;
|
||||
case 6:
|
||||
if (firstTaggedId) {
|
||||
|
@ -124,7 +125,7 @@ function eventStatsQuery(diffs: EventStatDiff[]) {
|
|||
}
|
||||
|
||||
/** 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([
|
||||
{ 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. */
|
||||
function updateFollowingCountQuery({ pubkey, tags }: Event<3>) {
|
||||
function updateFollowingCountQuery({ pubkey, tags }: NostrEvent) {
|
||||
const following_count = new Set(
|
||||
tags
|
||||
.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. */
|
||||
function getFollowDiff(event: Event<3>, prev?: Event<3>): AuthorStatDiff[] {
|
||||
function getFollowDiff(event: NostrEvent, prev?: NostrEvent): AuthorStatDiff[] {
|
||||
const prevTags = prev?.tags ?? [];
|
||||
|
||||
const prevPubkeys = new Set(
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import { Conf } from '@/config.ts';
|
||||
import { type DittoDB } from '@/db.ts';
|
||||
import { Debug, type Event, Kysely, type SelectQueryBuilder } from '@/deps.ts';
|
||||
import { type DittoFilter, normalizeFilters } from '@/filter.ts';
|
||||
import { Debug, Kysely, type NostrEvent, type SelectQueryBuilder } from '@/deps.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 { jsonMetaContentSchema } from '@/schemas/nostr.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. */
|
||||
type TagCondition = ({ event, count, value }: {
|
||||
|
@ -65,7 +68,8 @@ class EventsDB implements EventStore {
|
|||
}
|
||||
|
||||
/** 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));
|
||||
|
||||
if (isDittoInternalKind(event.kind) && event.pubkey !== Conf.pubkey) {
|
||||
|
@ -264,7 +268,7 @@ class EventsDB implements EventStore {
|
|||
}
|
||||
|
||||
/** 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.
|
||||
|
||||
if (opts.signal?.aborted) return Promise.resolve([]);
|
||||
|
@ -278,9 +282,9 @@ class EventsDB implements EventStore {
|
|||
}
|
||||
|
||||
return (await query.execute()).map((row) => {
|
||||
const event: DittoEvent<K> = {
|
||||
const event: DittoEvent = {
|
||||
id: row.id,
|
||||
kind: row.kind as K,
|
||||
kind: row.kind,
|
||||
pubkey: row.pubkey,
|
||||
content: row.content,
|
||||
created_at: row.created_at,
|
||||
|
@ -337,7 +341,7 @@ class EventsDB implements EventStore {
|
|||
}
|
||||
|
||||
/** 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();
|
||||
this.#debug('DELETE', JSON.stringify(filters));
|
||||
|
||||
|
@ -345,7 +349,7 @@ class EventsDB implements EventStore {
|
|||
}
|
||||
|
||||
/** 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);
|
||||
this.#debug('COUNT', JSON.stringify(filters));
|
||||
const query = this.getEventsQuery(filters);
|
||||
|
@ -393,10 +397,10 @@ function filterIndexableTags(event: DittoEvent): string[][] {
|
|||
}
|
||||
|
||||
/** Build a search index from the event. */
|
||||
function buildSearchContent(event: Event): string {
|
||||
function buildSearchContent(event: NostrEvent): string {
|
||||
switch (event.kind) {
|
||||
case 0:
|
||||
return buildUserSearchContent(event as Event<0>);
|
||||
return buildUserSearchContent(event);
|
||||
case 1:
|
||||
return event.content;
|
||||
case 30009:
|
||||
|
@ -407,7 +411,7 @@ function buildSearchContent(event: Event): string {
|
|||
}
|
||||
|
||||
/** 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);
|
||||
return [name, nip05, about].filter(Boolean).join('\n');
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { type DittoFilter } from '@/filter.ts';
|
||||
import { type DittoEvent, type EventStore } from '@/storages/types.ts';
|
||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||
import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
|
||||
import { type EventStore } from '@/storages/types.ts';
|
||||
|
||||
interface HydrateEventOpts<K extends number> {
|
||||
events: DittoEvent<K>[];
|
||||
filters: DittoFilter<K>[];
|
||||
interface HydrateEventOpts {
|
||||
events: DittoEvent[];
|
||||
filters: DittoFilter[];
|
||||
storage: EventStore;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
|
||||
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 { EventSet } from '@/utils/event-set.ts';
|
||||
|
||||
import { type EventStore, type GetEventsOpts } from './types.ts';
|
||||
|
||||
/** In-memory data store for events. */
|
||||
class Memorelay implements EventStore {
|
||||
#debug = Debug('ditto:memorelay');
|
||||
#cache: LRUCache<string, Event>;
|
||||
#cache: LRUCache<string, NostrEvent>;
|
||||
|
||||
/** NIPs supported by this storage method. */
|
||||
supportedNips = [1, 45];
|
||||
|
||||
constructor(...args: ConstructorParameters<typeof LRUCache<string, Event>>) {
|
||||
this.#cache = new LRUCache<string, Event>(...args);
|
||||
constructor(...args: ConstructorParameters<typeof LRUCache<string, NostrEvent>>) {
|
||||
this.#cache = new LRUCache<string, NostrEvent>(...args);
|
||||
}
|
||||
|
||||
/** Iterate stored events. */
|
||||
*#events(): Generator<Event> {
|
||||
*#events(): Generator<NostrEvent> {
|
||||
for (const event of this.#cache.values()) {
|
||||
if (event && !(event instanceof Promise)) {
|
||||
yield event;
|
||||
|
@ -26,7 +25,7 @@ class Memorelay implements EventStore {
|
|||
}
|
||||
|
||||
/** 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);
|
||||
|
||||
if (opts.signal?.aborted) return Promise.resolve([]);
|
||||
|
@ -35,7 +34,7 @@ class Memorelay implements EventStore {
|
|||
this.#debug('REQ', JSON.stringify(filters));
|
||||
|
||||
/** 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. */
|
||||
const filterUsages: number[] = [];
|
||||
|
@ -52,7 +51,7 @@ class Memorelay implements EventStore {
|
|||
for (const id of filter.ids) {
|
||||
const event = this.#cache.get(id);
|
||||
if (event && matchFilter(filter, event)) {
|
||||
results.add(event as Event<K>);
|
||||
results.add(event);
|
||||
}
|
||||
}
|
||||
filterUsages[index] = Infinity;
|
||||
|
@ -73,7 +72,7 @@ class Memorelay implements EventStore {
|
|||
if (usage >= limit) {
|
||||
return;
|
||||
} else if (matchFilter(filter, event)) {
|
||||
results.add(event as Event<K>);
|
||||
results.add(event);
|
||||
this.#cache.get(event.id);
|
||||
filterUsages[index] = usage + 1;
|
||||
}
|
||||
|
@ -91,19 +90,19 @@ class Memorelay implements EventStore {
|
|||
}
|
||||
|
||||
/** Insert an event into memory. */
|
||||
add(event: Event): Promise<void> {
|
||||
add(event: NostrEvent): Promise<void> {
|
||||
this.#cache.set(event.id, event);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/** 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);
|
||||
return events.length;
|
||||
}
|
||||
|
||||
/** Delete events from memory. */
|
||||
async deleteFilters(filters: Filter[]): Promise<void> {
|
||||
async deleteFilters(filters: NostrFilter[]): Promise<void> {
|
||||
for (const event of await this.filter(filters)) {
|
||||
this.#cache.delete(event.id);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Debug } from '@/deps.ts';
|
||||
import { type DittoFilter, normalizeFilters } from '@/filter.ts';
|
||||
import { EventSet } from '@/utils/event-set.ts';
|
||||
import { Debug, NSet } from '@/deps.ts';
|
||||
import { normalizeFilters } from '@/filter.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 {
|
||||
db: EventStore;
|
||||
|
@ -25,17 +26,17 @@ class Optimizer implements EventStore {
|
|||
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([
|
||||
this.#db.add(event, opts),
|
||||
this.#cache.add(event, opts),
|
||||
]);
|
||||
}
|
||||
|
||||
async filter<K extends number>(
|
||||
filters: DittoFilter<K>[],
|
||||
async filter(
|
||||
filters: DittoFilter[],
|
||||
opts: GetEventsOpts | undefined = {},
|
||||
): Promise<DittoEvent<K>[]> {
|
||||
): Promise<DittoEvent[]> {
|
||||
this.#debug('REQ', JSON.stringify(filters));
|
||||
|
||||
const { limit = Infinity } = opts;
|
||||
|
@ -44,7 +45,7 @@ class Optimizer implements EventStore {
|
|||
if (opts?.signal?.aborted) 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.
|
||||
for (let i = 0; i < filters.length; i++) {
|
||||
|
@ -99,11 +100,11 @@ class Optimizer implements EventStore {
|
|||
return getResults();
|
||||
}
|
||||
|
||||
countEvents<K extends number>(_filters: DittoFilter<K>[]): Promise<number> {
|
||||
countEvents(_filters: DittoFilter[]): Promise<number> {
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { type EventStore, type GetEventsOpts, type StoreEventOpts } from '@/storages/types.ts';
|
||||
import { EventSet } from '@/utils/event-set.ts';
|
||||
|
||||
interface PoolStoreOpts {
|
||||
pool: InstanceType<typeof RelayPoolWorker>;
|
||||
relays: WebSocket['url'][];
|
||||
publisher: {
|
||||
handleEvent(event: Event): Promise<void>;
|
||||
handleEvent(event: NostrEvent): Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ class PoolStore implements EventStore {
|
|||
#pool: InstanceType<typeof RelayPoolWorker>;
|
||||
#relays: WebSocket['url'][];
|
||||
#publisher: {
|
||||
handleEvent(event: Event): Promise<void>;
|
||||
handleEvent(event: NostrEvent): Promise<void>;
|
||||
};
|
||||
|
||||
supportedNips = [1];
|
||||
|
@ -27,14 +27,15 @@ class PoolStore implements EventStore {
|
|||
this.#publisher = opts.publisher;
|
||||
}
|
||||
|
||||
add(event: Event, opts: StoreEventOpts = {}): Promise<void> {
|
||||
add(event: NostrEvent, opts: StoreEventOpts = {}): Promise<void> {
|
||||
const { relays = this.#relays } = opts;
|
||||
event = cleanEvent(event);
|
||||
this.#debug('EVENT', event);
|
||||
this.#pool.publish(event, relays);
|
||||
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);
|
||||
|
||||
if (opts.signal?.aborted) return Promise.resolve([]);
|
||||
|
@ -43,17 +44,17 @@ class PoolStore implements EventStore {
|
|||
this.#debug('REQ', JSON.stringify(filters));
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const results = new EventSet<Event<K>>();
|
||||
const results = new NSet<NostrEvent>();
|
||||
|
||||
const unsub = this.#pool.subscribe(
|
||||
filters,
|
||||
opts.relays ?? this.#relays,
|
||||
(event: Event | null) => {
|
||||
(event: NostrEvent | null) => {
|
||||
if (event && matchFilters(filters, event)) {
|
||||
this.#publisher.handleEvent(event).catch(() => {});
|
||||
results.add({
|
||||
id: event.id,
|
||||
kind: event.kind as K,
|
||||
kind: event.kind,
|
||||
pubkey: event.pubkey,
|
||||
content: event.content,
|
||||
tags: event.tags,
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
import { Debug, type Event, EventEmitter, type Filter } from '@/deps.ts';
|
||||
import {
|
||||
AuthorMicrofilter,
|
||||
eventToMicroFilter,
|
||||
getFilterId,
|
||||
IdMicrofilter,
|
||||
isMicrofilter,
|
||||
type MicroFilter,
|
||||
} from '@/filter.ts';
|
||||
import { Debug, EventEmitter, type NostrEvent, type NostrFilter } from '@/deps.ts';
|
||||
import { eventToMicroFilter, getFilterId, isMicrofilter, type MicroFilter } from '@/filter.ts';
|
||||
import { type EventStore, GetEventsOpts } from '@/storages/types.ts';
|
||||
import { Time } from '@/utils/time.ts';
|
||||
|
||||
|
@ -24,7 +17,7 @@ interface ReqmeisterReqOpts {
|
|||
type ReqmeisterQueueItem = [string, MicroFilter, WebSocket['url'][]];
|
||||
|
||||
/** 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');
|
||||
|
||||
#opts: ReqmeisterOpts;
|
||||
|
@ -55,8 +48,8 @@ class Reqmeister extends EventEmitter<{ [filterId: string]: (event: Event) => an
|
|||
const queue = this.#queue;
|
||||
this.#queue = [];
|
||||
|
||||
const wantedEvents = new Set<Event['id']>();
|
||||
const wantedAuthors = new Set<Event['pubkey']>();
|
||||
const wantedEvents = new Set<NostrEvent['id']>();
|
||||
const wantedAuthors = new Set<NostrEvent['pubkey']>();
|
||||
|
||||
// TODO: batch by relays.
|
||||
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 (wantedAuthors.size) filters.push({ kinds: [0], authors: [...wantedAuthors] });
|
||||
|
@ -85,10 +78,7 @@ class Reqmeister extends EventEmitter<{ [filterId: string]: (event: Event) => an
|
|||
this.#perform();
|
||||
}
|
||||
|
||||
req(filter: IdMicrofilter, opts?: ReqmeisterReqOpts): Promise<Event>;
|
||||
req(filter: AuthorMicrofilter, opts?: ReqmeisterReqOpts): Promise<Event<0>>;
|
||||
req(filter: MicroFilter, opts?: ReqmeisterReqOpts): Promise<Event>;
|
||||
req(filter: MicroFilter, opts: ReqmeisterReqOpts = {}): Promise<Event> {
|
||||
req(filter: MicroFilter, opts: ReqmeisterReqOpts = {}): Promise<NostrEvent> {
|
||||
const {
|
||||
relays = [],
|
||||
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]);
|
||||
|
||||
return new Promise<Event>((resolve, reject) => {
|
||||
const handleEvent = (event: Event) => {
|
||||
return new Promise<NostrEvent>((resolve, reject) => {
|
||||
const handleEvent = (event: NostrEvent) => {
|
||||
resolve(event);
|
||||
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));
|
||||
this.#queue = this.#queue.filter(([id]) => id !== filterId);
|
||||
this.emit(filterId, event);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
isWanted(event: Event): boolean {
|
||||
isWanted(event: NostrEvent): boolean {
|
||||
const filterId = getFilterId(eventToMicroFilter(event));
|
||||
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 (!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)) {
|
||||
result.push(this.req(filter) as Promise<Event<K>>);
|
||||
result.push(this.req(filter) as Promise<NostrEvent>);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
@ -145,11 +135,11 @@ class Reqmeister extends EventEmitter<{ [filterId: string]: (event: Event) => an
|
|||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
count(_filters: Filter[]): Promise<number> {
|
||||
count(_filters: NostrFilter[]): Promise<number> {
|
||||
throw new Error('COUNT not implemented.');
|
||||
}
|
||||
|
||||
deleteFilters(_filters: Filter[]): Promise<void> {
|
||||
deleteFilters(_filters: NostrFilter[]): Promise<void> {
|
||||
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 { Debug, type Event, type Filter } from '@/deps.ts';
|
||||
import { type DittoFilter, normalizeFilters } from '@/filter.ts';
|
||||
import { Debug, type NostrEvent, type NostrFilter, NSet } from '@/deps.ts';
|
||||
import { normalizeFilters } from '@/filter.ts';
|
||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||
import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
import { type DittoEvent, type EventStore, type GetEventsOpts, type StoreEventOpts } from '@/storages/types.ts';
|
||||
import { EventSet } from '@/utils/event-set.ts';
|
||||
import { type EventStore, type GetEventsOpts, type StoreEventOpts } from '@/storages/types.ts';
|
||||
|
||||
interface SearchStoreOpts {
|
||||
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.');
|
||||
}
|
||||
|
||||
async filter<K extends number>(
|
||||
filters: DittoFilter<K>[],
|
||||
async filter(
|
||||
filters: DittoFilter[],
|
||||
opts?: GetEventsOpts | undefined,
|
||||
): Promise<DittoEvent<K>[]> {
|
||||
): Promise<DittoEvent[]> {
|
||||
filters = normalizeFilters(filters);
|
||||
|
||||
if (opts?.signal?.aborted) return Promise.resolve([]);
|
||||
|
@ -60,7 +61,7 @@ class SearchStore implements EventStore {
|
|||
opts?.signal?.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) {
|
||||
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.');
|
||||
}
|
||||
|
||||
deleteFilters<K extends number>(_filters: Filter<K>[]): Promise<void> {
|
||||
deleteFilters(_filters: NostrFilter[]): Promise<void> {
|
||||
throw new Error('DELETE not implemented.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { type DittoDB } from '@/db.ts';
|
||||
import { type Event } from '@/deps.ts';
|
||||
import { type DittoFilter } from '@/filter.ts';
|
||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||
import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
|
||||
|
||||
/** Additional options to apply to the whole subscription. */
|
||||
interface GetEventsOpts {
|
||||
|
@ -18,30 +17,18 @@ interface StoreEventOpts {
|
|||
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. */
|
||||
interface EventStore {
|
||||
/** Indicates NIPs supported by this data store, similar to NIP-11. For example, `50` would indicate support for `search` filters. */
|
||||
supportedNips: readonly number[];
|
||||
/** Add an event to the store. */
|
||||
add(event: Event, opts?: StoreEventOpts): Promise<void>;
|
||||
add(event: DittoEvent, opts?: StoreEventOpts): Promise<void>;
|
||||
/** 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. */
|
||||
count?<K extends number>(filters: DittoFilter<K>[]): Promise<number>;
|
||||
count?(filters: DittoFilter[]): Promise<number>;
|
||||
/** 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 { type DittoFilter } from '@/filter.ts';
|
||||
import { type DittoEvent } from '@/storages/types.ts';
|
||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||
import { type DittoFilter } from '@/interfaces/DittoFilter.ts';
|
||||
import { Subscription } from '@/subscription.ts';
|
||||
|
||||
const debug = Debug('ditto:subs');
|
||||
|
@ -21,7 +21,7 @@ class SubscriptionStore {
|
|||
* }
|
||||
* ```
|
||||
*/
|
||||
sub<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));
|
||||
let subs = this.#store.get(socket);
|
||||
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import { type Event, Machina } from '@/deps.ts';
|
||||
import { type DittoFilter, matchDittoFilters } from '@/filter.ts';
|
||||
import { type DittoEvent } from '@/storages/types.ts';
|
||||
import { Machina, type NostrEvent } from '@/deps.ts';
|
||||
import { matchDittoFilters } from '@/filter.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>> {
|
||||
filters: DittoFilter<K>[];
|
||||
#machina: Machina<Event<K>>;
|
||||
class Subscription implements AsyncIterable<NostrEvent> {
|
||||
filters: DittoFilter[];
|
||||
#machina: Machina<NostrEvent>;
|
||||
|
||||
constructor(filters: DittoFilter<K>[]) {
|
||||
constructor(filters: DittoFilter[]) {
|
||||
this.filters = filters;
|
||||
this.#machina = new Machina();
|
||||
}
|
||||
|
||||
stream(event: Event<K>): void {
|
||||
stream(event: NostrEvent): void {
|
||||
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 { nip05Cache } from '@/utils/nip05.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);
|
||||
|
||||
/** 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. */
|
||||
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. */
|
||||
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}`);
|
||||
|
||||
const pubkey = bech32ToPubkey(value) ||
|
||||
|
@ -68,7 +68,7 @@ async function lookupAccount(value: string, signal = AbortSignal.timeout(3000)):
|
|||
}
|
||||
|
||||
/** 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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
/** 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()];
|
||||
}
|
||||
|
||||
|
@ -111,7 +111,7 @@ function stripTags<E extends EventTemplate>(event: E, tags: string[] = []): E {
|
|||
}
|
||||
|
||||
/** 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'];
|
||||
|
||||
event = stripTags(event, whitelist);
|
||||
|
|
|
@ -3,10 +3,10 @@ import { Conf } from '@/config.ts';
|
|||
import {
|
||||
type Context,
|
||||
Debug,
|
||||
type Event,
|
||||
EventTemplate,
|
||||
Filter,
|
||||
HTTPException,
|
||||
type NostrEvent,
|
||||
NostrFilter,
|
||||
parseFormData,
|
||||
type TypeFest,
|
||||
z,
|
||||
|
@ -19,10 +19,10 @@ import { nostrNow } from '@/utils.ts';
|
|||
const debug = Debug('ditto:api');
|
||||
|
||||
/** 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. */
|
||||
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');
|
||||
|
||||
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. */
|
||||
interface UpdateEventFilter<K extends number> extends Filter<K> {
|
||||
kinds: [K];
|
||||
interface UpdateEventFilter extends NostrFilter {
|
||||
kinds: [number];
|
||||
limit?: 1;
|
||||
}
|
||||
|
||||
/** Fetch existing event, update it, then publish the new event. */
|
||||
async function updateEvent<K extends number, E extends EventStub<K>>(
|
||||
filter: UpdateEventFilter<K>,
|
||||
fn: (prev: Event<K> | undefined) => E,
|
||||
async function updateEvent<K extends number, E extends EventStub>(
|
||||
filter: UpdateEventFilter,
|
||||
fn: (prev: NostrEvent | undefined) => E,
|
||||
c: AppContext,
|
||||
): Promise<Event<K>> {
|
||||
): Promise<NostrEvent> {
|
||||
const [prev] = await eventsDB.filter([filter], { limit: 1 });
|
||||
return createEvent(fn(prev), c);
|
||||
}
|
||||
|
||||
/** Fetch existing event, update its tags, then publish the new event. */
|
||||
function updateListEvent<K extends number>(
|
||||
filter: UpdateEventFilter<K>,
|
||||
function updateListEvent(
|
||||
filter: UpdateEventFilter,
|
||||
fn: (tags: string[][]) => string[][],
|
||||
c: AppContext,
|
||||
): Promise<Event<K>> {
|
||||
): Promise<NostrEvent> {
|
||||
return updateEvent(filter, (prev) => ({
|
||||
kind: filter.kinds[0],
|
||||
content: prev?.content ?? '',
|
||||
|
@ -69,7 +69,7 @@ function updateListEvent<K extends number>(
|
|||
}
|
||||
|
||||
/** 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({
|
||||
content: '',
|
||||
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. */
|
||||
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);
|
||||
try {
|
||||
await pipeline.handleEvent(event);
|
||||
|
@ -118,7 +118,7 @@ const paginationSchema = z.object({
|
|||
type PaginationParams = z.infer<typeof paginationSchema>;
|
||||
|
||||
/** 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;
|
||||
const firstEvent = events[0];
|
||||
const lastEvent = events[events.length - 1];
|
||||
|
@ -138,7 +138,7 @@ type Entity = { id: string };
|
|||
type HeaderRecord = Record<string, string | string[]>;
|
||||
|
||||
/** 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);
|
||||
|
||||
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 { signedEventSchema } from '@/schemas/nostr.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. */
|
||||
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 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) => 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) => pow ? nip13.getPow(event.id) >= pow : true, 'Insufficient proof of work')
|
||||
.refine(validateBody, 'Event payload does not match request body');
|
||||
|
||||
function validateBody(event: Event<27235>) {
|
||||
function validateBody(event: NostrEvent) {
|
||||
if (!validatePayload) return true;
|
||||
return req.clone().text()
|
||||
.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. */
|
||||
function tagValue(event: Event, tagName: string): string | undefined {
|
||||
function tagValue(event: NostrEvent, tagName: string): string | undefined {
|
||||
return findTag(event.tags, tagName)?.[1];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { AppContext } from '@/app.ts';
|
||||
import { type Filter } from '@/deps.ts';
|
||||
import { type NostrFilter } from '@/deps.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { renderAccount } from '@/views/mastodon/accounts.ts';
|
||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||
import { paginated, paginationSchema } from '@/utils/api.ts';
|
||||
|
||||
/** 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) {
|
||||
return c.json([]);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@ import { Conf } from '@/config.ts';
|
|||
import { jsonMetaContentSchema } from '@/schemas/nostr.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';
|
||||
|
||||
/** 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);
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Conf } from '@/config.ts';
|
||||
import { findUser } from '@/db/users.ts';
|
||||
import { lodash, nip19, type UnsignedEvent } from '@/deps.ts';
|
||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
||||
import { type DittoEvent } from '@/storages/types.ts';
|
||||
import { getLnurl } from '@/utils/lnurl.ts';
|
||||
import { nip05Cache } from '@/utils/nip05.ts';
|
||||
import { Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts';
|
||||
|
@ -13,7 +13,7 @@ interface ToAccountOpts {
|
|||
}
|
||||
|
||||
async function renderAccount(
|
||||
event: Omit<DittoEvent<0>, 'id' | 'sig'>,
|
||||
event: Omit<DittoEvent, 'id' | 'sig'>,
|
||||
opts: ToAccountOpts = {},
|
||||
) {
|
||||
const { withSource = false } = opts;
|
||||
|
@ -81,7 +81,7 @@ async function renderAccount(
|
|||
}
|
||||
|
||||
function accountFromPubkey(pubkey: string, opts: ToAccountOpts = {}) {
|
||||
const event: UnsignedEvent<0> = {
|
||||
const event: UnsignedEvent = {
|
||||
kind: 0,
|
||||
pubkey,
|
||||
content: '',
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { DittoEvent } from '@/storages/types.ts';
|
||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||
import { nostrDate } from '@/utils.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 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 { nostrDate } from '@/utils.ts';
|
||||
import { accountFromPubkey } from '@/views/mastodon/accounts.ts';
|
||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||
|
||||
function renderNotification(event: Event, viewerPubkey?: string) {
|
||||
function renderNotification(event: NostrEvent, viewerPubkey?: string) {
|
||||
switch (event.kind) {
|
||||
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 status = await renderStatus({ ...event, author }, viewerPubkey);
|
||||
if (!status) return;
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.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 { getAuthor } from '@/queries.ts';
|
||||
import { jsonMediaDataSchema } from '@/schemas/nostr.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { type DittoEvent } from '@/storages/types.ts';
|
||||
import { findReplyTag } from '@/tags.ts';
|
||||
import { nostrDate } from '@/utils.ts';
|
||||
import { unfurlCardCached } from '@/utils/unfurl.ts';
|
||||
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||
import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.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
|
||||
? await renderAccount({ ...event.author, author_stats: event.author_stats })
|
||||
: await accountFromPubkey(event.pubkey);
|
||||
|
||||
const replyTag = findReplyTag(event);
|
||||
const replyTag = findReplyTag(event.tags);
|
||||
|
||||
const mentionedPubkeys = [
|
||||
...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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ const worker = Comlink.wrap<typeof VerifyWorker>(
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
verifySignature<K extends number>(event: Event<K>): event is VerifiedEvent<K> {
|
||||
verifySignature(event: NostrEvent): event is VerifiedEvent {
|
||||
return verifySignature(event);
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue