Merge branch 'interfaces' into 'main'

Add new DittoEvent and DittoFilter interfaces

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

View File

@ -5,12 +5,12 @@ import {
type Context,
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;
};

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,12 @@
import { eventsDB } from '@/storages.ts';
import { type AppContext, type AppController } from '@/app.ts';
import { z } from '@/deps.ts';
import { 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 },

View File

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

View File

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

View File

@ -11,9 +11,7 @@ export { cors, logger, serveStatic } from 'https://deno.land/x/hono@v3.10.1/midd
export { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts';
export { 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';

16
src/events.ts Normal file
View File

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

View File

@ -1,4 +1,3 @@
import { type Event } from '@/deps.ts';
import { assertEquals } from '@/deps-test.ts';
import 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] });

View File

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

View File

@ -1,4 +1,4 @@
import { Debug, type Event } from '@/deps.ts';
import { Debug, type NostrEvent } from '@/deps.ts';
import { activeRelays, pool } from '@/pool.ts';
import { 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)

View File

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

View File

@ -0,0 +1,14 @@
import { type NostrEvent, type NostrFilter } from '@/deps.ts';
import { type DittoEvent } from './DittoEvent.ts';
/** Additional properties that may be added by Ditto to events. */
export type DittoRelation = Exclude<keyof DittoEvent, keyof NostrEvent>;
/** Custom filter interface that extends Nostr filters with extra options for Ditto. */
export interface DittoFilter extends NostrFilter {
/** Whether the event was authored by a local user. */
local?: boolean;
/** Additional fields to add to the returned event. */
relations?: DittoRelation[];
}

View File

@ -1,5 +1,5 @@
import { type AppContext, type AppMiddleware } from '@/app.ts';
import { type 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) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,23 +1,22 @@
import { Debug, type Event, type Filter, LRUCache, matchFilter } from '@/deps.ts';
import { Debug, LRUCache, matchFilter, type NostrEvent, type NostrFilter, NSet } from '@/deps.ts';
import { normalizeFilters } from '@/filter.ts';
import { 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);
}

View File

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

View File

@ -1,13 +1,13 @@
import { Debug, type Event, type Filter, matchFilters, type RelayPoolWorker } from '@/deps.ts';
import { Debug, matchFilters, type NostrEvent, type NostrFilter, NSet, type RelayPoolWorker } from '@/deps.ts';
import { cleanEvent } from '@/events.ts';
import { normalizeFilters } from '@/filter.ts';
import { 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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { type Event, type EventTemplate, getEventHash, nip19, z } from '@/deps.ts';
import { type EventTemplate, getEventHash, nip19, type NostrEvent, z } from '@/deps.ts';
import { getAuthor } from '@/queries.ts';
import { 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);

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { type Event, type EventTemplate, nip13, type VerifiedEvent } from '@/deps.ts';
import { type EventTemplate, nip13, type NostrEvent } from '@/deps.ts';
import { decode64Schema, jsonSchema } from '@/schema.ts';
import { 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];
}

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { DittoEvent } from '@/storages/types.ts';
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { nostrDate } from '@/utils.ts';
import { 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);

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { Comlink, type Event } from '@/deps.ts';
import { Comlink, type NostrEvent } from '@/deps.ts';
import type { VerifyWorker } from './verify.worker.ts';
@ -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);
}

View File

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