Merge branch 'main' into mint-cashu

Conflicts:
	packages/ditto/controllers/api/cashu.ts
This commit is contained in:
P. Reis 2025-02-28 10:51:02 -03:00
commit dcfdfb1c7f
71 changed files with 360 additions and 203 deletions

View file

@ -1,4 +1,4 @@
image: denoland/deno:2.2.0 image: denoland/deno:2.2.2
default: default:
interruptible: true interruptible: true

View file

@ -1 +1 @@
deno 2.2.0 deno 2.2.2

View file

@ -1,4 +1,4 @@
FROM denoland/deno:2.2.0 FROM denoland/deno:2.2.2
ENV PORT 5000 ENV PORT 5000
WORKDIR /app WORKDIR /app

View file

@ -14,10 +14,10 @@ export class DittoPush {
private server: Promise<ApplicationServer | undefined>; private server: Promise<ApplicationServer | undefined>;
constructor(opts: DittoPushOpts) { constructor(opts: DittoPushOpts) {
const { conf, relay } = opts; const { conf } = opts;
this.server = (async () => { this.server = (async () => {
const meta = await getInstanceMetadata(relay); const meta = await getInstanceMetadata(opts);
const keys = await conf.vapidKeys; const keys = await conf.vapidKeys;
if (keys) { if (keys) {

View file

@ -1,5 +1,5 @@
import { DittoConf } from '@ditto/conf'; import { DittoConf } from '@ditto/conf';
import { DittoDB, DittoPolyPg } from '@ditto/db'; import { DittoPolyPg } from '@ditto/db';
import { paginationMiddleware, tokenMiddleware, userMiddleware } from '@ditto/mastoapi/middleware'; import { paginationMiddleware, tokenMiddleware, userMiddleware } from '@ditto/mastoapi/middleware';
import { DittoApp, type DittoEnv } from '@ditto/mastoapi/router'; import { DittoApp, type DittoEnv } from '@ditto/mastoapi/router';
import { relayPoolRelaysSizeGauge, relayPoolSubscriptionsSizeGauge } from '@ditto/metrics'; import { relayPoolRelaysSizeGauge, relayPoolSubscriptionsSizeGauge } from '@ditto/metrics';
@ -12,6 +12,7 @@ import { NostrEvent, NostrSigner, NRelay, NUploader } from '@nostrify/nostrify';
import { cron } from '@/cron.ts'; import { cron } from '@/cron.ts';
import { startFirehose } from '@/firehose.ts'; import { startFirehose } from '@/firehose.ts';
import { startSentry } from '@/sentry.ts';
import { DittoAPIStore } from '@/storages/DittoAPIStore.ts'; import { DittoAPIStore } from '@/storages/DittoAPIStore.ts';
import { DittoPgStore } from '@/storages/DittoPgStore.ts'; import { DittoPgStore } from '@/storages/DittoPgStore.ts';
import { DittoPool } from '@/storages/DittoPool.ts'; import { DittoPool } from '@/storages/DittoPool.ts';
@ -151,21 +152,15 @@ import { logiMiddleware } from '@/middleware/logiMiddleware.ts';
import { DittoRelayStore } from '@/storages/DittoRelayStore.ts'; import { DittoRelayStore } from '@/storages/DittoRelayStore.ts';
export interface AppEnv extends DittoEnv { export interface AppEnv extends DittoEnv {
Variables: { Variables: DittoEnv['Variables'] & {
conf: DittoConf;
/** Uploader for the user to upload files. */ /** Uploader for the user to upload files. */
uploader?: NUploader; uploader?: NUploader;
/** NIP-98 signed event proving the pubkey is owned by the user. */ /** NIP-98 signed event proving the pubkey is owned by the user. */
proof?: NostrEvent; proof?: NostrEvent;
/** Kysely instance for the database. */
db: DittoDB;
/** Base database store. No content filtering. */
relay: NRelay;
/** Normalized pagination params. */ /** Normalized pagination params. */
pagination: { since?: number; until?: number; limit: number }; pagination: { since?: number; until?: number; limit: number };
/** Translation service. */ /** Translation service. */
translator?: DittoTranslator; translator?: DittoTranslator;
signal: AbortSignal;
user?: { user?: {
/** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */ /** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */
signer: NostrSigner; signer: NostrSigner;
@ -182,6 +177,8 @@ type AppController<P extends string = any> = Handler<AppEnv, P, HonoInput, Respo
const conf = new DittoConf(Deno.env); const conf = new DittoConf(Deno.env);
startSentry(conf);
const db = new DittoPolyPg(conf.databaseUrl, { const db = new DittoPolyPg(conf.databaseUrl, {
poolSize: conf.pg.poolSize, poolSize: conf.pg.poolSize,
debug: conf.pgliteDebug, debug: conf.pgliteDebug,
@ -191,7 +188,7 @@ await db.migrate();
const pgstore = new DittoPgStore({ const pgstore = new DittoPgStore({
db, db,
pubkey: await conf.signer.getPublicKey(), conf,
timeout: conf.db.timeouts.default, timeout: conf.db.timeouts.default,
notify: conf.notifyEnabled, notify: conf.notifyEnabled,
}); });
@ -199,7 +196,7 @@ const pgstore = new DittoPgStore({
const pool = new DittoPool({ conf, relay: pgstore }); const pool = new DittoPool({ conf, relay: pgstore });
const relay = new DittoRelayStore({ db, conf, relay: pgstore }); const relay = new DittoRelayStore({ db, conf, relay: pgstore });
await seedZapSplits(relay); await seedZapSplits({ conf, relay });
if (conf.firehoseEnabled) { if (conf.firehoseEnabled) {
startFirehose({ startFirehose({

View file

@ -1,8 +1,8 @@
import { MastodonTranslation } from '@ditto/mastoapi/types';
import { LanguageCode } from 'iso-639-1'; import { LanguageCode } from 'iso-639-1';
import { LRUCache } from 'lru-cache'; import { LRUCache } from 'lru-cache';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { MastodonTranslation } from '@/entities/MastodonTranslation.ts';
/** Translations LRU cache. */ /** Translations LRU cache. */
export const translationCache = new LRUCache<`${LanguageCode}-${string}`, MastodonTranslation>({ export const translationCache = new LRUCache<`${LanguageCode}-${string}`, MastodonTranslation>({

View file

@ -19,7 +19,8 @@ import { hydrateEvents } from '@/storages/hydrate.ts';
import { bech32ToPubkey } from '@/utils.ts'; import { bech32ToPubkey } from '@/utils.ts';
import { addTag, deleteTag, findReplyTag, getTagSet } from '@/utils/tags.ts'; import { addTag, deleteTag, findReplyTag, getTagSet } from '@/utils/tags.ts';
import { getPubkeysBySearch } from '@/utils/search.ts'; import { getPubkeysBySearch } from '@/utils/search.ts';
import { MastodonAccount } from '@/entities/MastodonAccount.ts';
import type { MastodonAccount } from '@ditto/mastoapi/types';
const createAccountSchema = z.object({ const createAccountSchema = z.object({
username: z.string().min(1).max(30).regex(/^[a-z0-9_]+$/i), username: z.string().min(1).max(30).regex(/^[a-z0-9_]+$/i),

View file

@ -128,7 +128,7 @@ const adminAccountActionSchema = z.object({
}); });
const adminActionController: AppController = async (c) => { const adminActionController: AppController = async (c) => {
const { conf, relay } = c.var; const { conf, relay, requestId } = c.var;
const body = await parseBody(c.req.raw); const body = await parseBody(c.req.raw);
const result = adminAccountActionSchema.safeParse(body); const result = adminAccountActionSchema.safeParse(body);
@ -155,7 +155,7 @@ const adminActionController: AppController = async (c) => {
n.disabled = true; n.disabled = true;
n.suspended = true; n.suspended = true;
relay.remove!([{ authors: [authorId] }]).catch((e: unknown) => { relay.remove!([{ authors: [authorId] }]).catch((e: unknown) => {
logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, requestId, error: errorJson(e) });
}); });
} }
if (data.type === 'revoke_name') { if (data.type === 'revoke_name') {
@ -163,7 +163,7 @@ const adminActionController: AppController = async (c) => {
try { try {
await relay.remove!([{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#p': [authorId] }]); await relay.remove!([{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#p': [authorId] }]);
} catch (e) { } catch (e) {
logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, requestId, error: errorJson(e) });
return c.json({ error: 'Unexpected runtime error' }, 500); return c.json({ error: 'Unexpected runtime error' }, 500);
} }
} }

View file

@ -235,7 +235,8 @@ route.put('/wallet', userMiddleware({ enc: 'nip44' }), async (c) => {
/** Gets a wallet, if it exists. */ /** Gets a wallet, if it exists. */
route.get('/wallet', userMiddleware({ enc: 'nip44' }), swapNutzapsMiddleware, async (c) => { route.get('/wallet', userMiddleware({ enc: 'nip44' }), swapNutzapsMiddleware, async (c) => {
const { conf, relay, user, signal } = c.var; const { conf, relay, user, signal, requestId } = c.var;
const pubkey = await user.signer.getPublicKey(); const pubkey = await user.signer.getPublicKey();
const { data, error } = await validateAndParseWallet(relay, user.signer, pubkey, { signal }); const { data, error } = await validateAndParseWallet(relay, user.signer, pubkey, { signal });
@ -262,7 +263,7 @@ route.get('/wallet', userMiddleware({ enc: 'nip44' }), swapNutzapsMiddleware, as
return accumulator + current.amount; return accumulator + current.amount;
}, 0); }, 0);
} catch (e) { } catch (e) {
logi({ level: 'error', ns: 'ditto.api.cashu.wallet', error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.api.cashu.wallet', requestId, error: errorJson(e) });
} }
} }

View file

@ -186,7 +186,8 @@ const zapSplitSchema = z.record(
); );
export const updateZapSplitsController: AppController = async (c) => { export const updateZapSplitsController: AppController = async (c) => {
const { conf, relay } = c.var; const { conf } = c.var;
const body = await parseBody(c.req.raw); const body = await parseBody(c.req.raw);
const result = zapSplitSchema.safeParse(body); const result = zapSplitSchema.safeParse(body);
@ -196,7 +197,7 @@ export const updateZapSplitsController: AppController = async (c) => {
const adminPubkey = await conf.signer.getPublicKey(); const adminPubkey = await conf.signer.getPublicKey();
const dittoZapSplit = await getZapSplits(relay, adminPubkey); const dittoZapSplit = await getZapSplits(adminPubkey, c.var);
if (!dittoZapSplit) { if (!dittoZapSplit) {
return c.json({ error: 'Zap split not activated, restart the server.' }, 404); return c.json({ error: 'Zap split not activated, restart the server.' }, 404);
} }
@ -223,7 +224,8 @@ export const updateZapSplitsController: AppController = async (c) => {
const deleteZapSplitSchema = z.array(n.id()).min(1); const deleteZapSplitSchema = z.array(n.id()).min(1);
export const deleteZapSplitsController: AppController = async (c) => { export const deleteZapSplitsController: AppController = async (c) => {
const { conf, relay } = c.var; const { conf } = c.var;
const body = await parseBody(c.req.raw); const body = await parseBody(c.req.raw);
const result = deleteZapSplitSchema.safeParse(body); const result = deleteZapSplitSchema.safeParse(body);
@ -233,7 +235,7 @@ export const deleteZapSplitsController: AppController = async (c) => {
const adminPubkey = await conf.signer.getPublicKey(); const adminPubkey = await conf.signer.getPublicKey();
const dittoZapSplit = await getZapSplits(relay, adminPubkey); const dittoZapSplit = await getZapSplits(adminPubkey, c.var);
if (!dittoZapSplit) { if (!dittoZapSplit) {
return c.json({ error: 'Zap split not activated, restart the server.' }, 404); return c.json({ error: 'Zap split not activated, restart the server.' }, 404);
} }
@ -253,9 +255,9 @@ export const deleteZapSplitsController: AppController = async (c) => {
}; };
export const getZapSplitsController: AppController = async (c) => { export const getZapSplitsController: AppController = async (c) => {
const { conf, relay } = c.var; const { conf } = c.var;
const dittoZapSplit: DittoZapSplits | undefined = await getZapSplits(relay, await conf.signer.getPublicKey()) ?? {}; const dittoZapSplit: DittoZapSplits | undefined = await getZapSplits(await conf.signer.getPublicKey(), c.var) ?? {};
if (!dittoZapSplit) { if (!dittoZapSplit) {
return c.json({ error: 'Zap split not activated, restart the server.' }, 404); return c.json({ error: 'Zap split not activated, restart the server.' }, 404);
} }
@ -325,7 +327,7 @@ const updateInstanceSchema = z.object({
}); });
export const updateInstanceController: AppController = async (c) => { export const updateInstanceController: AppController = async (c) => {
const { conf, relay, signal } = c.var; const { conf } = c.var;
const body = await parseBody(c.req.raw); const body = await parseBody(c.req.raw);
const result = updateInstanceSchema.safeParse(body); const result = updateInstanceSchema.safeParse(body);
@ -335,7 +337,7 @@ export const updateInstanceController: AppController = async (c) => {
return c.json(result.error, 422); return c.json(result.error, 422);
} }
const meta = await getInstanceMetadata(relay, signal); const meta = await getInstanceMetadata(c.var);
await updateAdminEvent( await updateAdminEvent(
{ kinds: [0], authors: [pubkey], limit: 1 }, { kinds: [0], authors: [pubkey], limit: 1 },

View file

@ -15,9 +15,9 @@ const features = [
]; ];
const instanceV1Controller: AppController = async (c) => { const instanceV1Controller: AppController = async (c) => {
const { conf, relay, signal } = c.var; const { conf } = c.var;
const { host, protocol } = conf.url; const { host, protocol } = conf.url;
const meta = await getInstanceMetadata(relay, signal); const meta = await getInstanceMetadata(c.var);
/** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */
const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:';
@ -75,9 +75,9 @@ const instanceV1Controller: AppController = async (c) => {
}; };
const instanceV2Controller: AppController = async (c) => { const instanceV2Controller: AppController = async (c) => {
const { conf, relay, signal } = c.var; const { conf } = c.var;
const { host, protocol } = conf.url; const { host, protocol } = conf.url;
const meta = await getInstanceMetadata(relay, signal); const meta = await getInstanceMetadata(c.var);
/** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */
const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:';
@ -164,9 +164,7 @@ const instanceV2Controller: AppController = async (c) => {
}; };
const instanceDescriptionController: AppController = async (c) => { const instanceDescriptionController: AppController = async (c) => {
const { relay, signal } = c.var; const meta = await getInstanceMetadata(c.var);
const meta = await getInstanceMetadata(relay, signal);
return c.json({ return c.json({
content: meta.about, content: meta.about,

View file

@ -21,7 +21,7 @@ const mediaUpdateSchema = z.object({
}); });
const mediaController: AppController = async (c) => { const mediaController: AppController = async (c) => {
const { user, signal } = c.var; const { user, signal, requestId } = c.var;
const pubkey = await user!.signer.getPublicKey(); const pubkey = await user!.signer.getPublicKey();
const result = mediaBodySchema.safeParse(await parseBody(c.req.raw)); const result = mediaBodySchema.safeParse(await parseBody(c.req.raw));
@ -35,7 +35,7 @@ const mediaController: AppController = async (c) => {
const media = await uploadFile(c, file, { pubkey, description }, signal); const media = await uploadFile(c, file, { pubkey, description }, signal);
return c.json(renderAttachment(media)); return c.json(renderAttachment(media));
} catch (e) { } catch (e) {
logi({ level: 'error', ns: 'ditto.api.media', error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.api.media', requestId, error: errorJson(e) });
return c.json({ error: 'Failed to upload file.' }, 500); return c.json({ error: 'Failed to upload file.' }, 500);
} }
}; };

View file

@ -7,9 +7,7 @@ import { lookupPubkey } from '@/utils/lookup.ts';
import { getPleromaConfigs } from '@/utils/pleroma.ts'; import { getPleromaConfigs } from '@/utils/pleroma.ts';
const frontendConfigController: AppController = async (c) => { const frontendConfigController: AppController = async (c) => {
const { relay, signal } = c.var; const configDB = await getPleromaConfigs(c.var);
const configDB = await getPleromaConfigs(relay, signal);
const frontendConfig = configDB.get(':pleroma', ':frontend_configurations'); const frontendConfig = configDB.get(':pleroma', ':frontend_configurations');
if (frontendConfig) { if (frontendConfig) {
@ -25,17 +23,15 @@ const frontendConfigController: AppController = async (c) => {
}; };
const configController: AppController = async (c) => { const configController: AppController = async (c) => {
const { relay, signal } = c.var; const configs = await getPleromaConfigs(c.var);
const configs = await getPleromaConfigs(relay, signal);
return c.json({ configs, need_reboot: false }); return c.json({ configs, need_reboot: false });
}; };
/** Pleroma admin config controller. */ /** Pleroma admin config controller. */
const updateConfigController: AppController = async (c) => { const updateConfigController: AppController = async (c) => {
const { conf, relay, signal } = c.var; const { conf } = c.var;
const configs = await getPleromaConfigs(relay, signal); const configs = await getPleromaConfigs(c.var);
const { configs: newConfigs } = z.object({ configs: z.array(configSchema) }).parse(await c.req.json()); const { configs: newConfigs } = z.object({ configs: z.array(configSchema) }).parse(await c.req.json());
configs.merge(newConfigs); configs.merge(newConfigs);

View file

@ -196,7 +196,7 @@ const createStatusController: AppController = async (c) => {
if (conf.zapSplitsEnabled) { if (conf.zapSplitsEnabled) {
const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content); const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content);
const lnurl = getLnurl(meta); const lnurl = getLnurl(meta);
const dittoZapSplit = await getZapSplits(relay, await conf.signer.getPublicKey()); const dittoZapSplit = await getZapSplits(await conf.signer.getPublicKey(), c.var);
if (lnurl && dittoZapSplit) { if (lnurl && dittoZapSplit) {
const totalSplit = Object.values(dittoZapSplit).reduce((total, { weight }) => total + weight, 0); const totalSplit = Object.values(dittoZapSplit).reduce((total, { weight }) => total + weight, 0);
for (const zapPubkey in dittoZapSplit) { for (const zapPubkey in dittoZapSplit) {

View file

@ -65,7 +65,8 @@ const limiter = new TTLCache<string, number>();
const connections = new Set<WebSocket>(); const connections = new Set<WebSocket>();
const streamingController: AppController = async (c) => { const streamingController: AppController = async (c) => {
const { conf, relay, user } = c.var; const { conf, relay, user, requestId } = c.var;
const upgrade = c.req.header('upgrade'); const upgrade = c.req.header('upgrade');
const token = c.req.header('sec-websocket-protocol'); const token = c.req.header('sec-websocket-protocol');
const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream')); const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream'));
@ -122,7 +123,7 @@ const streamingController: AppController = async (c) => {
} }
} }
} catch (e) { } catch (e) {
logi({ level: 'error', ns: 'ditto.streaming', msg: 'Error in streaming', error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.streaming', msg: 'Error in streaming', requestId, error: errorJson(e) });
} }
} }

View file

@ -1,11 +1,11 @@
import { cachedTranslationsSizeGauge } from '@ditto/metrics'; import { cachedTranslationsSizeGauge } from '@ditto/metrics';
import { MastodonTranslation } from '@ditto/mastoapi/types';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { LanguageCode } from 'iso-639-1'; import { LanguageCode } from 'iso-639-1';
import { z } from 'zod'; import { z } from 'zod';
import { AppController } from '@/app.ts'; import { AppController } from '@/app.ts';
import { translationCache } from '@/caches/translationCache.ts'; import { translationCache } from '@/caches/translationCache.ts';
import { MastodonTranslation } from '@/entities/MastodonTranslation.ts';
import { getEvent } from '@/queries.ts'; import { getEvent } from '@/queries.ts';
import { localeSchema } from '@/schema.ts'; import { localeSchema } from '@/schema.ts';
import { parseBody } from '@/utils/api.ts'; import { parseBody } from '@/utils/api.ts';
@ -17,7 +17,7 @@ const translateSchema = z.object({
}); });
const translateController: AppController = async (c) => { const translateController: AppController = async (c) => {
const { relay, user, signal } = c.var; const { relay, user, signal, requestId } = c.var;
const result = translateSchema.safeParse(await parseBody(c.req.raw)); const result = translateSchema.safeParse(await parseBody(c.req.raw));
@ -143,7 +143,7 @@ const translateController: AppController = async (c) => {
if (e instanceof Error && e.message.includes('not supported')) { if (e instanceof Error && e.message.includes('not supported')) {
return c.json({ error: `Translation of source language '${event.language}' not supported` }, 422); return c.json({ error: `Translation of source language '${event.language}' not supported` }, 422);
} }
logi({ level: 'error', ns: 'ditto.translate', error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.translate', requestId, error: errorJson(e) });
return c.json({ error: 'Service Unavailable' }, 503); return c.json({ error: 'Service Unavailable' }, 503);
} }
}; };

View file

@ -7,10 +7,12 @@ import { z } from 'zod';
import { AppController } from '@/app.ts'; import { AppController } from '@/app.ts';
import { hydrateEvents } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';
import { generateDateRange, Time } from '@/utils/time.ts'; import { generateDateRange, Time } from '@/utils/time.ts';
import { PreviewCard, unfurlCardCached } from '@/utils/unfurl.ts'; import { unfurlCardCached } from '@/utils/unfurl.ts';
import { errorJson } from '@/utils/log.ts'; import { errorJson } from '@/utils/log.ts';
import { renderStatus } from '@/views/mastodon/statuses.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts';
import type { MastodonPreviewCard } from '@ditto/mastoapi/types';
interface TrendHistory { interface TrendHistory {
day: string; day: string;
accounts: string; accounts: string;
@ -23,7 +25,7 @@ interface TrendingHashtag {
history: TrendHistory[]; history: TrendHistory[];
} }
interface TrendingLink extends PreviewCard { interface TrendingLink extends MastodonPreviewCard {
history: TrendHistory[]; history: TrendHistory[];
} }

View file

@ -4,7 +4,10 @@ import { logi } from '@soapbox/logi';
import { errorJson } from '@/utils/log.ts'; import { errorJson } from '@/utils/log.ts';
export const errorHandler: ErrorHandler = (err, c) => { import type { DittoEnv } from '@ditto/mastoapi/router';
export const errorHandler: ErrorHandler<DittoEnv> = (err, c) => {
const { requestId } = c.var;
const { method } = c.req; const { method } = c.req;
const { pathname } = new URL(c.req.url); const { pathname } = new URL(c.req.url);
@ -22,7 +25,15 @@ export const errorHandler: ErrorHandler = (err, c) => {
return c.json({ error: 'The server was unable to respond in a timely manner' }, 500); return c.json({ error: 'The server was unable to respond in a timely manner' }, 500);
} }
logi({ level: 'error', ns: 'ditto.http', msg: 'Unhandled error', method, pathname, error: errorJson(err) }); logi({
level: 'error',
ns: 'ditto.http',
msg: 'Unhandled error',
method,
pathname,
requestId,
error: errorJson(err),
});
return c.json({ error: 'Something went wrong' }, 500); return c.json({ error: 'Something went wrong' }, 500);
}; };

View file

@ -14,6 +14,8 @@ import { renderAccount } from '@/views/mastodon/accounts.ts';
const META_PLACEHOLDER = '<!--server-generated-meta-->' as const; const META_PLACEHOLDER = '<!--server-generated-meta-->' as const;
export const frontendController: AppMiddleware = async (c) => { export const frontendController: AppMiddleware = async (c) => {
const { requestId } = c.var;
c.header('Cache-Control', 'max-age=86400, s-maxage=30, public, stale-if-error=604800'); c.header('Cache-Control', 'max-age=86400, s-maxage=30, public, stale-if-error=604800');
try { try {
@ -26,7 +28,7 @@ export const frontendController: AppMiddleware = async (c) => {
const meta = renderMetadata(c.req.url, entities); const meta = renderMetadata(c.req.url, entities);
return c.html(content.replace(META_PLACEHOLDER, meta)); return c.html(content.replace(META_PLACEHOLDER, meta));
} catch (e) { } catch (e) {
logi({ level: 'error', ns: 'ditto.frontend', msg: 'Error building meta tags', error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.frontend', msg: 'Error building meta tags', requestId, error: errorJson(e) });
return c.html(content); return c.html(content);
} }
} }
@ -40,7 +42,7 @@ async function getEntities(c: AppContext, params: { acct?: string; statusId?: st
const { relay } = c.var; const { relay } = c.var;
const entities: MetadataEntities = { const entities: MetadataEntities = {
instance: await getInstanceMetadata(relay), instance: await getInstanceMetadata(c.var),
}; };
if (params.statusId) { if (params.statusId) {

View file

@ -3,9 +3,7 @@ import { WebManifestCombined } from '@/types/webmanifest.ts';
import { getInstanceMetadata } from '@/utils/instance.ts'; import { getInstanceMetadata } from '@/utils/instance.ts';
export const manifestController: AppController = async (c) => { export const manifestController: AppController = async (c) => {
const { relay, signal } = c.var; const meta = await getInstanceMetadata(c.var);
const meta = await getInstanceMetadata(relay, signal);
const manifest: WebManifestCombined = { const manifest: WebManifestCombined = {
description: meta.about, description: meta.about,

View file

@ -4,9 +4,9 @@ import { AppController } from '@/app.ts';
import { getInstanceMetadata } from '@/utils/instance.ts'; import { getInstanceMetadata } from '@/utils/instance.ts';
const relayInfoController: AppController = async (c) => { const relayInfoController: AppController = async (c) => {
const { conf, relay, signal } = c.var; const { conf } = c.var;
const meta = await getInstanceMetadata(relay, signal); const meta = await getInstanceMetadata(c.var);
c.res.headers.set('access-control-allow-origin', '*'); c.res.headers.set('access-control-allow-origin', '*');

View file

@ -11,6 +11,7 @@ import {
NostrClientMsg, NostrClientMsg,
NostrClientREQ, NostrClientREQ,
NostrRelayMsg, NostrRelayMsg,
NRelay,
NSchema as n, NSchema as n,
} from '@nostrify/nostrify'; } from '@nostrify/nostrify';
@ -40,8 +41,17 @@ const limiters = {
/** Connections for metrics purposes. */ /** Connections for metrics purposes. */
const connections = new Set<WebSocket>(); const connections = new Set<WebSocket>();
interface ConnectStreamOpts {
conf: DittoConf;
relay: NRelay;
requestId: string;
}
/** Set up the Websocket connection. */ /** Set up the Websocket connection. */
function connectStream(conf: DittoConf, relay: DittoPgStore, socket: WebSocket, ip: string | undefined) { function connectStream(socket: WebSocket, ip: string | undefined, opts: ConnectStreamOpts): void {
const { conf, requestId } = opts;
const relay = opts.relay as DittoPgStore;
const controllers = new Map<string, AbortController>(); const controllers = new Map<string, AbortController>();
if (ip) { if (ip) {
@ -74,7 +84,7 @@ function connectStream(conf: DittoConf, relay: DittoPgStore, socket: WebSocket,
const msg = result.data; const msg = result.data;
const verb = msg[0]; const verb = msg[0];
logi({ level: 'trace', ns: 'ditto.relay.msg', verb, msg: msg as JsonValue, ip }); logi({ level: 'trace', ns: 'ditto.relay.msg', verb, msg: msg as JsonValue, ip, requestId });
relayMessagesCounter.inc({ verb }); relayMessagesCounter.inc({ verb });
handleMsg(result.data); handleMsg(result.data);
@ -165,7 +175,7 @@ function connectStream(conf: DittoConf, relay: DittoPgStore, socket: WebSocket,
send(['OK', event.id, false, e.message]); send(['OK', event.id, false, e.message]);
} else { } else {
send(['OK', event.id, false, 'error: something went wrong']); send(['OK', event.id, false, 'error: something went wrong']);
logi({ level: 'error', ns: 'ditto.relay', msg: 'Error in relay', error: errorJson(e), ip }); logi({ level: 'error', ns: 'ditto.relay', msg: 'Error in relay', error: errorJson(e), ip, requestId });
} }
} }
} }
@ -195,7 +205,8 @@ function connectStream(conf: DittoConf, relay: DittoPgStore, socket: WebSocket,
} }
const relayController: AppController = (c, next) => { const relayController: AppController = (c, next) => {
const { conf, relay } = c.var; const { conf } = c.var;
const upgrade = c.req.header('upgrade'); const upgrade = c.req.header('upgrade');
// NIP-11: https://github.com/nostr-protocol/nips/blob/master/11.md // NIP-11: https://github.com/nostr-protocol/nips/blob/master/11.md
@ -214,7 +225,7 @@ const relayController: AppController = (c, next) => {
} }
const { socket, response } = Deno.upgradeWebSocket(c.req.raw); const { socket, response } = Deno.upgradeWebSocket(c.req.raw);
connectStream(conf, relay as DittoPgStore, socket, ip); connectStream(socket, ip, c.var);
return response; return response;
}; };

View file

@ -9,7 +9,7 @@ export const cspMiddleware = (): AppMiddleware => {
const { conf, relay } = c.var; const { conf, relay } = c.var;
if (!configDBCache) { if (!configDBCache) {
configDBCache = getPleromaConfigs(relay); configDBCache = getPleromaConfigs({ conf, relay });
} }
const { host, protocol, origin } = conf.url; const { host, protocol, origin } = conf.url;

View file

@ -1,11 +1,15 @@
import { MiddlewareHandler } from '@hono/hono';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
export const logiMiddleware: MiddlewareHandler = async (c, next) => { import type { DittoMiddleware } from '@ditto/mastoapi/router';
export const logiMiddleware: DittoMiddleware = async (c, next) => {
const { requestId } = c.var;
const { method } = c.req; const { method } = c.req;
const { pathname } = new URL(c.req.url); const { pathname } = new URL(c.req.url);
logi({ level: 'info', ns: 'ditto.http.request', method, pathname }); const ip = c.req.header('x-real-ip');
logi({ level: 'info', ns: 'ditto.http.request', method, pathname, ip, requestId });
const start = new Date(); const start = new Date();
@ -15,5 +19,5 @@ export const logiMiddleware: MiddlewareHandler = async (c, next) => {
const duration = (end.getTime() - start.getTime()) / 1000; const duration = (end.getTime() - start.getTime()) / 1000;
const level = c.res.status >= 500 ? 'error' : 'info'; const level = c.res.status >= 500 ? 'error' : 'info';
logi({ level, ns: 'ditto.http.response', method, pathname, status: c.res.status, duration }); logi({ level, ns: 'ditto.http.response', method, pathname, status: c.res.status, duration, ip, requestId });
}; };

View file

@ -1,15 +1,14 @@
import * as Sentry from '@sentry/deno'; import * as Sentry from '@sentry/deno';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { Conf } from '@/config.ts'; import type { DittoConf } from '@ditto/conf';
// Sentry /** Start Sentry, if configured. */
if (Conf.sentryDsn) { export function startSentry(conf: DittoConf): void {
if (conf.sentryDsn) {
logi({ level: 'info', ns: 'ditto.sentry', msg: 'Sentry enabled.', enabled: true }); logi({ level: 'info', ns: 'ditto.sentry', msg: 'Sentry enabled.', enabled: true });
Sentry.init({ Sentry.init({ dsn: conf.sentryDsn });
dsn: Conf.sentryDsn,
tracesSampleRate: 1.0,
});
} else { } else {
logi({ level: 'info', ns: 'ditto.sentry', msg: 'Sentry not configured. Skipping.', enabled: false }); logi({ level: 'info', ns: 'ditto.sentry', msg: 'Sentry not configured. Skipping.', enabled: false });
} }
}

View file

@ -1,12 +1,12 @@
import { DittoConf } from '@ditto/conf';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import '@/sentry.ts';
import '@/nostr-wasm.ts';
import app from '@/app.ts'; import app from '@/app.ts';
import { Conf } from '@/config.ts';
const conf = new DittoConf(Deno.env);
Deno.serve({ Deno.serve({
port: Conf.port, port: conf.port,
onListen({ hostname, port }): void { onListen({ hostname, port }): void {
logi({ level: 'info', ns: 'ditto.server', msg: `Listening on http://${hostname}:${port}`, hostname, port }); logi({ level: 'info', ns: 'ditto.server', msg: `Listening on http://${hostname}:${port}`, hostname, port });
}, },

View file

@ -5,7 +5,6 @@ import { generateSecretKey } from 'nostr-tools';
import { RelayError } from '@/RelayError.ts'; import { RelayError } from '@/RelayError.ts';
import { eventFixture } from '@/test.ts'; import { eventFixture } from '@/test.ts';
import { Conf } from '@/config.ts';
import { DittoPgStore } from '@/storages/DittoPgStore.ts'; import { DittoPgStore } from '@/storages/DittoPgStore.ts';
import { createTestDB } from '@/test.ts'; import { createTestDB } from '@/test.ts';
@ -152,7 +151,7 @@ Deno.test("user cannot delete another user's event", async () => {
Deno.test('admin can delete any event', async () => { Deno.test('admin can delete any event', async () => {
await using db = await createTestDB({ pure: true }); await using db = await createTestDB({ pure: true });
const { store } = db; const { conf, store } = db;
const sk = generateSecretKey(); const sk = generateSecretKey();
@ -168,7 +167,7 @@ Deno.test('admin can delete any event', async () => {
assertEquals(await store.query([{ kinds: [1] }]), [two, one]); assertEquals(await store.query([{ kinds: [1] }]), [two, one]);
await store.event( await store.event(
genEvent({ kind: 5, tags: [['e', one.id]] }, Conf.seckey), // admin sk genEvent({ kind: 5, tags: [['e', one.id]] }, conf.seckey), // admin sk
); );
assertEquals(await store.query([{ kinds: [1] }]), [two]); assertEquals(await store.query([{ kinds: [1] }]), [two]);
@ -176,12 +175,12 @@ Deno.test('admin can delete any event', async () => {
Deno.test('throws a RelayError when inserting an event deleted by the admin', async () => { Deno.test('throws a RelayError when inserting an event deleted by the admin', async () => {
await using db = await createTestDB({ pure: true }); await using db = await createTestDB({ pure: true });
const { store } = db; const { conf, store } = db;
const event = genEvent(); const event = genEvent();
await store.event(event); await store.event(event);
const deletion = genEvent({ kind: 5, tags: [['e', event.id]] }, Conf.seckey); const deletion = genEvent({ kind: 5, tags: [['e', event.id]] }, conf.seckey);
await store.event(deletion); await store.event(deletion);
await assertRejects( await assertRejects(

View file

@ -1,5 +1,6 @@
// deno-lint-ignore-file require-await // deno-lint-ignore-file require-await
import { type DittoConf } from '@ditto/conf';
import { type DittoDB, type DittoTables } from '@ditto/db'; import { type DittoDB, type DittoTables } from '@ditto/db';
import { detectLanguage } from '@ditto/lang'; import { detectLanguage } from '@ditto/lang';
import { NPostgres, NPostgresSchema } from '@nostrify/db'; import { NPostgres, NPostgresSchema } from '@nostrify/db';
@ -52,8 +53,8 @@ interface TagConditionOpts {
interface DittoPgStoreOpts { interface DittoPgStoreOpts {
/** Kysely instance to use. */ /** Kysely instance to use. */
db: DittoDB; db: DittoDB;
/** Pubkey of the admin account. */ /** Ditto configuration. */
pubkey: string; conf: DittoConf;
/** Timeout in milliseconds for database queries. */ /** Timeout in milliseconds for database queries. */
timeout?: number; timeout?: number;
/** Whether the event returned should be a Nostr event or a Ditto event. Defaults to false. */ /** Whether the event returned should be a Nostr event or a Ditto event. Defaults to false. */
@ -169,9 +170,10 @@ export class DittoPgStore extends NPostgres {
event: NostrEvent, event: NostrEvent,
opts: { signal?: AbortSignal; timeout?: number } = {}, opts: { signal?: AbortSignal; timeout?: number } = {},
): Promise<undefined> { ): Promise<undefined> {
const { conf } = this.opts;
try { try {
await super.transaction(async (relay, kysely) => { await super.transaction(async (relay, kysely) => {
await updateStats({ event, relay, kysely: kysely as unknown as Kysely<DittoTables> }); await updateStats({ conf, relay, kysely: kysely as unknown as Kysely<DittoTables>, event });
await relay.event(event, opts); await relay.event(event, opts);
}); });
} catch (e) { } catch (e) {
@ -229,8 +231,11 @@ export class DittoPgStore extends NPostgres {
/** Check if an event has been deleted by the admin. */ /** Check if an event has been deleted by the admin. */
private async isDeletedAdmin(event: NostrEvent): Promise<boolean> { private async isDeletedAdmin(event: NostrEvent): Promise<boolean> {
const { conf } = this.opts;
const adminPubkey = await conf.signer.getPublicKey();
const filters: NostrFilter[] = [ const filters: NostrFilter[] = [
{ kinds: [5], authors: [this.opts.pubkey], '#e': [event.id], limit: 1 }, { kinds: [5], authors: [adminPubkey], '#e': [event.id], limit: 1 },
]; ];
if (NKinds.replaceable(event.kind) || NKinds.parameterizedReplaceable(event.kind)) { if (NKinds.replaceable(event.kind) || NKinds.parameterizedReplaceable(event.kind)) {
@ -238,7 +243,7 @@ export class DittoPgStore extends NPostgres {
filters.push({ filters.push({
kinds: [5], kinds: [5],
authors: [this.opts.pubkey], authors: [adminPubkey],
'#a': [`${event.kind}:${event.pubkey}:${d}`], '#a': [`${event.kind}:${event.pubkey}:${d}`],
since: event.created_at, since: event.created_at,
limit: 1, limit: 1,
@ -251,7 +256,10 @@ export class DittoPgStore extends NPostgres {
/** The DITTO_NSEC can delete any event from the database. NDatabase already handles user deletions. */ /** The DITTO_NSEC can delete any event from the database. NDatabase already handles user deletions. */
private async deleteEventsAdmin(event: NostrEvent): Promise<void> { private async deleteEventsAdmin(event: NostrEvent): Promise<void> {
if (event.kind === 5 && event.pubkey === this.opts.pubkey) { const { conf } = this.opts;
const adminPubkey = await conf.signer.getPublicKey();
if (event.kind === 5 && event.pubkey === adminPubkey) {
const ids = new Set(event.tags.filter(([name]) => name === 'e').map(([_name, value]) => value)); const ids = new Set(event.tags.filter(([name]) => name === 'e').map(([_name, value]) => value));
const addrs = new Set(event.tags.filter(([name]) => name === 'a').map(([_name, value]) => value)); const addrs = new Set(event.tags.filter(([name]) => name === 'a').map(([_name, value]) => value));

View file

@ -324,7 +324,8 @@ export class DittoRelayStore implements NRelay {
} }
private async prewarmLinkPreview(event: NostrEvent, signal?: AbortSignal): Promise<void> { private async prewarmLinkPreview(event: NostrEvent, signal?: AbortSignal): Promise<void> {
const { firstUrl } = parseNoteContent(stripimeta(event.content, event.tags), []); const { firstUrl } = parseNoteContent(stripimeta(event.content, event.tags), [], this.opts);
if (firstUrl) { if (firstUrl) {
await unfurlCardCached(firstUrl, signal); await unfurlCardCached(firstUrl, signal);
} }

View file

@ -1,7 +1,7 @@
import { DittoConf } from '@ditto/conf';
import { DittoPolyPg } from '@ditto/db'; import { DittoPolyPg } from '@ditto/db';
import { NostrEvent } from '@nostrify/nostrify'; import { NostrEvent } from '@nostrify/nostrify';
import { Conf } from '@/config.ts';
import { DittoPgStore } from '@/storages/DittoPgStore.ts'; import { DittoPgStore } from '@/storages/DittoPgStore.ts';
import { sql } from 'kysely'; import { sql } from 'kysely';
@ -13,13 +13,14 @@ export async function eventFixture(name: string): Promise<NostrEvent> {
/** Create a database for testing. It uses `DATABASE_URL`, or creates an in-memory database by default. */ /** Create a database for testing. It uses `DATABASE_URL`, or creates an in-memory database by default. */
export async function createTestDB(opts?: { pure?: boolean }) { export async function createTestDB(opts?: { pure?: boolean }) {
const db = new DittoPolyPg(Conf.databaseUrl, { poolSize: 1 }); const conf = new DittoConf(Deno.env);
const db = new DittoPolyPg(conf.databaseUrl, { poolSize: 1 });
await db.migrate(); await db.migrate();
const store = new DittoPgStore({ const store = new DittoPgStore({
db, db,
timeout: Conf.db.timeouts.default, conf,
pubkey: await Conf.signer.getPublicKey(), timeout: conf.db.timeouts.default,
pure: opts?.pure ?? false, pure: opts?.pure ?? false,
notify: true, notify: true,
}); });
@ -28,6 +29,7 @@ export async function createTestDB(opts?: { pure?: boolean }) {
db, db,
...db, ...db,
store, store,
conf,
kysely: db.kysely, kysely: db.kysely,
[Symbol.asyncDispose]: async () => { [Symbol.asyncDispose]: async () => {
const { rows } = await sql< const { rows } = await sql<

View file

@ -1,9 +1,10 @@
import { NostrEvent, NostrMetadata, NSchema as n, NStore } from '@nostrify/nostrify'; import { NostrEvent, NostrMetadata, NSchema as n, NStore } from '@nostrify/nostrify';
import { z } from 'zod'; import { z } from 'zod';
import { Conf } from '@/config.ts';
import { screenshotsSchema, serverMetaSchema } from '@/schemas/nostr.ts'; import { screenshotsSchema, serverMetaSchema } from '@/schemas/nostr.ts';
import type { DittoConf } from '@ditto/conf';
/** Like NostrMetadata, but some fields are required and also contains some extra fields. */ /** Like NostrMetadata, but some fields are required and also contains some extra fields. */
export interface InstanceMetadata extends NostrMetadata { export interface InstanceMetadata extends NostrMetadata {
about: string; about: string;
@ -15,10 +16,18 @@ export interface InstanceMetadata extends NostrMetadata {
screenshots: z.infer<typeof screenshotsSchema>; screenshots: z.infer<typeof screenshotsSchema>;
} }
interface GetInstanceMetadataOpts {
conf: DittoConf;
relay: NStore;
signal?: AbortSignal;
}
/** Get and parse instance metadata from the kind 0 of the admin user. */ /** Get and parse instance metadata from the kind 0 of the admin user. */
export async function getInstanceMetadata(store: NStore, signal?: AbortSignal): Promise<InstanceMetadata> { export async function getInstanceMetadata(opts: GetInstanceMetadataOpts): Promise<InstanceMetadata> {
const [event] = await store.query( const { conf, relay, signal } = opts;
[{ kinds: [0], authors: [await Conf.signer.getPublicKey()], limit: 1 }],
const [event] = await relay.query(
[{ kinds: [0], authors: [await conf.signer.getPublicKey()], limit: 1 }],
{ signal }, { signal },
); );
@ -33,8 +42,8 @@ export async function getInstanceMetadata(store: NStore, signal?: AbortSignal):
name: meta.name ?? 'Ditto', name: meta.name ?? 'Ditto',
about: meta.about ?? 'Nostr community server', about: meta.about ?? 'Nostr community server',
tagline: meta.tagline ?? meta.about ?? 'Nostr community server', tagline: meta.tagline ?? meta.about ?? 'Nostr community server',
email: meta.email ?? `postmaster@${Conf.url.host}`, email: meta.email ?? `postmaster@${conf.url.host}`,
picture: meta.picture ?? Conf.local('/images/thumbnail.png'), picture: meta.picture ?? conf.local('/images/thumbnail.png'),
event, event,
screenshots: meta.screenshots ?? [], screenshots: meta.screenshots ?? [],
}; };

View file

@ -1,26 +1,35 @@
import { DittoConf } from '@ditto/conf';
import { assertEquals } from '@std/assert'; import { assertEquals } from '@std/assert';
import { eventFixture } from '@/test.ts'; import { eventFixture } from '@/test.ts';
import { getMediaLinks, parseNoteContent, stripimeta } from '@/utils/note.ts'; import { getMediaLinks, parseNoteContent, stripimeta } from '@/utils/note.ts';
Deno.test('parseNoteContent', () => { Deno.test('parseNoteContent', () => {
const { html, links, firstUrl } = parseNoteContent('Hello, world!', []); const conf = new DittoConf(new Map());
const { html, links, firstUrl } = parseNoteContent('Hello, world!', [], { conf });
assertEquals(html, 'Hello, world!'); assertEquals(html, 'Hello, world!');
assertEquals(links, []); assertEquals(links, []);
assertEquals(firstUrl, undefined); assertEquals(firstUrl, undefined);
}); });
Deno.test('parseNoteContent parses URLs', () => { Deno.test('parseNoteContent parses URLs', () => {
const { html } = parseNoteContent('check out my website: https://alexgleason.me', []); const conf = new DittoConf(new Map());
const { html } = parseNoteContent('check out my website: https://alexgleason.me', [], { conf });
assertEquals(html, 'check out my website: <a href="https://alexgleason.me">https://alexgleason.me</a>'); assertEquals(html, 'check out my website: <a href="https://alexgleason.me">https://alexgleason.me</a>');
}); });
Deno.test('parseNoteContent parses bare URLs', () => { Deno.test('parseNoteContent parses bare URLs', () => {
const { html } = parseNoteContent('have you seen ditto.pub?', []); const conf = new DittoConf(new Map());
const { html } = parseNoteContent('have you seen ditto.pub?', [], { conf });
assertEquals(html, 'have you seen <a href="http://ditto.pub">ditto.pub</a>?'); assertEquals(html, 'have you seen <a href="http://ditto.pub">ditto.pub</a>?');
}); });
Deno.test('parseNoteContent parses mentions with apostrophes', () => { Deno.test('parseNoteContent parses mentions with apostrophes', () => {
const conf = new DittoConf(new Map());
const { html } = parseNoteContent( const { html } = parseNoteContent(
`did you see nostr:nprofile1qqsqgc0uhmxycvm5gwvn944c7yfxnnxm0nyh8tt62zhrvtd3xkj8fhgprdmhxue69uhkwmr9v9ek7mnpw3hhytnyv4mz7un9d3shjqgcwaehxw309ahx7umywf5hvefwv9c8qtmjv4kxz7gpzemhxue69uhhyetvv9ujumt0wd68ytnsw43z7s3al0v's speech?`, `did you see nostr:nprofile1qqsqgc0uhmxycvm5gwvn944c7yfxnnxm0nyh8tt62zhrvtd3xkj8fhgprdmhxue69uhkwmr9v9ek7mnpw3hhytnyv4mz7un9d3shjqgcwaehxw309ahx7umywf5hvefwv9c8qtmjv4kxz7gpzemhxue69uhhyetvv9ujumt0wd68ytnsw43z7s3al0v's speech?`,
[{ [{
@ -29,7 +38,9 @@ Deno.test('parseNoteContent parses mentions with apostrophes', () => {
acct: 'alex@gleasonator.dev', acct: 'alex@gleasonator.dev',
url: 'https://gleasonator.dev/@alex', url: 'https://gleasonator.dev/@alex',
}], }],
{ conf },
); );
assertEquals( assertEquals(
html, html,
'did you see <span class="h-card"><a class="u-url mention" href="https://gleasonator.dev/@alex" rel="ugc">@<span>alex@gleasonator.dev</span></a></span>&apos;s speech?', 'did you see <span class="h-card"><a class="u-url mention" href="https://gleasonator.dev/@alex" rel="ugc">@<span>alex@gleasonator.dev</span></a></span>&apos;s speech?',
@ -37,6 +48,8 @@ Deno.test('parseNoteContent parses mentions with apostrophes', () => {
}); });
Deno.test('parseNoteContent parses mentions with commas', () => { Deno.test('parseNoteContent parses mentions with commas', () => {
const conf = new DittoConf(new Map());
const { html } = parseNoteContent( const { html } = parseNoteContent(
`Sim. Hi nostr:npub1q3sle0kvfsehgsuexttt3ugjd8xdklxfwwkh559wxckmzddywnws6cd26p and nostr:npub1gujeqakgt7fyp6zjggxhyy7ft623qtcaay5lkc8n8gkry4cvnrzqd3f67z, any chance to have Cobrafuma as PWA?`, `Sim. Hi nostr:npub1q3sle0kvfsehgsuexttt3ugjd8xdklxfwwkh559wxckmzddywnws6cd26p and nostr:npub1gujeqakgt7fyp6zjggxhyy7ft623qtcaay5lkc8n8gkry4cvnrzqd3f67z, any chance to have Cobrafuma as PWA?`,
[{ [{
@ -50,7 +63,9 @@ Deno.test('parseNoteContent parses mentions with commas', () => {
acct: 'patrick@patrickdosreis.com', acct: 'patrick@patrickdosreis.com',
url: 'https://gleasonator.dev/@patrick@patrickdosreis.com', url: 'https://gleasonator.dev/@patrick@patrickdosreis.com',
}], }],
{ conf },
); );
assertEquals( assertEquals(
html, html,
'Sim. Hi <span class="h-card"><a class="u-url mention" href="https://gleasonator.dev/@alex" rel="ugc">@<span>alex@gleasonator.dev</span></a></span> and <span class="h-card"><a class="u-url mention" href="https://gleasonator.dev/@patrick@patrickdosreis.com" rel="ugc">@<span>patrick@patrickdosreis.com</span></a></span>, any chance to have Cobrafuma as PWA?', 'Sim. Hi <span class="h-card"><a class="u-url mention" href="https://gleasonator.dev/@alex" rel="ugc">@<span>alex@gleasonator.dev</span></a></span> and <span class="h-card"><a class="u-url mention" href="https://gleasonator.dev/@patrick@patrickdosreis.com" rel="ugc">@<span>patrick@patrickdosreis.com</span></a></span>, any chance to have Cobrafuma as PWA?',
@ -58,19 +73,26 @@ Deno.test('parseNoteContent parses mentions with commas', () => {
}); });
Deno.test("parseNoteContent doesn't parse invalid nostr URIs", () => { Deno.test("parseNoteContent doesn't parse invalid nostr URIs", () => {
const { html } = parseNoteContent('nip19 has URIs like nostr:npub and nostr:nevent, etc.', []); const conf = new DittoConf(new Map());
const { html } = parseNoteContent('nip19 has URIs like nostr:npub and nostr:nevent, etc.', [], { conf });
assertEquals(html, 'nip19 has URIs like nostr:npub and nostr:nevent, etc.'); assertEquals(html, 'nip19 has URIs like nostr:npub and nostr:nevent, etc.');
}); });
Deno.test('parseNoteContent renders empty for non-profile nostr URIs', () => { Deno.test('parseNoteContent renders empty for non-profile nostr URIs', () => {
const conf = new DittoConf(new Map());
const { html } = parseNoteContent( const { html } = parseNoteContent(
'nostr:nevent1qgsr9cvzwc652r4m83d86ykplrnm9dg5gwdvzzn8ameanlvut35wy3gpz3mhxue69uhhztnnwashymtnw3ezucm0d5qzqru8mkz2q4gzsxg99q7pdneyx7n8p5u0afe3ntapj4sryxxmg4gpcdvgce', 'nostr:nevent1qgsr9cvzwc652r4m83d86ykplrnm9dg5gwdvzzn8ameanlvut35wy3gpz3mhxue69uhhztnnwashymtnw3ezucm0d5qzqru8mkz2q4gzsxg99q7pdneyx7n8p5u0afe3ntapj4sryxxmg4gpcdvgce',
[], [],
{ conf },
); );
assertEquals(html, ''); assertEquals(html, '');
}); });
Deno.test("parseNoteContent doesn't fuck up links to my own post", () => { Deno.test("parseNoteContent doesn't fuck up links to my own post", () => {
const conf = new DittoConf(new Map());
const { html } = parseNoteContent( const { html } = parseNoteContent(
'Check this post: https://gleasonator.dev/@alex@gleasonator.dev/posts/a8badb480d88f9e7b6a090342279ef47ed0e0a3989ed85f898dfedc6be94225f', 'Check this post: https://gleasonator.dev/@alex@gleasonator.dev/posts/a8badb480d88f9e7b6a090342279ef47ed0e0a3989ed85f898dfedc6be94225f',
[{ [{
@ -79,7 +101,9 @@ Deno.test("parseNoteContent doesn't fuck up links to my own post", () => {
acct: 'alex@gleasonator.dev', acct: 'alex@gleasonator.dev',
url: 'https://gleasonator.dev/@alex', url: 'https://gleasonator.dev/@alex',
}], }],
{ conf },
); );
assertEquals( assertEquals(
html, html,
'Check this post: <a href="https://gleasonator.dev/@alex@gleasonator.dev/posts/a8badb480d88f9e7b6a090342279ef47ed0e0a3989ed85f898dfedc6be94225f">https://gleasonator.dev/@alex@gleasonator.dev/posts/a8badb480d88f9e7b6a090342279ef47ed0e0a3989ed85f898dfedc6be94225f</a>', 'Check this post: <a href="https://gleasonator.dev/@alex@gleasonator.dev/posts/a8badb480d88f9e7b6a090342279ef47ed0e0a3989ed85f898dfedc6be94225f">https://gleasonator.dev/@alex@gleasonator.dev/posts/a8badb480d88f9e7b6a090342279ef47ed0e0a3989ed85f898dfedc6be94225f</a>',

View file

@ -3,11 +3,12 @@ import linkifyStr from 'linkify-string';
import linkify from 'linkifyjs'; import linkify from 'linkifyjs';
import { nip19, nip27 } from 'nostr-tools'; import { nip19, nip27 } from 'nostr-tools';
import { Conf } from '@/config.ts';
import { MastodonMention } from '@/entities/MastodonMention.ts';
import { html } from '@/utils/html.ts'; import { html } from '@/utils/html.ts';
import { getUrlMediaType, isPermittedMediaType } from '@/utils/media.ts'; import { getUrlMediaType, isPermittedMediaType } from '@/utils/media.ts';
import type { DittoConf } from '@ditto/conf';
import type { MastodonMention } from '@ditto/mastoapi/types';
linkify.registerCustomProtocol('nostr', true); linkify.registerCustomProtocol('nostr', true);
linkify.registerCustomProtocol('wss'); linkify.registerCustomProtocol('wss');
@ -20,8 +21,14 @@ interface ParsedNoteContent {
firstUrl: string | undefined; firstUrl: string | undefined;
} }
interface ParseNoteContentOpts {
conf: DittoConf;
}
/** Convert Nostr content to Mastodon API HTML. Also return parsed data. */ /** Convert Nostr content to Mastodon API HTML. Also return parsed data. */
function parseNoteContent(content: string, mentions: MastodonMention[]): ParsedNoteContent { function parseNoteContent(content: string, mentions: MastodonMention[], opts: ParseNoteContentOpts): ParsedNoteContent {
const { conf } = opts;
const links = linkify.find(content).filter(({ type }) => type === 'url'); const links = linkify.find(content).filter(({ type }) => type === 'url');
const firstUrl = links.find(isNonMediaLink)?.href; const firstUrl = links.find(isNonMediaLink)?.href;
@ -29,7 +36,7 @@ function parseNoteContent(content: string, mentions: MastodonMention[]): ParsedN
render: { render: {
hashtag: ({ content }) => { hashtag: ({ content }) => {
const tag = content.replace(/^#/, ''); const tag = content.replace(/^#/, '');
const href = Conf.local(`/tags/${tag}`); const href = conf.local(`/tags/${tag}`);
return html`<a class="mention hashtag" href="${href}" rel="tag"><span>#</span>${tag}</a>`; return html`<a class="mention hashtag" href="${href}" rel="tag"><span>#</span>${tag}</a>`;
}, },
url: ({ attributes, content }) => { url: ({ attributes, content }) => {
@ -48,7 +55,7 @@ function parseNoteContent(content: string, mentions: MastodonMention[]): ParsedN
const npub = nip19.npubEncode(pubkey); const npub = nip19.npubEncode(pubkey);
const acct = mention?.acct ?? npub; const acct = mention?.acct ?? npub;
const name = mention?.acct ?? npub.substring(0, 8); const name = mention?.acct ?? npub.substring(0, 8);
const href = mention?.url ?? Conf.local(`/@${acct}`); const href = mention?.url ?? conf.local(`/@${acct}`);
return html`<span class="h-card"><a class="u-url mention" href="${href}" rel="ugc">@<span>${name}</span></a></span>${extra}`; return html`<span class="h-card"><a class="u-url mention" href="${href}" rel="ugc">@<span>${name}</span></a></span>${extra}`;
} else { } else {
return ''; return '';

View file

@ -1,10 +1,10 @@
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { match } from 'path-to-regexp'; import { match } from 'path-to-regexp';
import { MastodonAccount } from '@/entities/MastodonAccount.ts';
import { MastodonStatus } from '@/entities/MastodonStatus.ts';
import { InstanceMetadata } from '@/utils/instance.ts'; import { InstanceMetadata } from '@/utils/instance.ts';
import type { MastodonAccount, MastodonStatus } from '@ditto/mastoapi/types';
export interface MetadataEntities { export interface MetadataEntities {
status?: MastodonStatus; status?: MastodonStatus;
account?: MastodonAccount; account?: MastodonAccount;

View file

@ -1,14 +1,23 @@
import { NSchema as n, NStore } from '@nostrify/nostrify'; import { NSchema as n, NStore } from '@nostrify/nostrify';
import { Conf } from '@/config.ts';
import { configSchema } from '@/schemas/pleroma-api.ts'; import { configSchema } from '@/schemas/pleroma-api.ts';
import { PleromaConfigDB } from '@/utils/PleromaConfigDB.ts'; import { PleromaConfigDB } from '@/utils/PleromaConfigDB.ts';
export async function getPleromaConfigs(store: NStore, signal?: AbortSignal): Promise<PleromaConfigDB> { import type { DittoConf } from '@ditto/conf';
const signer = Conf.signer;
interface GetPleromaConfigsOpts {
conf: DittoConf;
relay: NStore;
signal?: AbortSignal;
}
export async function getPleromaConfigs(opts: GetPleromaConfigsOpts): Promise<PleromaConfigDB> {
const { conf, relay, signal } = opts;
const signer = conf.signer;
const pubkey = await signer.getPublicKey(); const pubkey = await signer.getPublicKey();
const [event] = await store.query([{ const [event] = await relay.query([{
kinds: [30078], kinds: [30078],
authors: [pubkey], authors: [pubkey],
'#d': ['pub.ditto.pleroma.config'], '#d': ['pub.ditto.pleroma.config'],

View file

@ -23,7 +23,7 @@ Deno.test('updateStats with kind 1 increments notes count', async () => {
Deno.test('updateStats with kind 1 increments replies count', async () => { Deno.test('updateStats with kind 1 increments replies count', async () => {
await using test = await setupTest(); await using test = await setupTest();
const { relay, kysely } = test; const { kysely, relay } = test;
const sk = generateSecretKey(); const sk = generateSecretKey();
@ -42,7 +42,7 @@ Deno.test('updateStats with kind 1 increments replies count', async () => {
Deno.test('updateStats with kind 5 decrements notes count', async () => { Deno.test('updateStats with kind 5 decrements notes count', async () => {
await using test = await setupTest(); await using test = await setupTest();
const { relay, kysely } = test; const { kysely, relay } = test;
const sk = generateSecretKey(); const sk = generateSecretKey();
const pubkey = getPublicKey(sk); const pubkey = getPublicKey(sk);
@ -74,7 +74,7 @@ Deno.test('updateStats with kind 3 increments followers count', async () => {
Deno.test('updateStats with kind 3 decrements followers count', async () => { Deno.test('updateStats with kind 3 decrements followers count', async () => {
await using test = await setupTest(); await using test = await setupTest();
const { relay, kysely } = test; const { kysely, relay } = test;
const sk = generateSecretKey(); const sk = generateSecretKey();
const follow = genEvent({ kind: 3, tags: [['p', 'alex']], created_at: 0 }, sk); const follow = genEvent({ kind: 3, tags: [['p', 'alex']], created_at: 0 }, sk);
@ -101,7 +101,7 @@ Deno.test('getFollowDiff returns added and removed followers', () => {
Deno.test('updateStats with kind 6 increments reposts count', async () => { Deno.test('updateStats with kind 6 increments reposts count', async () => {
await using test = await setupTest(); await using test = await setupTest();
const { relay, kysely } = test; const { kysely, relay } = test;
const note = genEvent({ kind: 1 }); const note = genEvent({ kind: 1 });
await updateStats({ ...test, event: note }); await updateStats({ ...test, event: note });
@ -118,7 +118,7 @@ Deno.test('updateStats with kind 6 increments reposts count', async () => {
Deno.test('updateStats with kind 5 decrements reposts count', async () => { Deno.test('updateStats with kind 5 decrements reposts count', async () => {
await using test = await setupTest(); await using test = await setupTest();
const { relay, kysely } = test; const { kysely, relay } = test;
const note = genEvent({ kind: 1 }); const note = genEvent({ kind: 1 });
await updateStats({ ...test, event: note }); await updateStats({ ...test, event: note });
@ -138,7 +138,7 @@ Deno.test('updateStats with kind 5 decrements reposts count', async () => {
Deno.test('updateStats with kind 7 increments reactions count', async () => { Deno.test('updateStats with kind 7 increments reactions count', async () => {
await using test = await setupTest(); await using test = await setupTest();
const { relay, kysely } = test; const { kysely, relay } = test;
const note = genEvent({ kind: 1 }); const note = genEvent({ kind: 1 });
await updateStats({ ...test, event: note }); await updateStats({ ...test, event: note });
@ -155,7 +155,7 @@ Deno.test('updateStats with kind 7 increments reactions count', async () => {
Deno.test('updateStats with kind 5 decrements reactions count', async () => { Deno.test('updateStats with kind 5 decrements reactions count', async () => {
await using test = await setupTest(); await using test = await setupTest();
const { relay, kysely } = test; const { kysely, relay } = test;
const note = genEvent({ kind: 1 }); const note = genEvent({ kind: 1 });
await updateStats({ ...test, event: note }); await updateStats({ ...test, event: note });
@ -175,7 +175,7 @@ Deno.test('updateStats with kind 5 decrements reactions count', async () => {
Deno.test('countAuthorStats counts author stats from the database', async () => { Deno.test('countAuthorStats counts author stats from the database', async () => {
await using test = await setupTest(); await using test = await setupTest();
const { relay } = test; const { kysely, relay } = test;
const sk = generateSecretKey(); const sk = generateSecretKey();
const pubkey = getPublicKey(sk); const pubkey = getPublicKey(sk);
@ -184,7 +184,7 @@ Deno.test('countAuthorStats counts author stats from the database', async () =>
await relay.event(genEvent({ kind: 1, content: 'yolo' }, sk)); await relay.event(genEvent({ kind: 1, content: 'yolo' }, sk));
await relay.event(genEvent({ kind: 3, tags: [['p', pubkey]] })); await relay.event(genEvent({ kind: 3, tags: [['p', pubkey]] }));
await test.kysely.insertInto('author_stats').values({ await kysely.insertInto('author_stats').values({
pubkey, pubkey,
search: 'Yolo Lolo', search: 'Yolo Lolo',
notes_count: 0, notes_count: 0,
@ -193,7 +193,7 @@ Deno.test('countAuthorStats counts author stats from the database', async () =>
}).onConflict((oc) => oc.column('pubkey').doUpdateSet({ 'search': 'baka' })) }).onConflict((oc) => oc.column('pubkey').doUpdateSet({ 'search': 'baka' }))
.execute(); .execute();
const stats = await countAuthorStats({ ...test, pubkey }); const stats = await countAuthorStats({ ...test, kysely, pubkey });
assertEquals(stats!.notes_count, 2); assertEquals(stats!.notes_count, 2);
assertEquals(stats!.followers_count, 1); assertEquals(stats!.followers_count, 1);
@ -206,9 +206,10 @@ async function setupTest() {
await db.migrate(); await db.migrate();
const { kysely } = db; const { kysely } = db;
const relay = new NPostgres(kysely); const relay = new NPostgres(db.kysely);
return { return {
conf,
relay, relay,
kysely, kysely,
[Symbol.asyncDispose]: async () => { [Symbol.asyncDispose]: async () => {

View file

@ -4,40 +4,46 @@ import { Insertable, Kysely, UpdateObject } from 'kysely';
import { SetRequired } from 'type-fest'; import { SetRequired } from 'type-fest';
import { z } from 'zod'; import { z } from 'zod';
import { Conf } from '@/config.ts';
import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts'; import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts';
import type { DittoConf } from '@ditto/conf';
interface UpdateStatsOpts { interface UpdateStatsOpts {
kysely: Kysely<DittoTables>; conf: DittoConf;
relay: NStore; relay: NStore;
kysely: Kysely<DittoTables>;
event: NostrEvent; event: NostrEvent;
x?: 1 | -1; x?: 1 | -1;
} }
/** Handle one event at a time and update relevant stats for it. */ /** Handle one event at a time and update relevant stats for it. */
// deno-lint-ignore require-await // deno-lint-ignore require-await
export async function updateStats({ event, kysely, relay, x = 1 }: UpdateStatsOpts): Promise<void> { export async function updateStats(opts: UpdateStatsOpts): Promise<void> {
const { event } = opts;
switch (event.kind) { switch (event.kind) {
case 1: case 1:
case 20: case 20:
case 1111: case 1111:
case 30023: case 30023:
return handleEvent1(kysely, event, x); return handleEvent1(opts);
case 3: case 3:
return handleEvent3(kysely, event, x, relay); return handleEvent3(opts);
case 5: case 5:
return handleEvent5(kysely, event, -1, relay); return handleEvent5(opts);
case 6: case 6:
return handleEvent6(kysely, event, x); return handleEvent6(opts);
case 7: case 7:
return handleEvent7(kysely, event, x); return handleEvent7(opts);
case 9735: case 9735:
return handleEvent9735(kysely, event); return handleEvent9735(opts);
} }
} }
/** Update stats for kind 1 event. */ /** Update stats for kind 1 event. */
async function handleEvent1(kysely: Kysely<DittoTables>, event: NostrEvent, x: number): Promise<void> { async function handleEvent1(opts: UpdateStatsOpts): Promise<void> {
const { conf, kysely, event, x = 1 } = opts;
await updateAuthorStats(kysely, event.pubkey, (prev) => { await updateAuthorStats(kysely, event.pubkey, (prev) => {
const now = event.created_at; const now = event.created_at;
@ -47,7 +53,7 @@ async function handleEvent1(kysely: Kysely<DittoTables>, event: NostrEvent, x: n
if (start && end) { // Streak exists. if (start && end) { // Streak exists.
if (now <= end) { if (now <= end) {
// Streak cannot go backwards in time. Skip it. // Streak cannot go backwards in time. Skip it.
} else if (now - end > Conf.streakWindow) { } else if (now - end > conf.streakWindow) {
// Streak is broken. Start a new streak. // Streak is broken. Start a new streak.
start = now; start = now;
end = now; end = now;
@ -88,7 +94,9 @@ async function handleEvent1(kysely: Kysely<DittoTables>, event: NostrEvent, x: n
} }
/** Update stats for kind 3 event. */ /** Update stats for kind 3 event. */
async function handleEvent3(kysely: Kysely<DittoTables>, event: NostrEvent, x: number, relay: NStore): Promise<void> { async function handleEvent3(opts: UpdateStatsOpts): Promise<void> {
const { relay, kysely, event, x = 1 } = opts;
const following = getTagSet(event.tags, 'p'); const following = getTagSet(event.tags, 'p');
await updateAuthorStats(kysely, event.pubkey, () => ({ following_count: following.size })); await updateAuthorStats(kysely, event.pubkey, () => ({ following_count: following.size }));
@ -117,26 +125,34 @@ async function handleEvent3(kysely: Kysely<DittoTables>, event: NostrEvent, x: n
} }
/** Update stats for kind 5 event. */ /** Update stats for kind 5 event. */
async function handleEvent5(kysely: Kysely<DittoTables>, event: NostrEvent, x: -1, relay: NStore): Promise<void> { async function handleEvent5(opts: UpdateStatsOpts): Promise<void> {
const { relay, event, x = -1 } = opts;
const id = event.tags.find(([name]) => name === 'e')?.[1]; const id = event.tags.find(([name]) => name === 'e')?.[1];
if (id) { if (id) {
const [target] = await relay.query([{ ids: [id], authors: [event.pubkey], limit: 1 }]); const [target] = await relay.query([{ ids: [id], authors: [event.pubkey], limit: 1 }]);
if (target) { if (target) {
await updateStats({ event: target, kysely, relay, x }); await updateStats({ ...opts, event: target, x });
} }
} }
} }
/** Update stats for kind 6 event. */ /** Update stats for kind 6 event. */
async function handleEvent6(kysely: Kysely<DittoTables>, event: NostrEvent, x: number): Promise<void> { async function handleEvent6(opts: UpdateStatsOpts): Promise<void> {
const { kysely, event, x = 1 } = opts;
const id = event.tags.find(([name]) => name === 'e')?.[1]; const id = event.tags.find(([name]) => name === 'e')?.[1];
if (id) { if (id) {
await updateEventStats(kysely, id, ({ reposts_count }) => ({ reposts_count: Math.max(0, reposts_count + x) })); await updateEventStats(kysely, id, ({ reposts_count }) => ({ reposts_count: Math.max(0, reposts_count + x) }));
} }
} }
/** Update stats for kind 7 event. */ /** Update stats for kind 7 event. */
async function handleEvent7(kysely: Kysely<DittoTables>, event: NostrEvent, x: number): Promise<void> { async function handleEvent7(opts: UpdateStatsOpts): Promise<void> {
const { kysely, event, x = 1 } = opts;
const id = event.tags.findLast(([name]) => name === 'e')?.[1]; const id = event.tags.findLast(([name]) => name === 'e')?.[1];
const emoji = event.content; const emoji = event.content;
@ -166,12 +182,15 @@ async function handleEvent7(kysely: Kysely<DittoTables>, event: NostrEvent, x: n
} }
/** Update stats for kind 9735 event. */ /** Update stats for kind 9735 event. */
async function handleEvent9735(kysely: Kysely<DittoTables>, event: NostrEvent): Promise<void> { async function handleEvent9735(opts: UpdateStatsOpts): Promise<void> {
const { kysely, event } = opts;
// https://github.com/nostr-protocol/nips/blob/master/57.md#appendix-f-validating-zap-receipts // https://github.com/nostr-protocol/nips/blob/master/57.md#appendix-f-validating-zap-receipts
const id = event.tags.find(([name]) => name === 'e')?.[1]; const id = event.tags.find(([name]) => name === 'e')?.[1];
if (!id) return; if (!id) return;
const amountSchema = z.coerce.number().int().nonnegative().catch(0); const amountSchema = z.coerce.number().int().nonnegative().catch(0);
let amount = 0; let amount = 0;
try { try {
const zapRequest = n.json().pipe(n.event()).parse(event.tags.find(([name]) => name === 'description')?.[1]); const zapRequest = n.json().pipe(n.event()).parse(event.tags.find(([name]) => name === 'description')?.[1]);

View file

@ -6,10 +6,11 @@ import DOMPurify from 'isomorphic-dompurify';
import { unfurl } from 'unfurl.js'; import { unfurl } from 'unfurl.js';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { PreviewCard } from '@/entities/PreviewCard.ts';
import { errorJson } from '@/utils/log.ts'; import { errorJson } from '@/utils/log.ts';
async function unfurlCard(url: string, signal: AbortSignal): Promise<PreviewCard | null> { import type { MastodonPreviewCard } from '@ditto/mastoapi/types';
async function unfurlCard(url: string, signal: AbortSignal): Promise<MastodonPreviewCard | null> {
try { try {
const result = await unfurl(url, { const result = await unfurl(url, {
fetch: (url) => fetch: (url) =>
@ -55,10 +56,10 @@ async function unfurlCard(url: string, signal: AbortSignal): Promise<PreviewCard
} }
/** TTL cache for preview cards. */ /** TTL cache for preview cards. */
const previewCardCache = new TTLCache<string, Promise<PreviewCard | null>>(Conf.caches.linkPreview); const previewCardCache = new TTLCache<string, Promise<MastodonPreviewCard | null>>(Conf.caches.linkPreview);
/** Unfurl card from cache if available, otherwise fetch it. */ /** Unfurl card from cache if available, otherwise fetch it. */
function unfurlCardCached(url: string, signal = AbortSignal.timeout(1000)): Promise<PreviewCard | null> { export function unfurlCardCached(url: string, signal = AbortSignal.timeout(1000)): Promise<MastodonPreviewCard | null> {
const cached = previewCardCache.get(url); const cached = previewCardCache.get(url);
if (cached !== undefined) { if (cached !== undefined) {
return cached; return cached;
@ -69,5 +70,3 @@ function unfurlCardCached(url: string, signal = AbortSignal.timeout(1000)): Prom
return card; return card;
} }
} }
export { type PreviewCard, unfurlCardCached };

View file

@ -6,7 +6,6 @@ import { encode } from 'blurhash';
import sharp from 'sharp'; import sharp from 'sharp';
import { AppContext } from '@/app.ts'; import { AppContext } from '@/app.ts';
import { Conf } from '@/config.ts';
import { DittoUpload, dittoUploads } from '@/DittoUploads.ts'; import { DittoUpload, dittoUploads } from '@/DittoUploads.ts';
import { errorJson } from '@/utils/log.ts'; import { errorJson } from '@/utils/log.ts';
@ -22,7 +21,8 @@ export async function uploadFile(
meta: FileMeta, meta: FileMeta,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<DittoUpload> { ): Promise<DittoUpload> {
const uploader = c.get('uploader'); const { conf, uploader } = c.var;
if (!uploader) { if (!uploader) {
throw new HTTPException(500, { throw new HTTPException(500, {
res: c.json({ error: 'No uploader configured.' }), res: c.json({ error: 'No uploader configured.' }),
@ -31,7 +31,7 @@ export async function uploadFile(
const { pubkey, description } = meta; const { pubkey, description } = meta;
if (file.size > Conf.maxUploadSize) { if (file.size > conf.maxUploadSize) {
throw new Error('File size is too large.'); throw new Error('File size is too large.');
} }
@ -63,7 +63,7 @@ export async function uploadFile(
// If the uploader didn't already, try to get a blurhash and media dimensions. // If the uploader didn't already, try to get a blurhash and media dimensions.
// This requires `MEDIA_ANALYZE=true` to be configured because it comes with security tradeoffs. // This requires `MEDIA_ANALYZE=true` to be configured because it comes with security tradeoffs.
if (Conf.mediaAnalyze && (!blurhash || !dim)) { if (conf.mediaAnalyze && (!blurhash || !dim)) {
try { try {
const bytes = await new Response(file.stream()).bytes(); const bytes = await new Response(file.stream()).bytes();
const img = sharp(bytes); const img = sharp(bytes);

View file

@ -1,8 +1,9 @@
import { Conf } from '@/config.ts';
import { NSchema as n, NStore } from '@nostrify/nostrify'; import { NSchema as n, NStore } from '@nostrify/nostrify';
import { nostrNow } from '@/utils.ts'; import { nostrNow } from '@/utils.ts';
import { percentageSchema } from '@/schema.ts'; import { percentageSchema } from '@/schema.ts';
import type { DittoConf } from '@ditto/conf';
type Pubkey = string; type Pubkey = string;
type ExtraMessage = string; type ExtraMessage = string;
/** Number from 1 to 100, stringified. */ /** Number from 1 to 100, stringified. */
@ -12,11 +13,18 @@ export type DittoZapSplits = {
[key: Pubkey]: { weight: splitPercentages; message: ExtraMessage }; [key: Pubkey]: { weight: splitPercentages; message: ExtraMessage };
}; };
interface GetZapSplitsOpts {
conf: DittoConf;
relay: NStore;
}
/** Gets zap splits from NIP-78 in DittoZapSplits format. */ /** Gets zap splits from NIP-78 in DittoZapSplits format. */
export async function getZapSplits(store: NStore, pubkey: string): Promise<DittoZapSplits | undefined> { export async function getZapSplits(pubkey: string, opts: GetZapSplitsOpts): Promise<DittoZapSplits | undefined> {
const { relay } = opts;
const zapSplits: DittoZapSplits = {}; const zapSplits: DittoZapSplits = {};
const [event] = await store.query([{ const [event] = await relay.query([{
authors: [pubkey], authors: [pubkey],
kinds: [30078], kinds: [30078],
'#d': ['pub.ditto.zapSplits'], '#d': ['pub.ditto.zapSplits'],
@ -36,15 +44,17 @@ export async function getZapSplits(store: NStore, pubkey: string): Promise<Ditto
return zapSplits; return zapSplits;
} }
export async function seedZapSplits(store: NStore) { export async function seedZapSplits(opts: GetZapSplitsOpts): Promise<void> {
const zapSplit: DittoZapSplits | undefined = await getZapSplits(store, await Conf.signer.getPublicKey()); const { conf, relay } = opts;
const pubkey = await conf.signer.getPublicKey();
const zapSplit: DittoZapSplits | undefined = await getZapSplits(pubkey, opts);
if (!zapSplit) { if (!zapSplit) {
const dittoPubkey = '781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5'; const dittoPubkey = '781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5';
const dittoMsg = 'Official Ditto Account'; const dittoMsg = 'Official Ditto Account';
const signer = Conf.signer; const event = await conf.signer.signEvent({
const event = await signer.signEvent({
content: '', content: '',
created_at: nostrNow(), created_at: nostrNow(),
kind: 30078, kind: 30078,
@ -54,6 +64,6 @@ export async function seedZapSplits(store: NStore) {
], ],
}); });
await store.event(event); await relay.event(event);
} }
} }

View file

@ -2,7 +2,6 @@ import { NSchema as n } from '@nostrify/nostrify';
import { nip19, UnsignedEvent } from 'nostr-tools'; import { nip19, UnsignedEvent } from 'nostr-tools';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { MastodonAccount } from '@/entities/MastodonAccount.ts';
import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { metadataSchema } from '@/schemas/nostr.ts'; import { metadataSchema } from '@/schemas/nostr.ts';
import { getLnurl } from '@/utils/lnurl.ts'; import { getLnurl } from '@/utils/lnurl.ts';
@ -11,6 +10,8 @@ import { getTagSet } from '@/utils/tags.ts';
import { nostrDate, nostrNow, parseNip05 } from '@/utils.ts'; import { nostrDate, nostrNow, parseNip05 } from '@/utils.ts';
import { renderEmojis } from '@/views/mastodon/emojis.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts';
import type { MastodonAccount } from '@ditto/mastoapi/types';
type ToAccountOpts = { type ToAccountOpts = {
withSource: true; withSource: true;
settingsStore: Record<string, unknown> | undefined; settingsStore: Record<string, unknown> | undefined;
@ -47,7 +48,7 @@ function renderAccount(event: Omit<DittoEvent, 'id' | 'sig'>, opts: ToAccountOpt
const parsed05 = stats?.nip05 ? parseNip05(stats.nip05) : undefined; const parsed05 = stats?.nip05 ? parseNip05(stats.nip05) : undefined;
const acct = parsed05?.handle || npub; const acct = parsed05?.handle || npub;
const { html } = parseNoteContent(about || '', []); const { html } = parseNoteContent(about || '', [], { conf: Conf });
const fields = _fields const fields = _fields
?.slice(0, Conf.profileFields.maxFields) ?.slice(0, Conf.profileFields.maxFields)
@ -83,7 +84,7 @@ function renderAccount(event: Omit<DittoEvent, 'id' | 'sig'>, opts: ToAccountOpt
discoverable: true, discoverable: true,
display_name: name ?? '', display_name: name ?? '',
emojis: renderEmojis(event), emojis: renderEmojis(event),
fields: fields.map((field) => ({ ...field, value: parseNoteContent(field.value, []).html })), fields: fields.map((field) => ({ ...field, value: parseNoteContent(field.value, [], { conf: Conf }).html })),
follow_requests_count: 0, follow_requests_count: 0,
followers_count: stats?.followers_count ?? 0, followers_count: stats?.followers_count ?? 0,
following_count: stats?.following_count ?? 0, following_count: stats?.following_count ?? 0,

View file

@ -1,4 +1,5 @@
import { MastodonAttachment } from '@/entities/MastodonAttachment.ts'; import { MastodonAttachment } from '@ditto/mastoapi/types';
import { getUrlMediaType } from '@/utils/media.ts'; import { getUrlMediaType } from '@/utils/media.ts';
/** Render Mastodon media attachment. */ /** Render Mastodon media attachment. */

View file

@ -2,9 +2,6 @@ import { NostrEvent, NStore } from '@nostrify/nostrify';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { MastodonAttachment } from '@/entities/MastodonAttachment.ts';
import { MastodonMention } from '@/entities/MastodonMention.ts';
import { MastodonStatus } from '@/entities/MastodonStatus.ts';
import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { nostrDate } from '@/utils.ts'; import { nostrDate } from '@/utils.ts';
import { getMediaLinks, parseNoteContent, stripimeta } from '@/utils/note.ts'; import { getMediaLinks, parseNoteContent, stripimeta } from '@/utils/note.ts';
@ -14,6 +11,8 @@ import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
import { renderAttachment } from '@/views/mastodon/attachments.ts'; import { renderAttachment } from '@/views/mastodon/attachments.ts';
import { renderEmojis } from '@/views/mastodon/emojis.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts';
import { MastodonAttachment, MastodonMention, MastodonStatus } from '@ditto/mastoapi/types';
interface RenderStatusOpts { interface RenderStatusOpts {
viewerPubkey?: string; viewerPubkey?: string;
depth?: number; depth?: number;
@ -43,7 +42,7 @@ async function renderStatus(
const mentions = event.mentions?.map((event) => renderMention(event)) ?? []; const mentions = event.mentions?.map((event) => renderMention(event)) ?? [];
const { html, links, firstUrl } = parseNoteContent(stripimeta(event.content, event.tags), mentions); const { html, links, firstUrl } = parseNoteContent(stripimeta(event.content, event.tags), mentions, { conf: Conf });
const [card, relatedEvents] = await Promise const [card, relatedEvents] = await Promise
.all([ .all([

View file

@ -0,0 +1,14 @@
import { DittoConf } from '@ditto/conf';
import { generateSecretKey, nip19 } from 'nostr-tools';
import { PolicyWorker } from './policy.ts';
Deno.test('PolicyWorker', () => {
const conf = new DittoConf(
new Map([
['DITTO_NSEC', nip19.nsecEncode(generateSecretKey())],
]),
);
new PolicyWorker(conf);
});

View file

@ -3,6 +3,8 @@ import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/nostrify';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import * as Comlink from 'comlink'; import * as Comlink from 'comlink';
import { errorJson } from '@/utils/log.ts';
import type { CustomPolicy } from '@/workers/policy.worker.ts'; import type { CustomPolicy } from '@/workers/policy.worker.ts';
export class PolicyWorker implements NPolicy { export class PolicyWorker implements NPolicy {
@ -85,6 +87,15 @@ export class PolicyWorker implements NPolicy {
return; return;
} }
logi({
level: 'error',
ns: 'ditto.system.policy',
msg: 'Failed to load custom policy',
path: conf.policy,
error: errorJson(e),
enabled: false,
});
throw new Error(`DITTO_POLICY (error importing policy): ${conf.policy}`); throw new Error(`DITTO_POLICY (error importing policy): ${conf.policy}`);
} }
} }

View file

@ -1,9 +1,11 @@
import { DittoConf } from '@ditto/conf';
import { DittoPolyPg } from '@ditto/db'; import { DittoPolyPg } from '@ditto/db';
import '@soapbox/safe-fetch/load'; import '@soapbox/safe-fetch/load';
import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/nostrify'; import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/nostrify';
import { ReadOnlyPolicy } from '@nostrify/policies'; import { ReadOnlyPolicy } from '@nostrify/policies';
import * as Comlink from 'comlink'; import * as Comlink from 'comlink';
import { ReadOnlySigner } from '@/signers/ReadOnlySigner.ts';
import { DittoPgStore } from '@/storages/DittoPgStore.ts'; import { DittoPgStore } from '@/storages/DittoPgStore.ts';
// @ts-ignore Don't try to access the env from this worker. // @ts-ignore Don't try to access the env from this worker.
@ -32,9 +34,18 @@ export class CustomPolicy implements NPolicy {
const db = new DittoPolyPg(databaseUrl, { poolSize: 1 }); const db = new DittoPolyPg(databaseUrl, { poolSize: 1 });
const conf = new Proxy(new DittoConf(new Map()), {
get(target, prop) {
if (prop === 'signer') {
return new ReadOnlySigner(pubkey);
}
return Reflect.get(target, prop);
},
});
const store = new DittoPgStore({ const store = new DittoPgStore({
db, db,
pubkey, conf,
timeout: 5_000, timeout: 5_000,
}); });

View file

@ -3,7 +3,6 @@ import * as Comlink from 'comlink';
import { VerifiedEvent, verifyEvent } from 'nostr-tools'; import { VerifiedEvent, verifyEvent } from 'nostr-tools';
import '@/nostr-wasm.ts'; import '@/nostr-wasm.ts';
import '@/sentry.ts';
export const VerifyWorker = { export const VerifyWorker = {
verifyEvent(event: NostrEvent): event is VerifiedEvent { verifyEvent(event: NostrEvent): event is VerifiedEvent {

View file

@ -5,6 +5,7 @@
"./middleware": "./middleware/mod.ts", "./middleware": "./middleware/mod.ts",
"./pagination": "./pagination/mod.ts", "./pagination": "./pagination/mod.ts",
"./router": "./router/mod.ts", "./router": "./router/mod.ts",
"./test": "./test.ts" "./test": "./test.ts",
"./types": "./types/mod.ts"
} }
} }

View file

@ -7,7 +7,7 @@ export class DittoApp extends Hono<DittoEnv> {
// @ts-ignore Require a DittoRoute for type safety. // @ts-ignore Require a DittoRoute for type safety.
declare route: (path: string, app: Hono<DittoEnv>) => Hono<DittoEnv>; declare route: (path: string, app: Hono<DittoEnv>) => Hono<DittoEnv>;
constructor(opts: Omit<DittoEnv['Variables'], 'signal'> & HonoOptions<DittoEnv>) { constructor(opts: Omit<DittoEnv['Variables'], 'signal' | 'requestId'> & HonoOptions<DittoEnv>) {
super(opts); super(opts);
this.use((c, next) => { this.use((c, next) => {
@ -15,6 +15,7 @@ export class DittoApp extends Hono<DittoEnv> {
c.set('conf', opts.conf); c.set('conf', opts.conf);
c.set('relay', opts.relay); c.set('relay', opts.relay);
c.set('signal', c.req.raw.signal); c.set('signal', c.req.raw.signal);
c.set('requestId', c.req.header('X-Request-Id') ?? crypto.randomUUID());
return next(); return next();
}); });
} }

View file

@ -16,5 +16,7 @@ export interface DittoEnv extends Env {
db: DittoDB; db: DittoDB;
/** Abort signal for the request. */ /** Abort signal for the request. */
signal: AbortSignal; signal: AbortSignal;
/** Unique ID for the request. */
requestId: string;
}; };
} }

View file

@ -2,4 +2,4 @@ import type { MiddlewareHandler } from '@hono/hono';
import type { DittoEnv } from './DittoEnv.ts'; import type { DittoEnv } from './DittoEnv.ts';
// deno-lint-ignore ban-types // deno-lint-ignore ban-types
export type DittoMiddleware<T extends {}> = MiddlewareHandler<DittoEnv & { Variables: T }>; export type DittoMiddleware<T extends {} = {}> = MiddlewareHandler<DittoEnv & { Variables: T }>;

View file

@ -25,6 +25,7 @@ export class DittoRoute extends Hono<DittoEnv> {
if (!vars.conf) this.throwMissingVar('conf'); if (!vars.conf) this.throwMissingVar('conf');
if (!vars.relay) this.throwMissingVar('relay'); if (!vars.relay) this.throwMissingVar('relay');
if (!vars.signal) this.throwMissingVar('signal'); if (!vars.signal) this.throwMissingVar('signal');
if (!vars.requestId) this.throwMissingVar('requestId');
return { return {
...vars, ...vars,
@ -32,6 +33,7 @@ export class DittoRoute extends Hono<DittoEnv> {
conf: vars.conf, conf: vars.conf,
relay: vars.relay, relay: vars.relay,
signal: vars.signal, signal: vars.signal,
requestId: vars.requestId,
}; };
} }

View file

@ -1,4 +1,4 @@
export interface PreviewCard { export interface MastodonPreviewCard {
url: string; url: string;
title: string; title: string;
description: string; description: string;

View file

@ -1,11 +1,11 @@
import { MastodonAccount } from '@/entities/MastodonAccount.ts'; import type { MastodonAccount } from './MastodonAccount.ts';
import { MastodonAttachment } from '@/entities/MastodonAttachment.ts'; import type { MastodonAttachment } from './MastodonAttachment.ts';
import { PreviewCard } from '@/entities/PreviewCard.ts'; import type { MastodonPreviewCard } from './MastodonPreviewCard.ts';
export interface MastodonStatus { export interface MastodonStatus {
id: string; id: string;
account: MastodonAccount; account: MastodonAccount;
card: PreviewCard | null; card: MastodonPreviewCard | null;
content: string; content: string;
created_at: string; created_at: string;
in_reply_to_id: string | null; in_reply_to_id: string | null;

View file

@ -1,4 +1,4 @@
import { LanguageCode } from 'iso-639-1'; import type { LanguageCode } from 'iso-639-1';
/** https://docs.joinmastodon.org/entities/Translation/ */ /** https://docs.joinmastodon.org/entities/Translation/ */
export interface MastodonTranslation { export interface MastodonTranslation {

View file

@ -0,0 +1,6 @@
export type { MastodonAccount } from './MastodonAccount.ts';
export type { MastodonAttachment } from './MastodonAttachment.ts';
export type { MastodonMention } from './MastodonMention.ts';
export type { MastodonPreviewCard } from './MastodonPreviewCard.ts';
export type { MastodonStatus } from './MastodonStatus.ts';
export type { MastodonTranslation } from './MastodonTranslation.ts';

View file

@ -72,8 +72,6 @@ export class LibreTranslateTranslator implements DittoTranslator {
const response = await this.fetch(request); const response = await this.fetch(request);
const json = await response.json(); const json = await response.json();
console.log(json);
if (!response.ok) { if (!response.ok) {
const result = LibreTranslateTranslator.errorSchema().safeParse(json); const result = LibreTranslateTranslator.errorSchema().safeParse(json);

View file

@ -9,7 +9,7 @@ import { nostrNow } from '../packages/ditto/utils.ts';
const conf = new DittoConf(Deno.env); const conf = new DittoConf(Deno.env);
const db = new DittoPolyPg(conf.databaseUrl); const db = new DittoPolyPg(conf.databaseUrl);
const relay = new DittoPgStore({ db, pubkey: await conf.signer.getPublicKey() }); const relay = new DittoPgStore({ db, conf });
const { signer } = conf; const { signer } = conf;

View file

@ -8,7 +8,7 @@ import { nostrNow } from '../packages/ditto/utils.ts';
const conf = new DittoConf(Deno.env); const conf = new DittoConf(Deno.env);
const db = new DittoPolyPg(conf.databaseUrl); const db = new DittoPolyPg(conf.databaseUrl);
const relay = new DittoPgStore({ db, pubkey: await conf.signer.getPublicKey() }); const relay = new DittoPgStore({ db, conf });
const [pubkeyOrNpub, role] = Deno.args; const [pubkeyOrNpub, role] = Deno.args;
const pubkey = pubkeyOrNpub.startsWith('npub1') ? nip19.decode(pubkeyOrNpub as `npub1${string}`).data : pubkeyOrNpub; const pubkey = pubkeyOrNpub.startsWith('npub1') ? nip19.decode(pubkeyOrNpub as `npub1${string}`).data : pubkeyOrNpub;

View file

@ -7,7 +7,7 @@ import { DittoPgStore } from '../packages/ditto/storages/DittoPgStore.ts';
const conf = new DittoConf(Deno.env); const conf = new DittoConf(Deno.env);
const db = new DittoPolyPg(conf.databaseUrl); const db = new DittoPolyPg(conf.databaseUrl);
const relay = new DittoPgStore({ db, pubkey: await conf.signer.getPublicKey() }); const relay = new DittoPgStore({ db, conf });
interface ExportFilter { interface ExportFilter {
authors?: string[]; authors?: string[];

View file

@ -9,7 +9,7 @@ import { DittoPgStore } from '../packages/ditto/storages/DittoPgStore.ts';
const conf = new DittoConf(Deno.env); const conf = new DittoConf(Deno.env);
const db = new DittoPolyPg(conf.databaseUrl); const db = new DittoPolyPg(conf.databaseUrl);
const relay = new DittoPgStore({ db, pubkey: await conf.signer.getPublicKey() }); const relay = new DittoPgStore({ db, conf });
const sem = new Semaphore(conf.pg.poolSize); const sem = new Semaphore(conf.pg.poolSize);
console.warn('Importing events...'); console.warn('Importing events...');

View file

@ -6,7 +6,7 @@ import { PolicyWorker } from '../packages/ditto/workers/policy.ts';
const conf = new DittoConf(Deno.env); const conf = new DittoConf(Deno.env);
const db = new DittoPolyPg(conf.databaseUrl); const db = new DittoPolyPg(conf.databaseUrl);
const relay = new DittoPgStore({ db, pubkey: await conf.signer.getPublicKey() }); const relay = new DittoPgStore({ db, conf });
const policyWorker = new PolicyWorker(conf); const policyWorker = new PolicyWorker(conf);
let count = 0; let count = 0;

View file

@ -10,7 +10,7 @@ import { DittoRelayStore } from '../packages/ditto/storages/DittoRelayStore.ts';
const conf = new DittoConf(Deno.env); const conf = new DittoConf(Deno.env);
const db = new DittoPolyPg(conf.databaseUrl); const db = new DittoPolyPg(conf.databaseUrl);
const pgstore = new DittoPgStore({ db, pubkey: await conf.signer.getPublicKey() }); const pgstore = new DittoPgStore({ db, conf });
const relaystore = new DittoRelayStore({ conf, db, relay: pgstore }); const relaystore = new DittoRelayStore({ conf, db, relay: pgstore });
const sem = new Semaphore(5); const sem = new Semaphore(5);

View file

@ -6,7 +6,7 @@ import { DittoPgStore } from '../packages/ditto/storages/DittoPgStore.ts';
const conf = new DittoConf(Deno.env); const conf = new DittoConf(Deno.env);
const db = new DittoPolyPg(conf.databaseUrl); const db = new DittoPolyPg(conf.databaseUrl);
const relay = new DittoPgStore({ db, pubkey: await conf.signer.getPublicKey() }); const relay = new DittoPgStore({ db, conf });
for await (const msg of relay.req([{ kinds: [0] }])) { for await (const msg of relay.req([{ kinds: [0] }])) {
if (msg[0] === 'EVENT') { if (msg[0] === 'EVENT') {

View file

@ -12,7 +12,7 @@ import { DittoPgStore } from '../packages/ditto/storages/DittoPgStore.ts';
const conf = new DittoConf(Deno.env); const conf = new DittoConf(Deno.env);
const db = new DittoPolyPg(conf.databaseUrl); const db = new DittoPolyPg(conf.databaseUrl);
const relay = new DittoPgStore({ db, pubkey: await conf.signer.getPublicKey() }); const relay = new DittoPgStore({ db, conf });
interface ImportEventsOpts { interface ImportEventsOpts {
profilesOnly: boolean; profilesOnly: boolean;

View file

@ -7,7 +7,7 @@ import { DittoPgStore } from '../packages/ditto/storages/DittoPgStore.ts';
const conf = new DittoConf(Deno.env); const conf = new DittoConf(Deno.env);
const db = new DittoPolyPg(conf.databaseUrl); const db = new DittoPolyPg(conf.databaseUrl);
const relay = new DittoPgStore({ db, pubkey: await conf.signer.getPublicKey() }); const relay = new DittoPgStore({ db, conf });
function die(code: number, ...args: unknown[]) { function die(code: number, ...args: unknown[]) {
console.error(...args); console.error(...args);

View file

@ -7,7 +7,7 @@ import { refreshAuthorStats } from '../packages/ditto/utils/stats.ts';
const conf = new DittoConf(Deno.env); const conf = new DittoConf(Deno.env);
const db = new DittoPolyPg(conf.databaseUrl); const db = new DittoPolyPg(conf.databaseUrl);
const relay = new DittoPgStore({ db, pubkey: await conf.signer.getPublicKey() }); const relay = new DittoPgStore({ db, conf });
const { kysely } = db; const { kysely } = db;

View file

@ -13,7 +13,7 @@ import {
const conf = new DittoConf(Deno.env); const conf = new DittoConf(Deno.env);
const db = new DittoPolyPg(conf.databaseUrl); const db = new DittoPolyPg(conf.databaseUrl);
const relay = new DittoPgStore({ db, pubkey: await conf.signer.getPublicKey() }); const relay = new DittoPgStore({ db, conf });
const ctx = { conf, db, relay }; const ctx = { conf, db, relay };
const trendSchema = z.enum(['pubkeys', 'zapped_events', 'events', 'hashtags', 'links']); const trendSchema = z.enum(['pubkeys', 'zapped_events', 'events', 'hashtags', 'links']);