mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Remove @/storages.ts (jesus christ)
This commit is contained in:
parent
ca5c887705
commit
3b17fd9b45
53 changed files with 639 additions and 1216 deletions
|
|
@ -1,39 +1,41 @@
|
||||||
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { ApplicationServer, PushMessageOptions, PushSubscriber, PushSubscription } from '@negrel/webpush';
|
import { ApplicationServer, PushMessageOptions, PushSubscriber, PushSubscription } from '@negrel/webpush';
|
||||||
|
import { NStore } from '@nostrify/types';
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
|
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { getInstanceMetadata } from '@/utils/instance.ts';
|
import { getInstanceMetadata } from '@/utils/instance.ts';
|
||||||
|
|
||||||
|
interface DittoPushOpts {
|
||||||
|
conf: DittoConf;
|
||||||
|
relay: NStore;
|
||||||
|
}
|
||||||
|
|
||||||
export class DittoPush {
|
export class DittoPush {
|
||||||
static _server: Promise<ApplicationServer | undefined> | undefined;
|
private server: Promise<ApplicationServer | undefined>;
|
||||||
|
|
||||||
static get server(): Promise<ApplicationServer | undefined> {
|
constructor(opts: DittoPushOpts) {
|
||||||
if (!this._server) {
|
const { conf, relay } = opts;
|
||||||
this._server = (async () => {
|
|
||||||
const store = await Storages.db();
|
|
||||||
const meta = await getInstanceMetadata(store);
|
|
||||||
const keys = await Conf.vapidKeys;
|
|
||||||
|
|
||||||
if (keys) {
|
this.server = (async () => {
|
||||||
return await ApplicationServer.new({
|
const meta = await getInstanceMetadata(relay);
|
||||||
contactInformation: `mailto:${meta.email}`,
|
const keys = await conf.vapidKeys;
|
||||||
vapidKeys: keys,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logi({
|
|
||||||
level: 'warn',
|
|
||||||
ns: 'ditto.push',
|
|
||||||
msg: 'VAPID keys are not set. Push notifications will be disabled.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._server;
|
if (keys) {
|
||||||
|
return await ApplicationServer.new({
|
||||||
|
contactInformation: `mailto:${meta.email}`,
|
||||||
|
vapidKeys: keys,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logi({
|
||||||
|
level: 'warn',
|
||||||
|
ns: 'ditto.push',
|
||||||
|
msg: 'VAPID keys are not set. Push notifications will be disabled.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async push(
|
async push(
|
||||||
subscription: PushSubscription,
|
subscription: PushSubscription,
|
||||||
json: object,
|
json: object,
|
||||||
opts: PushMessageOptions = {},
|
opts: PushMessageOptions = {},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { DittoConf } from '@ditto/conf';
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { DittoDB } from '@ditto/db';
|
import { DittoDB, 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 { type DittoTranslator } from '@ditto/translators';
|
import { type DittoTranslator } from '@ditto/translators';
|
||||||
import { type Context, Handler, Input as HonoInput, MiddlewareHandler } from '@hono/hono';
|
import { type Context, Handler, Input as HonoInput, MiddlewareHandler } from '@hono/hono';
|
||||||
import { every } from '@hono/hono/combine';
|
import { every } from '@hono/hono/combine';
|
||||||
|
|
@ -9,11 +10,13 @@ import { cors } from '@hono/hono/cors';
|
||||||
import { serveStatic } from '@hono/hono/deno';
|
import { serveStatic } from '@hono/hono/deno';
|
||||||
import { NostrEvent, NostrSigner, NRelay, NUploader } from '@nostrify/nostrify';
|
import { NostrEvent, NostrSigner, NRelay, NUploader } from '@nostrify/nostrify';
|
||||||
|
|
||||||
import '@/startup.ts';
|
import { cron } from '@/cron.ts';
|
||||||
|
import { startFirehose } from '@/firehose.ts';
|
||||||
import { Conf } from '@/config.ts';
|
import { DittoAPIStore } from '@/storages/DittoAPIStore.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { DittoPgStore } from '@/storages/DittoPgStore.ts';
|
||||||
|
import { DittoPool } from '@/storages/DittoPool.ts';
|
||||||
import { Time } from '@/utils/time.ts';
|
import { Time } from '@/utils/time.ts';
|
||||||
|
import { seedZapSplits } from '@/utils/zap-split.ts';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
accountController,
|
accountController,
|
||||||
|
|
@ -176,14 +179,42 @@ type AppMiddleware = MiddlewareHandler<AppEnv>;
|
||||||
// deno-lint-ignore no-explicit-any
|
// deno-lint-ignore no-explicit-any
|
||||||
type AppController<P extends string = any> = Handler<AppEnv, P, HonoInput, Response | Promise<Response>>;
|
type AppController<P extends string = any> = Handler<AppEnv, P, HonoInput, Response | Promise<Response>>;
|
||||||
|
|
||||||
const app = new DittoApp({
|
const conf = new DittoConf(Deno.env);
|
||||||
conf: Conf,
|
|
||||||
db: await Storages.database(),
|
const db = new DittoPolyPg(conf.databaseUrl, {
|
||||||
relay: await Storages.db(),
|
poolSize: conf.pg.poolSize,
|
||||||
}, {
|
debug: conf.pgliteDebug,
|
||||||
strict: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await db.migrate();
|
||||||
|
|
||||||
|
const store = new DittoPgStore({
|
||||||
|
db,
|
||||||
|
pubkey: await conf.signer.getPublicKey(),
|
||||||
|
timeout: conf.db.timeouts.default,
|
||||||
|
notify: conf.notifyEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pool = new DittoPool({ conf, relay: store });
|
||||||
|
const relay = new DittoAPIStore({ db, conf, relay: store, pool });
|
||||||
|
|
||||||
|
await seedZapSplits(relay);
|
||||||
|
|
||||||
|
if (conf.firehoseEnabled) {
|
||||||
|
startFirehose({
|
||||||
|
pool,
|
||||||
|
store: relay,
|
||||||
|
concurrency: conf.firehoseConcurrency,
|
||||||
|
kinds: conf.firehoseKinds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conf.cronEnabled) {
|
||||||
|
cron({ conf, db, relay });
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new DittoApp({ conf, db, relay }, { strict: false });
|
||||||
|
|
||||||
/** User-provided files in the gitignored `public/` directory. */
|
/** User-provided files in the gitignored `public/` directory. */
|
||||||
const publicFiles = serveStatic({ root: './public/' });
|
const publicFiles = serveStatic({ root: './public/' });
|
||||||
/** Static files provided by the Ditto repo, checked into git. */
|
/** Static files provided by the Ditto repo, checked into git. */
|
||||||
|
|
@ -218,7 +249,17 @@ app.use(
|
||||||
uploaderMiddleware,
|
uploaderMiddleware,
|
||||||
);
|
);
|
||||||
|
|
||||||
app.get('/metrics', metricsController);
|
app.get('/metrics', async (_c, next) => {
|
||||||
|
relayPoolRelaysSizeGauge.reset();
|
||||||
|
relayPoolSubscriptionsSizeGauge.reset();
|
||||||
|
|
||||||
|
for (const relay of pool.relays.values()) {
|
||||||
|
relayPoolRelaysSizeGauge.inc({ ready_state: relay.socket.readyState });
|
||||||
|
relayPoolSubscriptionsSizeGauge.inc(relay.subscriptions.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}, metricsController);
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
'/.well-known/nodeinfo',
|
'/.well-known/nodeinfo',
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
import { paginated } from '@ditto/mastoapi/pagination';
|
||||||
|
import { NostrEvent, NostrFilter, NSchema as n, NStore } from '@nostrify/nostrify';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { getAuthor, getFollowedPubkeys } from '@/queries.ts';
|
import { getAuthor, getFollowedPubkeys } from '@/queries.ts';
|
||||||
import { booleanParamSchema, fileSchema } from '@/schema.ts';
|
import { booleanParamSchema, fileSchema } from '@/schema.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { uploadFile } from '@/utils/upload.ts';
|
import { uploadFile } from '@/utils/upload.ts';
|
||||||
import { nostrNow } from '@/utils.ts';
|
import { nostrNow } from '@/utils.ts';
|
||||||
import { assertAuthenticated, createEvent, paginated, parseBody, updateEvent, updateListEvent } from '@/utils/api.ts';
|
import { assertAuthenticated, createEvent, parseBody, updateEvent, updateListEvent } from '@/utils/api.ts';
|
||||||
import { extractIdentifier, lookupAccount, lookupPubkey } from '@/utils/lookup.ts';
|
import { extractIdentifier, lookupAccount, lookupPubkey } from '@/utils/lookup.ts';
|
||||||
import { renderAccounts, renderEventAccounts, renderStatuses } from '@/views.ts';
|
import { renderAccounts, renderEventAccounts, renderStatuses } from '@/views.ts';
|
||||||
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
|
|
@ -54,7 +54,7 @@ const verifyCredentialsController: AppController = async (c) => {
|
||||||
const pubkey = await signer.getPublicKey();
|
const pubkey = await signer.getPublicKey();
|
||||||
|
|
||||||
const [author, [settingsEvent]] = await Promise.all([
|
const [author, [settingsEvent]] = await Promise.all([
|
||||||
getAuthor(pubkey, { signal: AbortSignal.timeout(5000) }),
|
getAuthor(pubkey, c.var),
|
||||||
|
|
||||||
relay.query([{
|
relay.query([{
|
||||||
kinds: [30078],
|
kinds: [30078],
|
||||||
|
|
@ -81,7 +81,7 @@ const verifyCredentialsController: AppController = async (c) => {
|
||||||
const accountController: AppController = async (c) => {
|
const accountController: AppController = async (c) => {
|
||||||
const pubkey = c.req.param('pubkey');
|
const pubkey = c.req.param('pubkey');
|
||||||
|
|
||||||
const event = await getAuthor(pubkey);
|
const event = await getAuthor(pubkey, c.var);
|
||||||
if (event) {
|
if (event) {
|
||||||
assertAuthenticated(c, event);
|
assertAuthenticated(c, event);
|
||||||
return c.json(await renderAccount(event));
|
return c.json(await renderAccount(event));
|
||||||
|
|
@ -97,7 +97,7 @@ const accountLookupController: AppController = async (c) => {
|
||||||
return c.json({ error: 'Missing `acct` query parameter.' }, 422);
|
return c.json({ error: 'Missing `acct` query parameter.' }, 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = await lookupAccount(decodeURIComponent(acct));
|
const event = await lookupAccount(decodeURIComponent(acct), c.var);
|
||||||
if (event) {
|
if (event) {
|
||||||
assertAuthenticated(c, event);
|
assertAuthenticated(c, event);
|
||||||
return c.json(await renderAccount(event));
|
return c.json(await renderAccount(event));
|
||||||
|
|
@ -131,10 +131,10 @@ const accountSearchController: AppController = async (c) => {
|
||||||
const query = decodeURIComponent(result.data.q);
|
const query = decodeURIComponent(result.data.q);
|
||||||
|
|
||||||
const lookup = extractIdentifier(query);
|
const lookup = extractIdentifier(query);
|
||||||
const event = await lookupAccount(lookup ?? query);
|
const event = await lookupAccount(lookup ?? query, c.var);
|
||||||
|
|
||||||
if (!event && lookup) {
|
if (!event && lookup) {
|
||||||
const pubkey = await lookupPubkey(lookup);
|
const pubkey = await lookupPubkey(lookup, c.var);
|
||||||
return c.json(pubkey ? [accountFromPubkey(pubkey)] : []);
|
return c.json(pubkey ? [accountFromPubkey(pubkey)] : []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,7 +143,7 @@ const accountSearchController: AppController = async (c) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
events.push(event);
|
events.push(event);
|
||||||
} else {
|
} else {
|
||||||
const following = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set<string>();
|
const following = viewerPubkey ? await getFollowedPubkeys(relay, viewerPubkey, signal) : new Set<string>();
|
||||||
const authors = [...await getPubkeysBySearch(db.kysely, { q: query, limit, offset: 0, following })];
|
const authors = [...await getPubkeysBySearch(db.kysely, { q: query, limit, offset: 0, following })];
|
||||||
const profiles = await relay.query([{ kinds: [0], authors, limit }], { signal });
|
const profiles = await relay.query([{ kinds: [0], authors, limit }], { signal });
|
||||||
|
|
||||||
|
|
@ -155,14 +155,14 @@ const accountSearchController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const accounts = await hydrateEvents({ events, relay, signal })
|
const accounts = await hydrateEvents({ ...c.var, events })
|
||||||
.then((events) => events.map((event) => renderAccount(event)));
|
.then((events) => events.map((event) => renderAccount(event)));
|
||||||
|
|
||||||
return c.json(accounts);
|
return c.json(accounts);
|
||||||
};
|
};
|
||||||
|
|
||||||
const relationshipsController: AppController = async (c) => {
|
const relationshipsController: AppController = async (c) => {
|
||||||
const { user } = c.var;
|
const { relay, user } = c.var;
|
||||||
|
|
||||||
const pubkey = await user!.signer.getPublicKey();
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const ids = z.array(z.string()).safeParse(c.req.queries('id[]'));
|
const ids = z.array(z.string()).safeParse(c.req.queries('id[]'));
|
||||||
|
|
@ -171,11 +171,9 @@ const relationshipsController: AppController = async (c) => {
|
||||||
return c.json({ error: 'Missing `id[]` query parameters.' }, 422);
|
return c.json({ error: 'Missing `id[]` query parameters.' }, 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = await Storages.db();
|
|
||||||
|
|
||||||
const [sourceEvents, targetEvents] = await Promise.all([
|
const [sourceEvents, targetEvents] = await Promise.all([
|
||||||
db.query([{ kinds: [3, 10000], authors: [pubkey] }]),
|
relay.query([{ kinds: [3, 10000], authors: [pubkey] }]),
|
||||||
db.query([{ kinds: [3], authors: ids.data }]),
|
relay.query([{ kinds: [3], authors: ids.data }]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const event3 = sourceEvents.find((event) => event.kind === 3 && event.pubkey === pubkey);
|
const event3 = sourceEvents.find((event) => event.kind === 3 && event.pubkey === pubkey);
|
||||||
|
|
@ -267,7 +265,7 @@ const accountStatusesController: AppController = async (c) => {
|
||||||
const opts = { signal, limit, timeout: conf.db.timeouts.timelines };
|
const opts = { signal, limit, timeout: conf.db.timeouts.timelines };
|
||||||
|
|
||||||
const events = await relay.query([filter], opts)
|
const events = await relay.query([filter], opts)
|
||||||
.then((events) => hydrateEvents({ events, relay, signal }))
|
.then((events) => hydrateEvents({ ...c.var, events }))
|
||||||
.then((events) => {
|
.then((events) => {
|
||||||
if (exclude_replies) {
|
if (exclude_replies) {
|
||||||
return events.filter((event) => {
|
return events.filter((event) => {
|
||||||
|
|
@ -282,8 +280,8 @@ const accountStatusesController: AppController = async (c) => {
|
||||||
|
|
||||||
const statuses = await Promise.all(
|
const statuses = await Promise.all(
|
||||||
events.map((event) => {
|
events.map((event) => {
|
||||||
if (event.kind === 6) return renderReblog(event, { viewerPubkey });
|
if (event.kind === 6) return renderReblog(relay, event, { viewerPubkey });
|
||||||
return renderStatus(event, { viewerPubkey });
|
return renderStatus(relay, event, { viewerPubkey });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return paginated(c, events, statuses);
|
return paginated(c, events, statuses);
|
||||||
|
|
@ -305,7 +303,7 @@ const updateCredentialsSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateCredentialsController: AppController = async (c) => {
|
const updateCredentialsController: AppController = async (c) => {
|
||||||
const { relay, user, signal } = c.var;
|
const { relay, user } = c.var;
|
||||||
|
|
||||||
const pubkey = await user!.signer.getPublicKey();
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const body = await parseBody(c.req.raw);
|
const body = await parseBody(c.req.raw);
|
||||||
|
|
@ -375,7 +373,7 @@ const updateCredentialsController: AppController = async (c) => {
|
||||||
|
|
||||||
let account: MastodonAccount;
|
let account: MastodonAccount;
|
||||||
if (event) {
|
if (event) {
|
||||||
await hydrateEvents({ events: [event], relay, signal });
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
account = await renderAccount(event, { withSource: true, settingsStore });
|
account = await renderAccount(event, { withSource: true, settingsStore });
|
||||||
} else {
|
} else {
|
||||||
account = await accountFromPubkey(pubkey, { withSource: true, settingsStore });
|
account = await accountFromPubkey(pubkey, { withSource: true, settingsStore });
|
||||||
|
|
@ -394,7 +392,7 @@ const updateCredentialsController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/accounts/#follow */
|
/** https://docs.joinmastodon.org/methods/accounts/#follow */
|
||||||
const followController: AppController = async (c) => {
|
const followController: AppController = async (c) => {
|
||||||
const { user } = c.var;
|
const { relay, user } = c.var;
|
||||||
|
|
||||||
const sourcePubkey = await user!.signer.getPublicKey();
|
const sourcePubkey = await user!.signer.getPublicKey();
|
||||||
const targetPubkey = c.req.param('pubkey');
|
const targetPubkey = c.req.param('pubkey');
|
||||||
|
|
@ -405,7 +403,7 @@ const followController: AppController = async (c) => {
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
const relationship = await getRelationship(sourcePubkey, targetPubkey);
|
const relationship = await getRelationship(relay, sourcePubkey, targetPubkey);
|
||||||
relationship.following = true;
|
relationship.following = true;
|
||||||
|
|
||||||
return c.json(relationship);
|
return c.json(relationship);
|
||||||
|
|
@ -413,7 +411,7 @@ const followController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/accounts/#unfollow */
|
/** https://docs.joinmastodon.org/methods/accounts/#unfollow */
|
||||||
const unfollowController: AppController = async (c) => {
|
const unfollowController: AppController = async (c) => {
|
||||||
const { user } = c.var;
|
const { relay, user } = c.var;
|
||||||
|
|
||||||
const sourcePubkey = await user!.signer.getPublicKey();
|
const sourcePubkey = await user!.signer.getPublicKey();
|
||||||
const targetPubkey = c.req.param('pubkey');
|
const targetPubkey = c.req.param('pubkey');
|
||||||
|
|
@ -424,7 +422,7 @@ const unfollowController: AppController = async (c) => {
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
const relationship = await getRelationship(sourcePubkey, targetPubkey);
|
const relationship = await getRelationship(relay, sourcePubkey, targetPubkey);
|
||||||
return c.json(relationship);
|
return c.json(relationship);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -435,8 +433,9 @@ const followersController: AppController = (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const followingController: AppController = async (c) => {
|
const followingController: AppController = async (c) => {
|
||||||
|
const { relay, signal } = c.var;
|
||||||
const pubkey = c.req.param('pubkey');
|
const pubkey = c.req.param('pubkey');
|
||||||
const pubkeys = await getFollowedPubkeys(pubkey);
|
const pubkeys = await getFollowedPubkeys(relay, pubkey, signal);
|
||||||
return renderAccounts(c, [...pubkeys]);
|
return renderAccounts(c, [...pubkeys]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -452,7 +451,7 @@ const unblockController: AppController = (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/accounts/#mute */
|
/** https://docs.joinmastodon.org/methods/accounts/#mute */
|
||||||
const muteController: AppController = async (c) => {
|
const muteController: AppController = async (c) => {
|
||||||
const { user } = c.var;
|
const { relay, user } = c.var;
|
||||||
|
|
||||||
const sourcePubkey = await user!.signer.getPublicKey();
|
const sourcePubkey = await user!.signer.getPublicKey();
|
||||||
const targetPubkey = c.req.param('pubkey');
|
const targetPubkey = c.req.param('pubkey');
|
||||||
|
|
@ -463,13 +462,13 @@ const muteController: AppController = async (c) => {
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
const relationship = await getRelationship(sourcePubkey, targetPubkey);
|
const relationship = await getRelationship(relay, sourcePubkey, targetPubkey);
|
||||||
return c.json(relationship);
|
return c.json(relationship);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/accounts/#unmute */
|
/** https://docs.joinmastodon.org/methods/accounts/#unmute */
|
||||||
const unmuteController: AppController = async (c) => {
|
const unmuteController: AppController = async (c) => {
|
||||||
const { user } = c.var;
|
const { relay, user } = c.var;
|
||||||
|
|
||||||
const sourcePubkey = await user!.signer.getPublicKey();
|
const sourcePubkey = await user!.signer.getPublicKey();
|
||||||
const targetPubkey = c.req.param('pubkey');
|
const targetPubkey = c.req.param('pubkey');
|
||||||
|
|
@ -480,7 +479,7 @@ const unmuteController: AppController = async (c) => {
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
const relationship = await getRelationship(sourcePubkey, targetPubkey);
|
const relationship = await getRelationship(relay, sourcePubkey, targetPubkey);
|
||||||
return c.json(relationship);
|
return c.json(relationship);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -499,26 +498,26 @@ const favouritesController: AppController = async (c) => {
|
||||||
.filter((id): id is string => !!id);
|
.filter((id): id is string => !!id);
|
||||||
|
|
||||||
const events1 = await relay.query([{ kinds: [1, 20], ids }], { signal })
|
const events1 = await relay.query([{ kinds: [1, 20], ids }], { signal })
|
||||||
.then((events) => hydrateEvents({ events, relay, signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
const viewerPubkey = await user?.signer.getPublicKey();
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
|
|
||||||
const statuses = await Promise.all(
|
const statuses = await Promise.all(
|
||||||
events1.map((event) => renderStatus(event, { viewerPubkey })),
|
events1.map((event) => renderStatus(relay, event, { viewerPubkey })),
|
||||||
);
|
);
|
||||||
return paginated(c, events1, statuses);
|
return paginated(c, events1, statuses);
|
||||||
};
|
};
|
||||||
|
|
||||||
const familiarFollowersController: AppController = async (c) => {
|
const familiarFollowersController: AppController = async (c) => {
|
||||||
const { relay, user } = c.var;
|
const { relay, user, signal } = c.var;
|
||||||
|
|
||||||
const pubkey = await user!.signer.getPublicKey();
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const ids = z.array(z.string()).parse(c.req.queries('id[]'));
|
const ids = z.array(z.string()).parse(c.req.queries('id[]'));
|
||||||
const follows = await getFollowedPubkeys(pubkey);
|
const follows = await getFollowedPubkeys(relay, pubkey, signal);
|
||||||
|
|
||||||
const results = await Promise.all(ids.map(async (id) => {
|
const results = await Promise.all(ids.map(async (id) => {
|
||||||
const followLists = await relay.query([{ kinds: [3], authors: [...follows], '#p': [id] }])
|
const followLists = await relay.query([{ kinds: [3], authors: [...follows], '#p': [id] }])
|
||||||
.then((events) => hydrateEvents({ events, relay }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
const accounts = await Promise.all(
|
const accounts = await Promise.all(
|
||||||
followLists.map((event) => event.author ? renderAccount(event.author) : accountFromPubkey(event.pubkey)),
|
followLists.map((event) => event.author ? renderAccount(event.author) : accountFromPubkey(event.pubkey)),
|
||||||
|
|
@ -530,12 +529,10 @@ const familiarFollowersController: AppController = async (c) => {
|
||||||
return c.json(results);
|
return c.json(results);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getRelationship(sourcePubkey: string, targetPubkey: string) {
|
async function getRelationship(relay: NStore, sourcePubkey: string, targetPubkey: string) {
|
||||||
const db = await Storages.db();
|
|
||||||
|
|
||||||
const [sourceEvents, targetEvents] = await Promise.all([
|
const [sourceEvents, targetEvents] = await Promise.all([
|
||||||
db.query([{ kinds: [3, 10000], authors: [sourcePubkey] }]),
|
relay.query([{ kinds: [3, 10000], authors: [sourcePubkey] }]),
|
||||||
db.query([{ kinds: [3], authors: [targetPubkey] }]),
|
relay.query([{ kinds: [3], authors: [targetPubkey] }]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return renderRelationship({
|
return renderRelationship({
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { paginated } from '@ditto/mastoapi/pagination';
|
||||||
import { NostrFilter } from '@nostrify/nostrify';
|
import { NostrFilter } from '@nostrify/nostrify';
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
@ -5,7 +6,7 @@ import { z } from 'zod';
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { booleanParamSchema } from '@/schema.ts';
|
import { booleanParamSchema } from '@/schema.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { createAdminEvent, paginated, parseBody, updateEventInfo, updateUser } from '@/utils/api.ts';
|
import { createAdminEvent, parseBody, updateEventInfo, updateUser } from '@/utils/api.ts';
|
||||||
import { renderNameRequest } from '@/views/ditto.ts';
|
import { renderNameRequest } from '@/views/ditto.ts';
|
||||||
import { renderAdminAccount, renderAdminAccountFromPubkey } from '@/views/mastodon/admin-accounts.ts';
|
import { renderAdminAccount, renderAdminAccountFromPubkey } from '@/views/mastodon/admin-accounts.ts';
|
||||||
import { errorJson } from '@/utils/log.ts';
|
import { errorJson } from '@/utils/log.ts';
|
||||||
|
|
@ -59,7 +60,7 @@ const adminAccountsController: AppController = async (c) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const events = await relay.query([{ kinds: [3036], ids: [...ids] }])
|
const events = await relay.query([{ kinds: [3036], ids: [...ids] }])
|
||||||
.then((events) => hydrateEvents({ relay, events, signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
const nameRequests = await Promise.all(events.map(renderNameRequest));
|
const nameRequests = await Promise.all(events.map(renderNameRequest));
|
||||||
return paginated(c, orig, nameRequests);
|
return paginated(c, orig, nameRequests);
|
||||||
|
|
@ -97,7 +98,7 @@ const adminAccountsController: AppController = async (c) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const authors = await relay.query([{ kinds: [0], authors: [...pubkeys] }])
|
const authors = await relay.query([{ kinds: [0], authors: [...pubkeys] }])
|
||||||
.then((events) => hydrateEvents({ relay, events, signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
const accounts = await Promise.all(
|
const accounts = await Promise.all(
|
||||||
[...pubkeys].map((pubkey) => {
|
[...pubkeys].map((pubkey) => {
|
||||||
|
|
@ -116,7 +117,7 @@ const adminAccountsController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await relay.query([filter], { signal })
|
const events = await relay.query([filter], { signal })
|
||||||
.then((events) => hydrateEvents({ relay, events, signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
const accounts = await Promise.all(events.map(renderAdminAccount));
|
const accounts = await Promise.all(events.map(renderAdminAccount));
|
||||||
return paginated(c, events, accounts);
|
return paginated(c, events, accounts);
|
||||||
|
|
@ -210,7 +211,7 @@ const adminApproveController: AppController = async (c) => {
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
await updateEventInfo(eventId, { pending: false, approved: true, rejected: false }, c);
|
await updateEventInfo(eventId, { pending: false, approved: true, rejected: false }, c);
|
||||||
await hydrateEvents({ events: [event], relay });
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
|
|
||||||
const nameRequest = await renderNameRequest(event);
|
const nameRequest = await renderNameRequest(event);
|
||||||
return c.json(nameRequest);
|
return c.json(nameRequest);
|
||||||
|
|
@ -226,7 +227,7 @@ const adminRejectController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateEventInfo(eventId, { pending: false, approved: false, rejected: true }, c);
|
await updateEventInfo(eventId, { pending: false, approved: false, rejected: true }, c);
|
||||||
await hydrateEvents({ events: [event], relay });
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
|
|
||||||
const nameRequest = await renderNameRequest(event);
|
const nameRequest = await renderNameRequest(event);
|
||||||
return c.json(nameRequest);
|
return c.json(nameRequest);
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,7 @@ async function createTestRoute() {
|
||||||
const sk = generateSecretKey();
|
const sk = generateSecretKey();
|
||||||
const signer = new NSecSigner(sk);
|
const signer = new NSecSigner(sk);
|
||||||
|
|
||||||
const route = new DittoApp({ db, relay, conf });
|
const route = new DittoApp({ db: db.db, relay, conf });
|
||||||
|
|
||||||
route.use(testUserMiddleware({ signer, relay }));
|
route.use(testUserMiddleware({ signer, relay }));
|
||||||
route.route('/', cashuRoute);
|
route.route('/', cashuRoute);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { paginated, paginatedList } from '@ditto/mastoapi/pagination';
|
||||||
import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
|
@ -5,7 +6,7 @@ import { AppController } from '@/app.ts';
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { getAuthor } from '@/queries.ts';
|
import { getAuthor } from '@/queries.ts';
|
||||||
import { addTag } from '@/utils/tags.ts';
|
import { addTag } from '@/utils/tags.ts';
|
||||||
import { createEvent, paginated, parseBody, updateAdminEvent } from '@/utils/api.ts';
|
import { createEvent, parseBody, updateAdminEvent } from '@/utils/api.ts';
|
||||||
import { getInstanceMetadata } from '@/utils/instance.ts';
|
import { getInstanceMetadata } from '@/utils/instance.ts';
|
||||||
import { deleteTag } from '@/utils/tags.ts';
|
import { deleteTag } from '@/utils/tags.ts';
|
||||||
import { DittoZapSplits, getZapSplits } from '@/utils/zap-split.ts';
|
import { DittoZapSplits, getZapSplits } from '@/utils/zap-split.ts';
|
||||||
|
|
@ -15,7 +16,6 @@ import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { renderNameRequest } from '@/views/ditto.ts';
|
import { renderNameRequest } from '@/views/ditto.ts';
|
||||||
import { accountFromPubkey } from '@/views/mastodon/accounts.ts';
|
import { accountFromPubkey } from '@/views/mastodon/accounts.ts';
|
||||||
import { renderAccount } from '@/views/mastodon/accounts.ts';
|
import { renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { updateListAdminEvent } from '@/utils/api.ts';
|
import { updateListAdminEvent } from '@/utils/api.ts';
|
||||||
|
|
||||||
const markerSchema = z.enum(['read', 'write']);
|
const markerSchema = z.enum(['read', 'write']);
|
||||||
|
|
@ -120,7 +120,7 @@ export const nameRequestController: AppController = async (c) => {
|
||||||
],
|
],
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], relay });
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
|
|
||||||
const nameRequest = await renderNameRequest(event);
|
const nameRequest = await renderNameRequest(event);
|
||||||
return c.json(nameRequest);
|
return c.json(nameRequest);
|
||||||
|
|
@ -132,7 +132,7 @@ const nameRequestsSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const nameRequestsController: AppController = async (c) => {
|
export const nameRequestsController: AppController = async (c) => {
|
||||||
const { conf, relay, user, signal } = c.var;
|
const { conf, relay, user } = c.var;
|
||||||
const pubkey = await user!.signer.getPublicKey();
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
|
|
||||||
const params = c.get('pagination');
|
const params = c.get('pagination');
|
||||||
|
|
@ -168,7 +168,7 @@ export const nameRequestsController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await relay.query([{ kinds: [3036], ids: [...ids], authors: [pubkey] }])
|
const events = await relay.query([{ kinds: [3036], ids: [...ids], authors: [pubkey] }])
|
||||||
.then((events) => hydrateEvents({ relay, events: events, signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
const nameRequests = await Promise.all(
|
const nameRequests = await Promise.all(
|
||||||
events.map((event) => renderNameRequest(event)),
|
events.map((event) => renderNameRequest(event)),
|
||||||
|
|
@ -263,7 +263,7 @@ export const getZapSplitsController: AppController = async (c) => {
|
||||||
const pubkeys = Object.keys(dittoZapSplit);
|
const pubkeys = Object.keys(dittoZapSplit);
|
||||||
|
|
||||||
const zapSplits = await Promise.all(pubkeys.map(async (pubkey) => {
|
const zapSplits = await Promise.all(pubkeys.map(async (pubkey) => {
|
||||||
const author = await getAuthor(pubkey);
|
const author = await getAuthor(pubkey, c.var);
|
||||||
|
|
||||||
const account = author ? renderAccount(author) : accountFromPubkey(pubkey);
|
const account = author ? renderAccount(author) : accountFromPubkey(pubkey);
|
||||||
|
|
||||||
|
|
@ -292,7 +292,7 @@ export const statusZapSplitsController: AppController = async (c) => {
|
||||||
const pubkeys = zapsTag.map((name) => name[1]);
|
const pubkeys = zapsTag.map((name) => name[1]);
|
||||||
|
|
||||||
const users = await relay.query([{ authors: pubkeys, kinds: [0], limit: pubkeys.length }], { signal });
|
const users = await relay.query([{ authors: pubkeys, kinds: [0], limit: pubkeys.length }], { signal });
|
||||||
await hydrateEvents({ events: users, relay, signal });
|
await hydrateEvents({ ...c.var, events: users });
|
||||||
|
|
||||||
const zapSplits = (await Promise.all(pubkeys.map((pubkey) => {
|
const zapSplits = (await Promise.all(pubkeys.map((pubkey) => {
|
||||||
const author = (users.find((event) => event.pubkey === pubkey) as DittoEvent | undefined)?.author;
|
const author = (users.find((event) => event.pubkey === pubkey) as DittoEvent | undefined)?.author;
|
||||||
|
|
@ -325,7 +325,8 @@ const updateInstanceSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateInstanceController: AppController = async (c) => {
|
export const updateInstanceController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, signal } = 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);
|
||||||
const pubkey = await conf.signer.getPublicKey();
|
const pubkey = await conf.signer.getPublicKey();
|
||||||
|
|
@ -334,7 +335,7 @@ export const updateInstanceController: AppController = async (c) => {
|
||||||
return c.json(result.error, 422);
|
return c.json(result.error, 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = await getInstanceMetadata(await Storages.db(), c.req.raw.signal);
|
const meta = await getInstanceMetadata(relay, signal);
|
||||||
|
|
||||||
await updateAdminEvent(
|
await updateAdminEvent(
|
||||||
{ kinds: [0], authors: [pubkey], limit: 1 },
|
{ kinds: [0], authors: [pubkey], limit: 1 },
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import denoJson from 'deno.json' with { type: 'json' };
|
import denoJson from 'deno.json' with { type: 'json' };
|
||||||
|
|
||||||
import { AppController } from '@/app.ts';
|
import { AppController } from '@/app.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { getInstanceMetadata } from '@/utils/instance.ts';
|
import { getInstanceMetadata } from '@/utils/instance.ts';
|
||||||
|
|
||||||
const version = `3.0.0 (compatible; Ditto ${denoJson.version})`;
|
const version = `3.0.0 (compatible; Ditto ${denoJson.version})`;
|
||||||
|
|
@ -16,9 +15,9 @@ const features = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const instanceV1Controller: AppController = async (c) => {
|
const instanceV1Controller: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, signal } = c.var;
|
||||||
const { host, protocol } = conf.url;
|
const { host, protocol } = conf.url;
|
||||||
const meta = await getInstanceMetadata(await Storages.db(), c.req.raw.signal);
|
const meta = await getInstanceMetadata(relay, signal);
|
||||||
|
|
||||||
/** 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:';
|
||||||
|
|
@ -76,9 +75,9 @@ const instanceV1Controller: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const instanceV2Controller: AppController = async (c) => {
|
const instanceV2Controller: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, signal } = c.var;
|
||||||
const { host, protocol } = conf.url;
|
const { host, protocol } = conf.url;
|
||||||
const meta = await getInstanceMetadata(await Storages.db(), c.req.raw.signal);
|
const meta = await getInstanceMetadata(relay, signal);
|
||||||
|
|
||||||
/** 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:';
|
||||||
|
|
@ -165,7 +164,9 @@ const instanceV2Controller: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const instanceDescriptionController: AppController = async (c) => {
|
const instanceDescriptionController: AppController = async (c) => {
|
||||||
const meta = await getInstanceMetadata(await Storages.db(), c.req.raw.signal);
|
const { relay, signal } = c.var;
|
||||||
|
|
||||||
|
const meta = await getInstanceMetadata(relay, signal);
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
content: meta.about,
|
content: meta.about,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
|
import { paginated } from '@ditto/mastoapi/pagination';
|
||||||
import { NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
import { NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { AppContext, AppController } from '@/app.ts';
|
import { AppContext, AppController } from '@/app.ts';
|
||||||
import { DittoPagination } from '@/interfaces/DittoPagination.ts';
|
import { DittoPagination } from '@/interfaces/DittoPagination.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { paginated } from '@/utils/api.ts';
|
|
||||||
import { renderNotification } from '@/views/mastodon/notifications.ts';
|
import { renderNotification } from '@/views/mastodon/notifications.ts';
|
||||||
|
|
||||||
/** Set of known notification types across backends. */
|
/** Set of known notification types across backends. */
|
||||||
|
|
@ -90,9 +90,9 @@ const notificationController: AppController = async (c) => {
|
||||||
return c.json({ error: 'Event not found' }, { status: 404 });
|
return c.json({ error: 'Event not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], relay });
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
|
|
||||||
const notification = await renderNotification(event, { viewerPubkey: pubkey });
|
const notification = await renderNotification(relay, event, { viewerPubkey: pubkey });
|
||||||
|
|
||||||
if (!notification) {
|
if (!notification) {
|
||||||
return c.json({ error: 'Notification not found' }, { status: 404 });
|
return c.json({ error: 'Notification not found' }, { status: 404 });
|
||||||
|
|
@ -116,14 +116,14 @@ async function renderNotifications(
|
||||||
const events = await relay
|
const events = await relay
|
||||||
.query(filters, opts)
|
.query(filters, opts)
|
||||||
.then((events) => events.filter((event) => event.pubkey !== pubkey))
|
.then((events) => events.filter((event) => event.pubkey !== pubkey))
|
||||||
.then((events) => hydrateEvents({ events, relay, signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifications = (await Promise.all(events.map((event) => {
|
const notifications = (await Promise.all(events.map((event) => {
|
||||||
return renderNotification(event, { viewerPubkey: pubkey });
|
return renderNotification(relay, event, { viewerPubkey: pubkey });
|
||||||
})))
|
})))
|
||||||
.filter((notification) => notification && types.has(notification.type));
|
.filter((notification) => notification && types.has(notification.type));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ import { escape } from 'entities';
|
||||||
import { generateSecretKey } from 'nostr-tools';
|
import { generateSecretKey } from 'nostr-tools';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { AppController } from '@/app.ts';
|
import { AppContext, AppController } from '@/app.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { nostrNow } from '@/utils.ts';
|
import { nostrNow } from '@/utils.ts';
|
||||||
import { parseBody } from '@/utils/api.ts';
|
import { parseBody } from '@/utils/api.ts';
|
||||||
import { aesEncrypt } from '@/utils/aes.ts';
|
import { aesEncrypt } from '@/utils/aes.ts';
|
||||||
|
|
@ -40,6 +39,7 @@ const createTokenSchema = z.discriminatedUnion('grant_type', [
|
||||||
|
|
||||||
const createTokenController: AppController = async (c) => {
|
const createTokenController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf } = c.var;
|
||||||
|
|
||||||
const body = await parseBody(c.req.raw);
|
const body = await parseBody(c.req.raw);
|
||||||
const result = createTokenSchema.safeParse(body);
|
const result = createTokenSchema.safeParse(body);
|
||||||
|
|
||||||
|
|
@ -50,7 +50,7 @@ const createTokenController: AppController = async (c) => {
|
||||||
switch (result.data.grant_type) {
|
switch (result.data.grant_type) {
|
||||||
case 'nostr_bunker':
|
case 'nostr_bunker':
|
||||||
return c.json({
|
return c.json({
|
||||||
access_token: await getToken(result.data, conf.seckey),
|
access_token: await getToken(c, result.data, conf.seckey),
|
||||||
token_type: 'Bearer',
|
token_type: 'Bearer',
|
||||||
scope: 'read write follow push',
|
scope: 'read write follow push',
|
||||||
created_at: nostrNow(),
|
created_at: nostrNow(),
|
||||||
|
|
@ -90,6 +90,8 @@ const revokeTokenSchema = z.object({
|
||||||
* https://docs.joinmastodon.org/methods/oauth/#revoke
|
* https://docs.joinmastodon.org/methods/oauth/#revoke
|
||||||
*/
|
*/
|
||||||
const revokeTokenController: AppController = async (c) => {
|
const revokeTokenController: AppController = async (c) => {
|
||||||
|
const { db } = c.var;
|
||||||
|
|
||||||
const body = await parseBody(c.req.raw);
|
const body = await parseBody(c.req.raw);
|
||||||
const result = revokeTokenSchema.safeParse(body);
|
const result = revokeTokenSchema.safeParse(body);
|
||||||
|
|
||||||
|
|
@ -99,10 +101,9 @@ const revokeTokenController: AppController = async (c) => {
|
||||||
|
|
||||||
const { token } = result.data;
|
const { token } = result.data;
|
||||||
|
|
||||||
const kysely = await Storages.kysely();
|
|
||||||
const tokenHash = await getTokenHash(token as `token1${string}`);
|
const tokenHash = await getTokenHash(token as `token1${string}`);
|
||||||
|
|
||||||
await kysely
|
await db.kysely
|
||||||
.deleteFrom('auth_tokens')
|
.deleteFrom('auth_tokens')
|
||||||
.where('token_hash', '=', tokenHash)
|
.where('token_hash', '=', tokenHash)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
@ -111,10 +112,11 @@ const revokeTokenController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getToken(
|
async function getToken(
|
||||||
|
c: AppContext,
|
||||||
{ pubkey: bunkerPubkey, secret, relays = [] }: { pubkey: string; secret?: string; relays?: string[] },
|
{ pubkey: bunkerPubkey, secret, relays = [] }: { pubkey: string; secret?: string; relays?: string[] },
|
||||||
dittoSeckey: Uint8Array,
|
dittoSeckey: Uint8Array,
|
||||||
): Promise<`token1${string}`> {
|
): Promise<`token1${string}`> {
|
||||||
const kysely = await Storages.kysely();
|
const { db, relay } = c.var;
|
||||||
const { token, hash } = await generateToken();
|
const { token, hash } = await generateToken();
|
||||||
|
|
||||||
const nip46Seckey = generateSecretKey();
|
const nip46Seckey = generateSecretKey();
|
||||||
|
|
@ -123,14 +125,14 @@ async function getToken(
|
||||||
encryption: 'nip44',
|
encryption: 'nip44',
|
||||||
pubkey: bunkerPubkey,
|
pubkey: bunkerPubkey,
|
||||||
signer: new NSecSigner(nip46Seckey),
|
signer: new NSecSigner(nip46Seckey),
|
||||||
relay: await Storages.db(), // TODO: Use the relays from the request.
|
relay,
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await signer.connect(secret);
|
await signer.connect(secret);
|
||||||
const userPubkey = await signer.getPublicKey();
|
const userPubkey = await signer.getPublicKey();
|
||||||
|
|
||||||
await kysely.insertInto('auth_tokens').values({
|
await db.kysely.insertInto('auth_tokens').values({
|
||||||
token_hash: hash,
|
token_hash: hash,
|
||||||
pubkey: userPubkey,
|
pubkey: userPubkey,
|
||||||
bunker_pubkey: bunkerPubkey,
|
bunker_pubkey: bunkerPubkey,
|
||||||
|
|
@ -236,7 +238,7 @@ const oauthAuthorizeController: AppController = async (c) => {
|
||||||
|
|
||||||
const bunker = new URL(bunker_uri);
|
const bunker = new URL(bunker_uri);
|
||||||
|
|
||||||
const token = await getToken({
|
const token = await getToken(c, {
|
||||||
pubkey: bunker.hostname,
|
pubkey: bunker.hostname,
|
||||||
secret: bunker.searchParams.get('secret') || undefined,
|
secret: bunker.searchParams.get('secret') || undefined,
|
||||||
relays: bunker.searchParams.getAll('relay'),
|
relays: bunker.searchParams.getAll('relay'),
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ const pleromaAdminTagController: AppController = async (c) => {
|
||||||
const params = pleromaAdminTagSchema.parse(await c.req.json());
|
const params = pleromaAdminTagSchema.parse(await c.req.json());
|
||||||
|
|
||||||
for (const nickname of params.nicknames) {
|
for (const nickname of params.nicknames) {
|
||||||
const pubkey = await lookupPubkey(nickname);
|
const pubkey = await lookupPubkey(nickname, c.var);
|
||||||
if (!pubkey) continue;
|
if (!pubkey) continue;
|
||||||
|
|
||||||
await updateAdminEvent(
|
await updateAdminEvent(
|
||||||
|
|
@ -104,7 +104,7 @@ const pleromaAdminUntagController: AppController = async (c) => {
|
||||||
const params = pleromaAdminTagSchema.parse(await c.req.json());
|
const params = pleromaAdminTagSchema.parse(await c.req.json());
|
||||||
|
|
||||||
for (const nickname of params.nicknames) {
|
for (const nickname of params.nicknames) {
|
||||||
const pubkey = await lookupPubkey(nickname);
|
const pubkey = await lookupPubkey(nickname, c.var);
|
||||||
if (!pubkey) continue;
|
if (!pubkey) continue;
|
||||||
|
|
||||||
await updateAdminEvent(
|
await updateAdminEvent(
|
||||||
|
|
@ -130,7 +130,7 @@ const pleromaAdminSuggestController: AppController = async (c) => {
|
||||||
const { nicknames } = pleromaAdminSuggestSchema.parse(await c.req.json());
|
const { nicknames } = pleromaAdminSuggestSchema.parse(await c.req.json());
|
||||||
|
|
||||||
for (const nickname of nicknames) {
|
for (const nickname of nicknames) {
|
||||||
const pubkey = await lookupPubkey(nickname);
|
const pubkey = await lookupPubkey(nickname, c.var);
|
||||||
if (!pubkey) continue;
|
if (!pubkey) continue;
|
||||||
await updateUser(pubkey, { suggested: true }, c);
|
await updateUser(pubkey, { suggested: true }, c);
|
||||||
}
|
}
|
||||||
|
|
@ -142,7 +142,7 @@ const pleromaAdminUnsuggestController: AppController = async (c) => {
|
||||||
const { nicknames } = pleromaAdminSuggestSchema.parse(await c.req.json());
|
const { nicknames } = pleromaAdminSuggestSchema.parse(await c.req.json());
|
||||||
|
|
||||||
for (const nickname of nicknames) {
|
for (const nickname of nicknames) {
|
||||||
const pubkey = await lookupPubkey(nickname);
|
const pubkey = await lookupPubkey(nickname, c.var);
|
||||||
if (!pubkey) continue;
|
if (!pubkey) continue;
|
||||||
await updateUser(pubkey, { suggested: false }, c);
|
await updateUser(pubkey, { suggested: false }, c);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { nip19 } from 'nostr-tools';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { AppController } from '@/app.ts';
|
import { AppController } from '@/app.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { parseBody } from '@/utils/api.ts';
|
import { parseBody } from '@/utils/api.ts';
|
||||||
import { getTokenHash } from '@/utils/auth.ts';
|
import { getTokenHash } from '@/utils/auth.ts';
|
||||||
|
|
||||||
|
|
@ -42,7 +41,7 @@ const pushSubscribeSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const pushSubscribeController: AppController = async (c) => {
|
export const pushSubscribeController: AppController = async (c) => {
|
||||||
const { conf, user } = c.var;
|
const { conf, db, user } = c.var;
|
||||||
const vapidPublicKey = await conf.vapidPublicKey;
|
const vapidPublicKey = await conf.vapidPublicKey;
|
||||||
|
|
||||||
if (!vapidPublicKey) {
|
if (!vapidPublicKey) {
|
||||||
|
|
@ -50,8 +49,6 @@ export const pushSubscribeController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = getAccessToken(c.req.raw);
|
const accessToken = getAccessToken(c.req.raw);
|
||||||
|
|
||||||
const kysely = await Storages.kysely();
|
|
||||||
const signer = user!.signer;
|
const signer = user!.signer;
|
||||||
|
|
||||||
const result = pushSubscribeSchema.safeParse(await parseBody(c.req.raw));
|
const result = pushSubscribeSchema.safeParse(await parseBody(c.req.raw));
|
||||||
|
|
@ -65,7 +62,7 @@ export const pushSubscribeController: AppController = async (c) => {
|
||||||
const pubkey = await signer.getPublicKey();
|
const pubkey = await signer.getPublicKey();
|
||||||
const tokenHash = await getTokenHash(accessToken);
|
const tokenHash = await getTokenHash(accessToken);
|
||||||
|
|
||||||
const { id } = await kysely.transaction().execute(async (trx) => {
|
const { id } = await db.kysely.transaction().execute(async (trx) => {
|
||||||
await trx
|
await trx
|
||||||
.deleteFrom('push_subscriptions')
|
.deleteFrom('push_subscriptions')
|
||||||
.where('token_hash', '=', tokenHash)
|
.where('token_hash', '=', tokenHash)
|
||||||
|
|
@ -97,7 +94,7 @@ export const pushSubscribeController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSubscriptionController: AppController = async (c) => {
|
export const getSubscriptionController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, db } = c.var;
|
||||||
const vapidPublicKey = await conf.vapidPublicKey;
|
const vapidPublicKey = await conf.vapidPublicKey;
|
||||||
|
|
||||||
if (!vapidPublicKey) {
|
if (!vapidPublicKey) {
|
||||||
|
|
@ -106,10 +103,9 @@ export const getSubscriptionController: AppController = async (c) => {
|
||||||
|
|
||||||
const accessToken = getAccessToken(c.req.raw);
|
const accessToken = getAccessToken(c.req.raw);
|
||||||
|
|
||||||
const kysely = await Storages.kysely();
|
|
||||||
const tokenHash = await getTokenHash(accessToken);
|
const tokenHash = await getTokenHash(accessToken);
|
||||||
|
|
||||||
const row = await kysely
|
const row = await db.kysely
|
||||||
.selectFrom('push_subscriptions')
|
.selectFrom('push_subscriptions')
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where('token_hash', '=', tokenHash)
|
.where('token_hash', '=', tokenHash)
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@ const reactionController: AppController = async (c) => {
|
||||||
tags: [['e', id], ['p', event.pubkey]],
|
tags: [['e', id], ['p', event.pubkey]],
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], relay });
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
|
|
||||||
const status = await renderStatus(event, { viewerPubkey: await user!.signer.getPublicKey() });
|
const status = await renderStatus(relay, event, { viewerPubkey: await user!.signer.getPublicKey() });
|
||||||
|
|
||||||
return c.json(status);
|
return c.json(status);
|
||||||
};
|
};
|
||||||
|
|
@ -76,7 +76,7 @@ const deleteReactionController: AppController = async (c) => {
|
||||||
tags,
|
tags,
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
const status = renderStatus(event, { viewerPubkey: pubkey });
|
const status = renderStatus(relay, event, { viewerPubkey: pubkey });
|
||||||
|
|
||||||
return c.json(status);
|
return c.json(status);
|
||||||
};
|
};
|
||||||
|
|
@ -99,7 +99,7 @@ const reactionsController: AppController = async (c) => {
|
||||||
const events = await relay.query([{ kinds: [7], '#e': [id], limit: 100 }])
|
const events = await relay.query([{ kinds: [7], '#e': [id], limit: 100 }])
|
||||||
.then((events) => events.filter(({ content }) => /^\p{RGI_Emoji}$/v.test(content)))
|
.then((events) => events.filter(({ content }) => /^\p{RGI_Emoji}$/v.test(content)))
|
||||||
.then((events) => events.filter((event) => !emoji || event.content === emoji))
|
.then((events) => events.filter((event) => !emoji || event.content === emoji))
|
||||||
.then((events) => hydrateEvents({ events, relay }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
/** Events grouped by emoji. */
|
/** Events grouped by emoji. */
|
||||||
const byEmoji = events.reduce((acc, event) => {
|
const byEmoji = events.reduce((acc, event) => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
|
import { paginated } from '@ditto/mastoapi/pagination';
|
||||||
import { NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
import { NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { createEvent, paginated, parseBody, updateEventInfo } from '@/utils/api.ts';
|
import { createEvent, parseBody, updateEventInfo } from '@/utils/api.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { renderAdminReport } from '@/views/mastodon/reports.ts';
|
import { renderAdminReport } from '@/views/mastodon/reports.ts';
|
||||||
import { renderReport } from '@/views/mastodon/reports.ts';
|
import { renderReport } from '@/views/mastodon/reports.ts';
|
||||||
|
|
@ -18,7 +19,7 @@ const reportSchema = z.object({
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/reports/#post */
|
/** https://docs.joinmastodon.org/methods/reports/#post */
|
||||||
const reportController: AppController = async (c) => {
|
const reportController: 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 = reportSchema.safeParse(body);
|
const result = reportSchema.safeParse(body);
|
||||||
|
|
@ -49,7 +50,7 @@ const reportController: AppController = async (c) => {
|
||||||
tags,
|
tags,
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], relay });
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
return c.json(await renderReport(event));
|
return c.json(await renderReport(event));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -94,10 +95,10 @@ const adminReportsController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await relay.query([{ kinds: [1984], ids: [...ids] }])
|
const events = await relay.query([{ kinds: [1984], ids: [...ids] }])
|
||||||
.then((events) => hydrateEvents({ relay, events: events, signal: c.req.raw.signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
const reports = await Promise.all(
|
const reports = await Promise.all(
|
||||||
events.map((event) => renderAdminReport(event, { viewerPubkey })),
|
events.map((event) => renderAdminReport(relay, event, { viewerPubkey })),
|
||||||
);
|
);
|
||||||
|
|
||||||
return paginated(c, orig, reports);
|
return paginated(c, orig, reports);
|
||||||
|
|
@ -120,9 +121,9 @@ const adminReportController: AppController = async (c) => {
|
||||||
return c.json({ error: 'Not found' }, 404);
|
return c.json({ error: 'Not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], relay, signal });
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
|
|
||||||
const report = await renderAdminReport(event, { viewerPubkey: pubkey });
|
const report = await renderAdminReport(relay, event, { viewerPubkey: pubkey });
|
||||||
return c.json(report);
|
return c.json(report);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -144,9 +145,9 @@ const adminReportResolveController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateEventInfo(eventId, { open: false, closed: true }, c);
|
await updateEventInfo(eventId, { open: false, closed: true }, c);
|
||||||
await hydrateEvents({ events: [event], relay, signal });
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
|
|
||||||
const report = await renderAdminReport(event, { viewerPubkey: pubkey });
|
const report = await renderAdminReport(relay, event, { viewerPubkey: pubkey });
|
||||||
return c.json(report);
|
return c.json(report);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -167,9 +168,9 @@ const adminReportReopenController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateEventInfo(eventId, { open: true, closed: false }, c);
|
await updateEventInfo(eventId, { open: true, closed: false }, c);
|
||||||
await hydrateEvents({ events: [event], relay, signal });
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
|
|
||||||
const report = await renderAdminReport(event, { viewerPubkey: pubkey });
|
const report = await renderAdminReport(relay, event, { viewerPubkey: pubkey });
|
||||||
return c.json(report);
|
return c.json(report);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
|
import { paginated, paginatedList } from '@ditto/mastoapi/pagination';
|
||||||
import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { AppController } from '@/app.ts';
|
import { AppContext, AppController } from '@/app.ts';
|
||||||
import { booleanParamSchema } from '@/schema.ts';
|
import { booleanParamSchema } from '@/schema.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { extractIdentifier, lookupPubkey } from '@/utils/lookup.ts';
|
import { extractIdentifier, lookupPubkey } from '@/utils/lookup.ts';
|
||||||
import { nip05Cache } from '@/utils/nip05.ts';
|
import { lookupNip05 } from '@/utils/nip05.ts';
|
||||||
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
import { getFollowedPubkeys } from '@/queries.ts';
|
import { getFollowedPubkeys } from '@/queries.ts';
|
||||||
import { getPubkeysBySearch } from '@/utils/search.ts';
|
import { getPubkeysBySearch } from '@/utils/search.ts';
|
||||||
import { paginated, paginatedList } from '@/utils/api.ts';
|
|
||||||
|
|
||||||
const searchQuerySchema = z.object({
|
const searchQuerySchema = z.object({
|
||||||
q: z.string().transform(decodeURIComponent),
|
q: z.string().transform(decodeURIComponent),
|
||||||
|
|
@ -26,7 +25,7 @@ const searchQuerySchema = z.object({
|
||||||
type SearchQuery = z.infer<typeof searchQuerySchema> & { since?: number; until?: number; limit: number };
|
type SearchQuery = z.infer<typeof searchQuerySchema> & { since?: number; until?: number; limit: number };
|
||||||
|
|
||||||
const searchController: AppController = async (c) => {
|
const searchController: AppController = async (c) => {
|
||||||
const { user, pagination, signal } = c.var;
|
const { relay, user, pagination, signal } = c.var;
|
||||||
|
|
||||||
const result = searchQuerySchema.safeParse(c.req.query());
|
const result = searchQuerySchema.safeParse(c.req.query());
|
||||||
const viewerPubkey = await user?.signer.getPublicKey();
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
|
|
@ -35,12 +34,12 @@ const searchController: AppController = async (c) => {
|
||||||
return c.json({ error: 'Bad request', schema: result.error }, 422);
|
return c.json({ error: 'Bad request', schema: result.error }, 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = await lookupEvent({ ...result.data, ...pagination }, signal);
|
const event = await lookupEvent(c, { ...result.data, ...pagination });
|
||||||
const lookup = extractIdentifier(result.data.q);
|
const lookup = extractIdentifier(result.data.q);
|
||||||
|
|
||||||
// Render account from pubkey.
|
// Render account from pubkey.
|
||||||
if (!event && lookup) {
|
if (!event && lookup) {
|
||||||
const pubkey = await lookupPubkey(lookup);
|
const pubkey = await lookupPubkey(lookup, c.var);
|
||||||
return c.json({
|
return c.json({
|
||||||
accounts: pubkey ? [accountFromPubkey(pubkey)] : [],
|
accounts: pubkey ? [accountFromPubkey(pubkey)] : [],
|
||||||
statuses: [],
|
statuses: [],
|
||||||
|
|
@ -54,7 +53,7 @@ const searchController: AppController = async (c) => {
|
||||||
events = [event];
|
events = [event];
|
||||||
}
|
}
|
||||||
|
|
||||||
events.push(...(await searchEvents({ ...result.data, ...pagination, viewerPubkey }, signal)));
|
events.push(...(await searchEvents(c, { ...result.data, ...pagination, viewerPubkey }, signal)));
|
||||||
|
|
||||||
const [accounts, statuses] = await Promise.all([
|
const [accounts, statuses] = await Promise.all([
|
||||||
Promise.all(
|
Promise.all(
|
||||||
|
|
@ -66,7 +65,7 @@ const searchController: AppController = async (c) => {
|
||||||
Promise.all(
|
Promise.all(
|
||||||
events
|
events
|
||||||
.filter((event) => event.kind === 1)
|
.filter((event) => event.kind === 1)
|
||||||
.map((event) => renderStatus(event, { viewerPubkey }))
|
.map((event) => renderStatus(relay, event, { viewerPubkey }))
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
@ -86,16 +85,17 @@ const searchController: AppController = async (c) => {
|
||||||
|
|
||||||
/** Get events for the search params. */
|
/** Get events for the search params. */
|
||||||
async function searchEvents(
|
async function searchEvents(
|
||||||
|
c: AppContext,
|
||||||
{ q, type, since, until, limit, offset, account_id, viewerPubkey }: SearchQuery & { viewerPubkey?: string },
|
{ q, type, since, until, limit, offset, account_id, viewerPubkey }: SearchQuery & { viewerPubkey?: string },
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
): Promise<NostrEvent[]> {
|
): Promise<NostrEvent[]> {
|
||||||
|
const { relay, db } = c.var;
|
||||||
|
|
||||||
// Hashtag search is not supported.
|
// Hashtag search is not supported.
|
||||||
if (type === 'hashtags') {
|
if (type === 'hashtags') {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relay = await Storages.db();
|
|
||||||
|
|
||||||
const filter: NostrFilter = {
|
const filter: NostrFilter = {
|
||||||
kinds: typeToKinds(type),
|
kinds: typeToKinds(type),
|
||||||
search: q,
|
search: q,
|
||||||
|
|
@ -104,12 +104,10 @@ async function searchEvents(
|
||||||
limit,
|
limit,
|
||||||
};
|
};
|
||||||
|
|
||||||
const kysely = await Storages.kysely();
|
|
||||||
|
|
||||||
// For account search, use a special index, and prioritize followed accounts.
|
// For account search, use a special index, and prioritize followed accounts.
|
||||||
if (type === 'accounts') {
|
if (type === 'accounts') {
|
||||||
const following = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set<string>();
|
const following = viewerPubkey ? await getFollowedPubkeys(relay, viewerPubkey) : new Set<string>();
|
||||||
const searchPubkeys = await getPubkeysBySearch(kysely, { q, limit, offset, following });
|
const searchPubkeys = await getPubkeysBySearch(db.kysely, { q, limit, offset, following });
|
||||||
|
|
||||||
filter.authors = [...searchPubkeys];
|
filter.authors = [...searchPubkeys];
|
||||||
filter.search = undefined;
|
filter.search = undefined;
|
||||||
|
|
@ -123,7 +121,7 @@ async function searchEvents(
|
||||||
// Query the events.
|
// Query the events.
|
||||||
let events = await relay
|
let events = await relay
|
||||||
.query([filter], { signal })
|
.query([filter], { signal })
|
||||||
.then((events) => hydrateEvents({ events, relay, signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
// When using an authors filter, return the events in the same order as the filter.
|
// When using an authors filter, return the events in the same order as the filter.
|
||||||
if (filter.authors) {
|
if (filter.authors) {
|
||||||
|
|
@ -148,17 +146,17 @@ function typeToKinds(type: SearchQuery['type']): number[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve a searched value into an event, if applicable. */
|
/** Resolve a searched value into an event, if applicable. */
|
||||||
async function lookupEvent(query: SearchQuery, signal: AbortSignal): Promise<NostrEvent | undefined> {
|
async function lookupEvent(c: AppContext, query: SearchQuery): Promise<NostrEvent | undefined> {
|
||||||
const filters = await getLookupFilters(query, signal);
|
const { relay, signal } = c.var;
|
||||||
const relay = await Storages.db();
|
const filters = await getLookupFilters(c, query);
|
||||||
|
|
||||||
return relay.query(filters, { limit: 1, signal })
|
return relay.query(filters, { signal })
|
||||||
.then((events) => hydrateEvents({ events, relay, signal }))
|
.then((events) => hydrateEvents({ ...c.var, events }))
|
||||||
.then(([event]) => event);
|
.then(([event]) => event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get filters to lookup the input value. */
|
/** Get filters to lookup the input value. */
|
||||||
async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: AbortSignal): Promise<NostrFilter[]> {
|
async function getLookupFilters(c: AppContext, { q, type, resolve }: SearchQuery): Promise<NostrFilter[]> {
|
||||||
const accounts = !type || type === 'accounts';
|
const accounts = !type || type === 'accounts';
|
||||||
const statuses = !type || type === 'statuses';
|
const statuses = !type || type === 'statuses';
|
||||||
|
|
||||||
|
|
@ -199,7 +197,7 @@ async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: Abort
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { pubkey } = await nip05Cache.fetch(lookup, { signal });
|
const { pubkey } = await lookupNip05(lookup, c.var);
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
return [{ kinds: [0], authors: [pubkey] }];
|
return [{ kinds: [0], authors: [pubkey] }];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { HTTPException } from '@hono/hono/http-exception';
|
import { HTTPException } from '@hono/hono/http-exception';
|
||||||
|
import { paginated, paginatedList } from '@ditto/mastoapi/pagination';
|
||||||
import { NostrEvent, NSchema as n } from '@nostrify/nostrify';
|
import { NostrEvent, NSchema as n } from '@nostrify/nostrify';
|
||||||
import 'linkify-plugin-hashtag';
|
import 'linkify-plugin-hashtag';
|
||||||
import linkify from 'linkifyjs';
|
import linkify from 'linkifyjs';
|
||||||
|
|
@ -15,7 +16,7 @@ import { asyncReplaceAll } from '@/utils/text.ts';
|
||||||
import { lookupPubkey } from '@/utils/lookup.ts';
|
import { lookupPubkey } from '@/utils/lookup.ts';
|
||||||
import { languageSchema } from '@/schema.ts';
|
import { languageSchema } from '@/schema.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { assertAuthenticated, createEvent, paginated, paginatedList, parseBody, updateListEvent } from '@/utils/api.ts';
|
import { assertAuthenticated, createEvent, parseBody, updateListEvent } from '@/utils/api.ts';
|
||||||
import { getInvoice, getLnurl } from '@/utils/lnurl.ts';
|
import { getInvoice, getLnurl } from '@/utils/lnurl.ts';
|
||||||
import { purifyEvent } from '@/utils/purify.ts';
|
import { purifyEvent } from '@/utils/purify.ts';
|
||||||
import { getZapSplits } from '@/utils/zap-split.ts';
|
import { getZapSplits } from '@/utils/zap-split.ts';
|
||||||
|
|
@ -46,10 +47,10 @@ const createStatusSchema = z.object({
|
||||||
);
|
);
|
||||||
|
|
||||||
const statusController: AppController = async (c) => {
|
const statusController: AppController = async (c) => {
|
||||||
const { user, signal } = c.var;
|
const { relay, user } = c.var;
|
||||||
|
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const event = await getEvent(id, { signal });
|
const event = await getEvent(id, c.var);
|
||||||
|
|
||||||
if (event?.author) {
|
if (event?.author) {
|
||||||
assertAuthenticated(c, event.author);
|
assertAuthenticated(c, event.author);
|
||||||
|
|
@ -57,7 +58,7 @@ const statusController: AppController = async (c) => {
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
const viewerPubkey = await user?.signer.getPublicKey();
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
const status = await renderStatus(event, { viewerPubkey });
|
const status = await renderStatus(relay, event, { viewerPubkey });
|
||||||
return c.json(status);
|
return c.json(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,7 +66,7 @@ const statusController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createStatusController: AppController = async (c) => {
|
const createStatusController: AppController = async (c) => {
|
||||||
const { conf, relay, user, signal } = c.var;
|
const { conf, relay, user } = c.var;
|
||||||
|
|
||||||
const body = await parseBody(c.req.raw);
|
const body = await parseBody(c.req.raw);
|
||||||
const result = createStatusSchema.safeParse(body);
|
const result = createStatusSchema.safeParse(body);
|
||||||
|
|
@ -153,7 +154,7 @@ const createStatusController: AppController = async (c) => {
|
||||||
data.status ?? '',
|
data.status ?? '',
|
||||||
/(?<![\w/])@([\w@+._-]+)(?![\w/\.])/g,
|
/(?<![\w/])@([\w@+._-]+)(?![\w/\.])/g,
|
||||||
async (match, username) => {
|
async (match, username) => {
|
||||||
const pubkey = await lookupPubkey(username);
|
const pubkey = await lookupPubkey(username, c.var);
|
||||||
if (!pubkey) return match;
|
if (!pubkey) return match;
|
||||||
|
|
||||||
// Content addressing (default)
|
// Content addressing (default)
|
||||||
|
|
@ -171,7 +172,7 @@ const createStatusController: AppController = async (c) => {
|
||||||
|
|
||||||
// Explicit addressing
|
// Explicit addressing
|
||||||
for (const to of data.to ?? []) {
|
for (const to of data.to ?? []) {
|
||||||
const pubkey = await lookupPubkey(to);
|
const pubkey = await lookupPubkey(to, c.var);
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
pubkeys.add(pubkey);
|
pubkeys.add(pubkey);
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +192,7 @@ const createStatusController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const pubkey = await user!.signer.getPublicKey();
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const author = pubkey ? await getAuthor(pubkey) : undefined;
|
const author = pubkey ? await getAuthor(pubkey, c.var) : undefined;
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -254,22 +255,18 @@ const createStatusController: AppController = async (c) => {
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
if (data.quote_id) {
|
if (data.quote_id) {
|
||||||
await hydrateEvents({
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
events: [event],
|
|
||||||
relay,
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json(await renderStatus({ ...event, author }, { viewerPubkey: author?.pubkey }));
|
return c.json(await renderStatus(relay, { ...event, author }, { viewerPubkey: author?.pubkey }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteStatusController: AppController = async (c) => {
|
const deleteStatusController: AppController = async (c) => {
|
||||||
const { conf, user, signal } = c.var;
|
const { conf, relay, user } = c.var;
|
||||||
|
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const pubkey = await user?.signer.getPublicKey();
|
const pubkey = await user?.signer.getPublicKey();
|
||||||
const event = await getEvent(id, { signal });
|
const event = await getEvent(id, c.var);
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
if (event.pubkey === pubkey) {
|
if (event.pubkey === pubkey) {
|
||||||
|
|
@ -278,8 +275,8 @@ const deleteStatusController: AppController = async (c) => {
|
||||||
tags: [['e', id, conf.relay, '', pubkey]],
|
tags: [['e', id, conf.relay, '', pubkey]],
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
const author = await getAuthor(event.pubkey);
|
const author = await getAuthor(event.pubkey, c.var);
|
||||||
return c.json(await renderStatus({ ...event, author }, { viewerPubkey: pubkey }));
|
return c.json(await renderStatus(relay, { ...event, author }, { viewerPubkey: pubkey }));
|
||||||
} else {
|
} else {
|
||||||
return c.json({ error: 'Unauthorized' }, 403);
|
return c.json({ error: 'Unauthorized' }, 403);
|
||||||
}
|
}
|
||||||
|
|
@ -297,7 +294,7 @@ const contextController: AppController = async (c) => {
|
||||||
|
|
||||||
async function renderStatuses(events: NostrEvent[]) {
|
async function renderStatuses(events: NostrEvent[]) {
|
||||||
const statuses = await Promise.all(
|
const statuses = await Promise.all(
|
||||||
events.map((event) => renderStatus(event, { viewerPubkey })),
|
events.map((event) => renderStatus(relay, event, { viewerPubkey })),
|
||||||
);
|
);
|
||||||
return statuses.filter(Boolean);
|
return statuses.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
@ -308,11 +305,7 @@ const contextController: AppController = async (c) => {
|
||||||
getDescendants(relay, event),
|
getDescendants(relay, event),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({ ...c.var, events: [...ancestorEvents, ...descendantEvents] });
|
||||||
events: [...ancestorEvents, ...descendantEvents],
|
|
||||||
signal: c.req.raw.signal,
|
|
||||||
relay,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [ancestors, descendants] = await Promise.all([
|
const [ancestors, descendants] = await Promise.all([
|
||||||
renderStatuses(ancestorEvents),
|
renderStatuses(ancestorEvents),
|
||||||
|
|
@ -341,9 +334,9 @@ const favouriteController: AppController = async (c) => {
|
||||||
],
|
],
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
await hydrateEvents({ events: [target], relay });
|
await hydrateEvents({ ...c.var, events: [target] });
|
||||||
|
|
||||||
const status = await renderStatus(target, { viewerPubkey: await user?.signer.getPublicKey() });
|
const status = await renderStatus(relay, target, { viewerPubkey: await user?.signer.getPublicKey() });
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
status.favourited = true;
|
status.favourited = true;
|
||||||
|
|
@ -367,10 +360,10 @@ const favouritedByController: AppController = (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/statuses/#boost */
|
/** https://docs.joinmastodon.org/methods/statuses/#boost */
|
||||||
const reblogStatusController: AppController = async (c) => {
|
const reblogStatusController: AppController = async (c) => {
|
||||||
const { conf, relay, user, signal } = c.var;
|
const { conf, relay, user } = c.var;
|
||||||
|
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
const event = await getEvent(eventId);
|
const event = await getEvent(eventId, c.var);
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return c.json({ error: 'Event not found.' }, 404);
|
return c.json({ error: 'Event not found.' }, 404);
|
||||||
|
|
@ -384,13 +377,9 @@ const reblogStatusController: AppController = async (c) => {
|
||||||
],
|
],
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({ ...c.var, events: [reblogEvent] });
|
||||||
events: [reblogEvent],
|
|
||||||
relay,
|
|
||||||
signal: signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const status = await renderReblog(reblogEvent, { viewerPubkey: await user?.signer.getPublicKey() });
|
const status = await renderReblog(relay, reblogEvent, { viewerPubkey: await user?.signer.getPublicKey() });
|
||||||
|
|
||||||
return c.json(status);
|
return c.json(status);
|
||||||
};
|
};
|
||||||
|
|
@ -420,7 +409,7 @@ const unreblogStatusController: AppController = async (c) => {
|
||||||
tags: [['e', repostEvent.id, conf.relay, '', repostEvent.pubkey]],
|
tags: [['e', repostEvent.id, conf.relay, '', repostEvent.pubkey]],
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
return c.json(await renderStatus(event, { viewerPubkey: pubkey }));
|
return c.json(await renderStatus(relay, event, { viewerPubkey: pubkey }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rebloggedByController: AppController = (c) => {
|
const rebloggedByController: AppController = (c) => {
|
||||||
|
|
@ -441,12 +430,12 @@ const quotesController: AppController = async (c) => {
|
||||||
|
|
||||||
const quotes = await relay
|
const quotes = await relay
|
||||||
.query([{ kinds: [1, 20], '#q': [event.id], ...pagination }])
|
.query([{ kinds: [1, 20], '#q': [event.id], ...pagination }])
|
||||||
.then((events) => hydrateEvents({ events, relay }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
const viewerPubkey = await user?.signer.getPublicKey();
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
|
|
||||||
const statuses = await Promise.all(
|
const statuses = await Promise.all(
|
||||||
quotes.map((event) => renderStatus(event, { viewerPubkey })),
|
quotes.map((event) => renderStatus(relay, event, { viewerPubkey })),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!statuses.length) {
|
if (!statuses.length) {
|
||||||
|
|
@ -458,11 +447,11 @@ const quotesController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/statuses/#bookmark */
|
/** https://docs.joinmastodon.org/methods/statuses/#bookmark */
|
||||||
const bookmarkController: AppController = async (c) => {
|
const bookmarkController: AppController = async (c) => {
|
||||||
const { conf, user } = c.var;
|
const { conf, relay, user } = c.var;
|
||||||
const pubkey = await user!.signer.getPublicKey();
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
|
|
||||||
const event = await getEvent(eventId);
|
const event = await getEvent(eventId, c.var);
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -471,7 +460,7 @@ const bookmarkController: AppController = async (c) => {
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
const status = await renderStatus(event, { viewerPubkey: pubkey });
|
const status = await renderStatus(relay, event, { viewerPubkey: pubkey });
|
||||||
if (status) {
|
if (status) {
|
||||||
status.bookmarked = true;
|
status.bookmarked = true;
|
||||||
}
|
}
|
||||||
|
|
@ -483,12 +472,12 @@ const bookmarkController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/statuses/#unbookmark */
|
/** https://docs.joinmastodon.org/methods/statuses/#unbookmark */
|
||||||
const unbookmarkController: AppController = async (c) => {
|
const unbookmarkController: AppController = async (c) => {
|
||||||
const { conf, user } = c.var;
|
const { conf, relay, user } = c.var;
|
||||||
|
|
||||||
const pubkey = await user!.signer.getPublicKey();
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
|
|
||||||
const event = await getEvent(eventId);
|
const event = await getEvent(eventId, c.var);
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -497,7 +486,7 @@ const unbookmarkController: AppController = async (c) => {
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
const status = await renderStatus(event, { viewerPubkey: pubkey });
|
const status = await renderStatus(relay, event, { viewerPubkey: pubkey });
|
||||||
if (status) {
|
if (status) {
|
||||||
status.bookmarked = false;
|
status.bookmarked = false;
|
||||||
}
|
}
|
||||||
|
|
@ -509,12 +498,12 @@ const unbookmarkController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/statuses/#pin */
|
/** https://docs.joinmastodon.org/methods/statuses/#pin */
|
||||||
const pinController: AppController = async (c) => {
|
const pinController: AppController = async (c) => {
|
||||||
const { conf, user } = c.var;
|
const { conf, relay, user } = c.var;
|
||||||
|
|
||||||
const pubkey = await user!.signer.getPublicKey();
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
|
|
||||||
const event = await getEvent(eventId);
|
const event = await getEvent(eventId, c.var);
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -523,7 +512,7 @@ const pinController: AppController = async (c) => {
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
const status = await renderStatus(event, { viewerPubkey: pubkey });
|
const status = await renderStatus(relay, event, { viewerPubkey: pubkey });
|
||||||
if (status) {
|
if (status) {
|
||||||
status.pinned = true;
|
status.pinned = true;
|
||||||
}
|
}
|
||||||
|
|
@ -535,15 +524,12 @@ const pinController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/statuses/#unpin */
|
/** https://docs.joinmastodon.org/methods/statuses/#unpin */
|
||||||
const unpinController: AppController = async (c) => {
|
const unpinController: AppController = async (c) => {
|
||||||
const { conf, user, signal } = c.var;
|
const { conf, relay, user } = c.var;
|
||||||
|
|
||||||
const pubkey = await user!.signer.getPublicKey();
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
|
|
||||||
const event = await getEvent(eventId, {
|
const event = await getEvent(eventId, c.var);
|
||||||
kind: 1,
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -552,7 +538,7 @@ const unpinController: AppController = async (c) => {
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
const status = await renderStatus(event, { viewerPubkey: pubkey });
|
const status = await renderStatus(relay, event, { viewerPubkey: pubkey });
|
||||||
if (status) {
|
if (status) {
|
||||||
status.pinned = false;
|
status.pinned = false;
|
||||||
}
|
}
|
||||||
|
|
@ -586,7 +572,7 @@ const zapController: AppController = async (c) => {
|
||||||
let lnurl: undefined | string;
|
let lnurl: undefined | string;
|
||||||
|
|
||||||
if (status_id) {
|
if (status_id) {
|
||||||
target = await getEvent(status_id, { kind: 1, signal });
|
target = await getEvent(status_id, c.var);
|
||||||
const author = target?.author;
|
const author = target?.author;
|
||||||
const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content);
|
const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content);
|
||||||
lnurl = getLnurl(meta);
|
lnurl = getLnurl(meta);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
streamingServerMessagesCounter,
|
streamingServerMessagesCounter,
|
||||||
} from '@ditto/metrics';
|
} from '@ditto/metrics';
|
||||||
import TTLCache from '@isaacs/ttlcache';
|
import TTLCache from '@isaacs/ttlcache';
|
||||||
import { NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify';
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
|
@ -111,7 +111,7 @@ const streamingController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], relay, signal: AbortSignal.timeout(1000) });
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
|
|
||||||
const result = await render(event);
|
const result = await render(event);
|
||||||
|
|
||||||
|
|
@ -130,17 +130,17 @@ const streamingController: AppController = async (c) => {
|
||||||
streamingConnectionsGauge.set(connections.size);
|
streamingConnectionsGauge.set(connections.size);
|
||||||
|
|
||||||
if (!stream) return;
|
if (!stream) return;
|
||||||
const topicFilter = await topicToFilter(stream, c.req.query(), pubkey, conf.url.host);
|
const topicFilter = await topicToFilter(relay, stream, c.req.query(), pubkey, conf.url.host);
|
||||||
|
|
||||||
if (topicFilter) {
|
if (topicFilter) {
|
||||||
sub(topicFilter, async (event) => {
|
sub(topicFilter, async (event) => {
|
||||||
let payload: object | undefined;
|
let payload: object | undefined;
|
||||||
|
|
||||||
if (event.kind === 1) {
|
if (event.kind === 1) {
|
||||||
payload = await renderStatus(event, { viewerPubkey: pubkey });
|
payload = await renderStatus(relay, event, { viewerPubkey: pubkey });
|
||||||
}
|
}
|
||||||
if (event.kind === 6) {
|
if (event.kind === 6) {
|
||||||
payload = await renderReblog(event, { viewerPubkey: pubkey });
|
payload = await renderReblog(relay, event, { viewerPubkey: pubkey });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload) {
|
if (payload) {
|
||||||
|
|
@ -156,13 +156,13 @@ const streamingController: AppController = async (c) => {
|
||||||
if (['user', 'user:notification'].includes(stream) && pubkey) {
|
if (['user', 'user:notification'].includes(stream) && pubkey) {
|
||||||
sub({ '#p': [pubkey], limit: 0 }, async (event) => {
|
sub({ '#p': [pubkey], limit: 0 }, async (event) => {
|
||||||
if (event.pubkey === pubkey) return; // skip own events
|
if (event.pubkey === pubkey) return; // skip own events
|
||||||
const payload = await renderNotification(event, { viewerPubkey: pubkey });
|
const payload = await renderNotification(relay, event, { viewerPubkey: pubkey });
|
||||||
if (payload) {
|
if (payload) {
|
||||||
return {
|
return {
|
||||||
event: 'notification',
|
event: 'notification',
|
||||||
payload: JSON.stringify(payload),
|
payload: JSON.stringify(payload),
|
||||||
stream: [stream],
|
stream: [stream],
|
||||||
};
|
} satisfies StreamingEvent;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -198,6 +198,7 @@ const streamingController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function topicToFilter(
|
async function topicToFilter(
|
||||||
|
relay: NStore,
|
||||||
topic: Stream,
|
topic: Stream,
|
||||||
query: Record<string, string>,
|
query: Record<string, string>,
|
||||||
pubkey: string | undefined,
|
pubkey: string | undefined,
|
||||||
|
|
@ -218,7 +219,7 @@ async function topicToFilter(
|
||||||
// HACK: this puts the user's entire contacts list into RAM,
|
// HACK: this puts the user's entire contacts list into RAM,
|
||||||
// and then calls `matchFilters` over it. Refreshing the page
|
// and then calls `matchFilters` over it. Refreshing the page
|
||||||
// is required after following a new user.
|
// is required after following a new user.
|
||||||
return pubkey ? { kinds: [1, 6, 20], authors: [...await getFeedPubkeys(pubkey)], limit: 0 } : undefined;
|
return pubkey ? { kinds: [1, 6, 20], authors: [...await getFeedPubkeys(relay, pubkey)], limit: 0 } : undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
|
import { paginated, paginatedList } from '@ditto/mastoapi/pagination';
|
||||||
import { NostrFilter } from '@nostrify/nostrify';
|
import { NostrFilter } from '@nostrify/nostrify';
|
||||||
import { matchFilter } from 'nostr-tools';
|
import { matchFilter } from 'nostr-tools';
|
||||||
|
|
||||||
import { AppContext, AppController } from '@/app.ts';
|
import { AppContext, AppController } from '@/app.ts';
|
||||||
import { paginationSchema } from '@/schemas/pagination.ts';
|
import { paginationSchema } from '@/schemas/pagination.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { paginated, paginatedList } from '@/utils/api.ts';
|
|
||||||
import { getTagSet } from '@/utils/tags.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@ async function renderV2Suggestions(c: AppContext, params: { offset: number; limi
|
||||||
[{ kinds: [0], authors, limit: authors.length }],
|
[{ kinds: [0], authors, limit: authors.length }],
|
||||||
{ signal },
|
{ signal },
|
||||||
)
|
)
|
||||||
.then((events) => hydrateEvents({ events, relay, signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
return Promise.all(authors.map(async (pubkey) => {
|
return Promise.all(authors.map(async (pubkey) => {
|
||||||
const profile = profiles.find((event) => event.pubkey === pubkey);
|
const profile = profiles.find((event) => event.pubkey === pubkey);
|
||||||
|
|
@ -115,7 +115,7 @@ export const localSuggestionsController: AppController = async (c) => {
|
||||||
[{ kinds: [0], authors: [...pubkeys], search: `domain:${conf.url.host}`, ...pagination }],
|
[{ kinds: [0], authors: [...pubkeys], search: `domain:${conf.url.host}`, ...pagination }],
|
||||||
{ signal },
|
{ signal },
|
||||||
)
|
)
|
||||||
.then((events) => hydrateEvents({ relay, events, signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
const suggestions = [...pubkeys].map((pubkey) => {
|
const suggestions = [...pubkeys].map((pubkey) => {
|
||||||
const profile = profiles.find((event) => event.pubkey === pubkey);
|
const profile = profiles.find((event) => event.pubkey === pubkey);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { paginated } from '@ditto/mastoapi/pagination';
|
||||||
import { NostrFilter } from '@nostrify/nostrify';
|
import { NostrFilter } from '@nostrify/nostrify';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
|
@ -5,7 +6,6 @@ import { type AppContext, type AppController } from '@/app.ts';
|
||||||
import { getFeedPubkeys } from '@/queries.ts';
|
import { getFeedPubkeys } from '@/queries.ts';
|
||||||
import { booleanParamSchema, languageSchema } from '@/schema.ts';
|
import { booleanParamSchema, languageSchema } from '@/schema.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { paginated } from '@/utils/api.ts';
|
|
||||||
import { getTagSet } from '@/utils/tags.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
|
import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ const homeQuerySchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const homeTimelineController: AppController = async (c) => {
|
const homeTimelineController: AppController = async (c) => {
|
||||||
const { user, pagination } = c.var;
|
const { relay, user, pagination } = c.var;
|
||||||
const pubkey = await user?.signer.getPublicKey()!;
|
const pubkey = await user?.signer.getPublicKey()!;
|
||||||
const result = homeQuerySchema.safeParse(c.req.query());
|
const result = homeQuerySchema.safeParse(c.req.query());
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ const homeTimelineController: AppController = async (c) => {
|
||||||
|
|
||||||
const { exclude_replies, only_media } = result.data;
|
const { exclude_replies, only_media } = result.data;
|
||||||
|
|
||||||
const authors = [...await getFeedPubkeys(pubkey)];
|
const authors = [...await getFeedPubkeys(relay, pubkey)];
|
||||||
const filter: NostrFilter = { authors, kinds: [1, 6, 20], ...pagination };
|
const filter: NostrFilter = { authors, kinds: [1, 6, 20], ...pagination };
|
||||||
|
|
||||||
const search: string[] = [];
|
const search: string[] = [];
|
||||||
|
|
@ -110,7 +110,7 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) {
|
||||||
|
|
||||||
const events = await relay
|
const events = await relay
|
||||||
.query(filters, opts)
|
.query(filters, opts)
|
||||||
.then((events) => hydrateEvents({ events, relay, signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
|
|
@ -120,9 +120,9 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) {
|
||||||
|
|
||||||
const statuses = (await Promise.all(events.map((event) => {
|
const statuses = (await Promise.all(events.map((event) => {
|
||||||
if (event.kind === 6) {
|
if (event.kind === 6) {
|
||||||
return renderReblog(event, { viewerPubkey });
|
return renderReblog(relay, event, { viewerPubkey });
|
||||||
}
|
}
|
||||||
return renderStatus(event, { viewerPubkey });
|
return renderStatus(relay, event, { viewerPubkey });
|
||||||
}))).filter(Boolean);
|
}))).filter(Boolean);
|
||||||
|
|
||||||
if (!statuses.length) {
|
if (!statuses.length) {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const translateSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const translateController: AppController = async (c) => {
|
const translateController: AppController = async (c) => {
|
||||||
const { user, signal } = c.var;
|
const { relay, user, signal } = c.var;
|
||||||
|
|
||||||
const result = translateSchema.safeParse(await parseBody(c.req.raw));
|
const result = translateSchema.safeParse(await parseBody(c.req.raw));
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ const translateController: AppController = async (c) => {
|
||||||
|
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
|
|
||||||
const event = await getEvent(id, { signal });
|
const event = await getEvent(id, c.var);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return c.json({ error: 'Record not found' }, 400);
|
return c.json({ error: 'Record not found' }, 400);
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +45,7 @@ const translateController: AppController = async (c) => {
|
||||||
return c.json({ error: 'Source and target languages are the same. No translation needed.' }, 400);
|
return c.json({ error: 'Source and target languages are the same. No translation needed.' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = await renderStatus(event, { viewerPubkey });
|
const status = await renderStatus(relay, event, { viewerPubkey });
|
||||||
if (!status?.content) {
|
if (!status?.content) {
|
||||||
return c.json({ error: 'Bad request.', schema: result.error }, 400);
|
return c.json({ error: 'Bad request.', schema: result.error }, 400);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,45 @@
|
||||||
import { type DittoConf } from '@ditto/conf';
|
import { type DittoConf } from '@ditto/conf';
|
||||||
|
import { paginated } from '@ditto/mastoapi/pagination';
|
||||||
import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify';
|
import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify';
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { AppController } from '@/app.ts';
|
import { AppController } from '@/app.ts';
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { paginationSchema } from '@/schemas/pagination.ts';
|
import { paginationSchema } from '@/schemas/pagination.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { generateDateRange, Time } from '@/utils/time.ts';
|
import { generateDateRange, Time } from '@/utils/time.ts';
|
||||||
import { unfurlCardCached } from '@/utils/unfurl.ts';
|
import { PreviewCard, unfurlCardCached } from '@/utils/unfurl.ts';
|
||||||
import { paginated } from '@/utils/api.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';
|
||||||
|
|
||||||
let trendingHashtagsCache = getTrendingHashtags(Conf).catch((e: unknown) => {
|
interface TrendHistory {
|
||||||
logi({
|
day: string;
|
||||||
level: 'error',
|
accounts: string;
|
||||||
ns: 'ditto.trends.api',
|
uses: string;
|
||||||
type: 'tags',
|
}
|
||||||
msg: 'Failed to get trending hashtags',
|
|
||||||
error: errorJson(e),
|
interface TrendingHashtag {
|
||||||
});
|
name: string;
|
||||||
return Promise.resolve([]);
|
url: string;
|
||||||
|
history: TrendHistory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TrendingLink extends PreviewCard {
|
||||||
|
history: TrendHistory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const trendingTagsQuerySchema = z.object({
|
||||||
|
limit: z.coerce.number().catch(10).transform((value) => Math.min(Math.max(value, 0), 20)),
|
||||||
|
offset: z.number().nonnegative().catch(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.cron('update trending hashtags cache', '35 * * * *', async () => {
|
const trendingTagsController: AppController = async (c) => {
|
||||||
|
const { conf, relay } = c.var;
|
||||||
|
const { limit, offset } = trendingTagsQuerySchema.parse(c.req.query());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const trends = await getTrendingHashtags(Conf);
|
const trends = await getTrendingHashtags(conf, relay);
|
||||||
trendingHashtagsCache = Promise.resolve(trends);
|
return c.json(trends.slice(offset, offset + limit));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logi({
|
logi({
|
||||||
level: 'error',
|
level: 'error',
|
||||||
|
|
@ -37,22 +48,11 @@ Deno.cron('update trending hashtags cache', '35 * * * *', async () => {
|
||||||
msg: 'Failed to get trending hashtags',
|
msg: 'Failed to get trending hashtags',
|
||||||
error: errorJson(e),
|
error: errorJson(e),
|
||||||
});
|
});
|
||||||
|
return c.json([]);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const trendingTagsQuerySchema = z.object({
|
|
||||||
limit: z.coerce.number().catch(10).transform((value) => Math.min(Math.max(value, 0), 20)),
|
|
||||||
offset: z.number().nonnegative().catch(0),
|
|
||||||
});
|
|
||||||
|
|
||||||
const trendingTagsController: AppController = async (c) => {
|
|
||||||
const { limit, offset } = trendingTagsQuerySchema.parse(c.req.query());
|
|
||||||
const trends = await trendingHashtagsCache;
|
|
||||||
return c.json(trends.slice(offset, offset + limit));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getTrendingHashtags(conf: DittoConf) {
|
async function getTrendingHashtags(conf: DittoConf, relay: NStore): Promise<TrendingHashtag[]> {
|
||||||
const relay = await Storages.db();
|
|
||||||
const trends = await getTrendingTags(relay, 't', await conf.signer.getPublicKey());
|
const trends = await getTrendingTags(relay, 't', await conf.signer.getPublicKey());
|
||||||
|
|
||||||
return trends.map((trend) => {
|
return trends.map((trend) => {
|
||||||
|
|
@ -72,21 +72,12 @@ async function getTrendingHashtags(conf: DittoConf) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let trendingLinksCache = getTrendingLinks(Conf).catch((e: unknown) => {
|
const trendingLinksController: AppController = async (c) => {
|
||||||
logi({
|
const { conf, relay } = c.var;
|
||||||
level: 'error',
|
const { limit, offset } = trendingTagsQuerySchema.parse(c.req.query());
|
||||||
ns: 'ditto.trends.api',
|
|
||||||
type: 'links',
|
|
||||||
msg: 'Failed to get trending links',
|
|
||||||
error: errorJson(e),
|
|
||||||
});
|
|
||||||
return Promise.resolve([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.cron('update trending links cache', '50 * * * *', async () => {
|
|
||||||
try {
|
try {
|
||||||
const trends = await getTrendingLinks(Conf);
|
const trends = await getTrendingLinks(conf, relay);
|
||||||
trendingLinksCache = Promise.resolve(trends);
|
return c.json(trends.slice(offset, offset + limit));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logi({
|
logi({
|
||||||
level: 'error',
|
level: 'error',
|
||||||
|
|
@ -95,17 +86,11 @@ Deno.cron('update trending links cache', '50 * * * *', async () => {
|
||||||
msg: 'Failed to get trending links',
|
msg: 'Failed to get trending links',
|
||||||
error: errorJson(e),
|
error: errorJson(e),
|
||||||
});
|
});
|
||||||
|
return c.json([]);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const trendingLinksController: AppController = async (c) => {
|
|
||||||
const { limit, offset } = trendingTagsQuerySchema.parse(c.req.query());
|
|
||||||
const trends = await trendingLinksCache;
|
|
||||||
return c.json(trends.slice(offset, offset + limit));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getTrendingLinks(conf: DittoConf) {
|
async function getTrendingLinks(conf: DittoConf, relay: NStore): Promise<TrendingLink[]> {
|
||||||
const relay = await Storages.db();
|
|
||||||
const trends = await getTrendingTags(relay, 'r', await conf.signer.getPublicKey());
|
const trends = await getTrendingTags(relay, 'r', await conf.signer.getPublicKey());
|
||||||
|
|
||||||
return Promise.all(trends.map(async (trend) => {
|
return Promise.all(trends.map(async (trend) => {
|
||||||
|
|
@ -162,7 +147,7 @@ const trendingStatusesController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await relay.query([{ kinds: [1, 20], ids }])
|
const results = await relay.query([{ kinds: [1, 20], ids }])
|
||||||
.then((events) => hydrateEvents({ events, relay }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
// Sort events in the order they appear in the label.
|
// Sort events in the order they appear in the label.
|
||||||
const events = ids
|
const events = ids
|
||||||
|
|
@ -170,7 +155,7 @@ const trendingStatusesController: AppController = async (c) => {
|
||||||
.filter((event): event is NostrEvent => !!event);
|
.filter((event): event is NostrEvent => !!event);
|
||||||
|
|
||||||
const statuses = await Promise.all(
|
const statuses = await Promise.all(
|
||||||
events.map((event) => renderStatus(event, {})),
|
events.map((event) => renderStatus(relay, event, {})),
|
||||||
);
|
);
|
||||||
|
|
||||||
return paginated(c, results, statuses);
|
return paginated(c, results, statuses);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
|
|
||||||
import { AppMiddleware } from '@/app.ts';
|
import { AppContext, AppMiddleware } from '@/app.ts';
|
||||||
import { getPathParams, MetadataEntities } from '@/utils/og-metadata.ts';
|
import { getPathParams, MetadataEntities } from '@/utils/og-metadata.ts';
|
||||||
import { getInstanceMetadata } from '@/utils/instance.ts';
|
import { getInstanceMetadata } from '@/utils/instance.ts';
|
||||||
import { errorJson } from '@/utils/log.ts';
|
import { errorJson } from '@/utils/log.ts';
|
||||||
|
|
@ -9,14 +9,11 @@ import { renderMetadata } from '@/views/meta.ts';
|
||||||
import { getAuthor, getEvent } from '@/queries.ts';
|
import { getAuthor, getEvent } from '@/queries.ts';
|
||||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
import { renderAccount } from '@/views/mastodon/accounts.ts';
|
import { renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
import { NStore } from '@nostrify/nostrify';
|
|
||||||
|
|
||||||
/** Placeholder to find & replace with metadata. */
|
/** Placeholder to find & replace with metadata. */
|
||||||
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 { relay } = 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 {
|
||||||
|
|
@ -25,7 +22,7 @@ export const frontendController: AppMiddleware = async (c) => {
|
||||||
if (content.includes(META_PLACEHOLDER)) {
|
if (content.includes(META_PLACEHOLDER)) {
|
||||||
const params = getPathParams(c.req.path);
|
const params = getPathParams(c.req.path);
|
||||||
try {
|
try {
|
||||||
const entities = await getEntities(relay, params ?? {});
|
const entities = await getEntities(c, params ?? {});
|
||||||
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) {
|
||||||
|
|
@ -39,25 +36,27 @@ export const frontendController: AppMiddleware = async (c) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getEntities(relay: NStore, params: { acct?: string; statusId?: string }): Promise<MetadataEntities> {
|
async function getEntities(c: AppContext, params: { acct?: string; statusId?: string }): Promise<MetadataEntities> {
|
||||||
|
const { relay } = c.var;
|
||||||
|
|
||||||
const entities: MetadataEntities = {
|
const entities: MetadataEntities = {
|
||||||
instance: await getInstanceMetadata(relay),
|
instance: await getInstanceMetadata(relay),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (params.statusId) {
|
if (params.statusId) {
|
||||||
const event = await getEvent(params.statusId, { kind: 1 });
|
const event = await getEvent(params.statusId, c.var);
|
||||||
if (event) {
|
if (event) {
|
||||||
entities.status = await renderStatus(event, {});
|
entities.status = await renderStatus(relay, event, {});
|
||||||
entities.account = entities.status?.account;
|
entities.account = entities.status?.account;
|
||||||
}
|
}
|
||||||
return entities;
|
return entities;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.acct) {
|
if (params.acct) {
|
||||||
const pubkey = await lookupPubkey(params.acct.replace(/^@/, ''));
|
const pubkey = await lookupPubkey(params.acct.replace(/^@/, ''), c.var);
|
||||||
const event = pubkey ? await getAuthor(pubkey) : undefined;
|
const event = pubkey ? await getAuthor(pubkey, c.var) : undefined;
|
||||||
if (event) {
|
if (event) {
|
||||||
entities.account = await renderAccount(event);
|
entities.account = renderAccount(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,16 @@
|
||||||
import {
|
import { dbAvailableConnectionsGauge, dbPoolSizeGauge } from '@ditto/metrics';
|
||||||
dbAvailableConnectionsGauge,
|
|
||||||
dbPoolSizeGauge,
|
|
||||||
relayPoolRelaysSizeGauge,
|
|
||||||
relayPoolSubscriptionsSizeGauge,
|
|
||||||
} from '@ditto/metrics';
|
|
||||||
import { register } from 'prom-client';
|
import { register } from 'prom-client';
|
||||||
|
|
||||||
import { AppController } from '@/app.ts';
|
import { AppController } from '@/app.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
|
|
||||||
/** Prometheus/OpenMetrics controller. */
|
/** Prometheus/OpenMetrics controller. */
|
||||||
export const metricsController: AppController = async (c) => {
|
export const metricsController: AppController = async (c) => {
|
||||||
const db = await Storages.database();
|
const { db } = c.var;
|
||||||
const pool = await Storages.client();
|
|
||||||
|
|
||||||
// Update some metrics at request time.
|
// Update some metrics at request time.
|
||||||
dbPoolSizeGauge.set(db.poolSize);
|
dbPoolSizeGauge.set(db.poolSize);
|
||||||
dbAvailableConnectionsGauge.set(db.availableConnections);
|
dbAvailableConnectionsGauge.set(db.availableConnections);
|
||||||
|
|
||||||
relayPoolRelaysSizeGauge.reset();
|
|
||||||
relayPoolSubscriptionsSizeGauge.reset();
|
|
||||||
|
|
||||||
for (const relay of pool.relays.values()) {
|
|
||||||
relayPoolRelaysSizeGauge.inc({ ready_state: relay.socket.readyState });
|
|
||||||
relayPoolSubscriptionsSizeGauge.inc(relay.subscriptions.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve the metrics.
|
// Serve the metrics.
|
||||||
const metrics = await register.metrics();
|
const metrics = await register.metrics();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import {
|
||||||
|
|
||||||
import { AppController } from '@/app.ts';
|
import { AppController } from '@/app.ts';
|
||||||
import { relayInfoController } from '@/controllers/nostr/relay-info.ts';
|
import { relayInfoController } from '@/controllers/nostr/relay-info.ts';
|
||||||
import * as pipeline from '@/pipeline.ts';
|
|
||||||
import { RelayError } from '@/RelayError.ts';
|
import { RelayError } from '@/RelayError.ts';
|
||||||
import { type DittoPgStore } from '@/storages/DittoPgStore.ts';
|
import { type DittoPgStore } from '@/storages/DittoPgStore.ts';
|
||||||
import { errorJson } from '@/utils/log.ts';
|
import { errorJson } from '@/utils/log.ts';
|
||||||
|
|
@ -159,7 +158,7 @@ function connectStream(conf: DittoConf, relay: DittoPgStore, socket: WebSocket,
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This will store it (if eligible) and run other side-effects.
|
// This will store it (if eligible) and run other side-effects.
|
||||||
await pipeline.handleEvent(purifyEvent(event), { source: 'relay', signal: AbortSignal.timeout(1000) });
|
await relay.event(purifyEvent(event), { signal: AbortSignal.timeout(1000) });
|
||||||
send(['OK', event.id, true, '']);
|
send(['OK', event.id, true, '']);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof RelayError) {
|
if (e instanceof RelayError) {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@ const emptyResult: NostrJson = { names: {}, relays: {} };
|
||||||
* https://github.com/nostr-protocol/nips/blob/master/05.md
|
* https://github.com/nostr-protocol/nips/blob/master/05.md
|
||||||
*/
|
*/
|
||||||
const nostrController: AppController = async (c) => {
|
const nostrController: AppController = async (c) => {
|
||||||
const { relay } = c.var;
|
|
||||||
|
|
||||||
// If there are no query parameters, this will always return an empty result.
|
// If there are no query parameters, this will always return an empty result.
|
||||||
if (!Object.entries(c.req.queries()).length) {
|
if (!Object.entries(c.req.queries()).length) {
|
||||||
c.header('Cache-Control', 'max-age=31536000, public, immutable, stale-while-revalidate=86400');
|
c.header('Cache-Control', 'max-age=31536000, public, immutable, stale-while-revalidate=86400');
|
||||||
|
|
@ -22,7 +20,7 @@ const nostrController: AppController = async (c) => {
|
||||||
|
|
||||||
const result = nameSchema.safeParse(c.req.query('name'));
|
const result = nameSchema.safeParse(c.req.query('name'));
|
||||||
const name = result.success ? result.data : undefined;
|
const name = result.success ? result.data : undefined;
|
||||||
const pointer = name ? await localNip05Lookup(relay, name) : undefined;
|
const pointer = name ? await localNip05Lookup(name, c.var) : undefined;
|
||||||
|
|
||||||
if (!name || !pointer) {
|
if (!name || !pointer) {
|
||||||
// Not found, cache for 5 minutes.
|
// Not found, cache for 5 minutes.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { sql } from 'kysely';
|
import { sql } from 'kysely';
|
||||||
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import {
|
import {
|
||||||
|
type TrendsCtx,
|
||||||
updateTrendingEvents,
|
updateTrendingEvents,
|
||||||
updateTrendingHashtags,
|
updateTrendingHashtags,
|
||||||
updateTrendingLinks,
|
updateTrendingLinks,
|
||||||
|
|
@ -10,15 +10,15 @@ import {
|
||||||
} from '@/trends.ts';
|
} from '@/trends.ts';
|
||||||
|
|
||||||
/** Start cron jobs for the application. */
|
/** Start cron jobs for the application. */
|
||||||
export function cron() {
|
export function cron(ctx: TrendsCtx) {
|
||||||
Deno.cron('update trending pubkeys', '0 * * * *', updateTrendingPubkeys);
|
Deno.cron('update trending pubkeys', '0 * * * *', () => updateTrendingPubkeys(ctx));
|
||||||
Deno.cron('update trending zapped events', '7 * * * *', updateTrendingZappedEvents);
|
Deno.cron('update trending zapped events', '7 * * * *', () => updateTrendingZappedEvents(ctx));
|
||||||
Deno.cron('update trending events', '15 * * * *', updateTrendingEvents);
|
Deno.cron('update trending events', '15 * * * *', () => updateTrendingEvents(ctx));
|
||||||
Deno.cron('update trending hashtags', '30 * * * *', updateTrendingHashtags);
|
Deno.cron('update trending hashtags', '30 * * * *', () => updateTrendingHashtags(ctx));
|
||||||
Deno.cron('update trending links', '45 * * * *', updateTrendingLinks);
|
Deno.cron('update trending links', '45 * * * *', () => updateTrendingLinks(ctx));
|
||||||
|
|
||||||
Deno.cron('refresh top authors', '20 * * * *', async () => {
|
Deno.cron('refresh top authors', '20 * * * *', async () => {
|
||||||
const kysely = await Storages.kysely();
|
const { kysely } = ctx.db;
|
||||||
await sql`refresh materialized view top_authors`.execute(kysely);
|
await sql`refresh materialized view top_authors`.execute(kysely);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,38 @@
|
||||||
import { firehoseEventsCounter } from '@ditto/metrics';
|
import { firehoseEventsCounter } from '@ditto/metrics';
|
||||||
import { Semaphore } from '@core/asyncutil';
|
import { Semaphore } from '@core/asyncutil';
|
||||||
|
import { NRelay, NStore } from '@nostrify/nostrify';
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
|
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { nostrNow } from '@/utils.ts';
|
import { nostrNow } from '@/utils.ts';
|
||||||
|
|
||||||
import * as pipeline from '@/pipeline.ts';
|
interface FirehoseOpts {
|
||||||
|
pool: NRelay;
|
||||||
const sem = new Semaphore(Conf.firehoseConcurrency);
|
store: NStore;
|
||||||
|
concurrency: number;
|
||||||
|
kinds: number[];
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function watches events on all known relays and performs
|
* This function watches events on all known relays and performs
|
||||||
* side-effects based on them, such as trending hashtag tracking
|
* side-effects based on them, such as trending hashtag tracking
|
||||||
* and storing events for notifications and the home feed.
|
* and storing events for notifications and the home feed.
|
||||||
*/
|
*/
|
||||||
export async function startFirehose(): Promise<void> {
|
export async function startFirehose(opts: FirehoseOpts): Promise<void> {
|
||||||
const store = await Storages.client();
|
const { pool, store, kinds, concurrency, timeout = 5000 } = opts;
|
||||||
|
|
||||||
for await (const msg of store.req([{ kinds: Conf.firehoseKinds, limit: 0, since: nostrNow() }])) {
|
const sem = new Semaphore(concurrency);
|
||||||
|
|
||||||
|
for await (const msg of pool.req([{ kinds, limit: 0, since: nostrNow() }])) {
|
||||||
if (msg[0] === 'EVENT') {
|
if (msg[0] === 'EVENT') {
|
||||||
const event = msg[2];
|
const event = msg[2];
|
||||||
|
|
||||||
logi({ level: 'debug', ns: 'ditto.event', source: 'firehose', id: event.id, kind: event.kind });
|
logi({ level: 'debug', ns: 'ditto.event', source: 'firehose', id: event.id, kind: event.kind });
|
||||||
firehoseEventsCounter.inc({ kind: event.kind });
|
firehoseEventsCounter.inc({ kind: event.kind });
|
||||||
|
|
||||||
sem.lock(async () => {
|
sem.lock(async () => {
|
||||||
try {
|
try {
|
||||||
await pipeline.handleEvent(event, { source: 'firehose', signal: AbortSignal.timeout(5000) });
|
await store.event(event, { signal: AbortSignal.timeout(timeout) });
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
import { AppMiddleware } from '@/app.ts';
|
import { AppMiddleware } from '@/app.ts';
|
||||||
import { PleromaConfigDB } from '@/utils/PleromaConfigDB.ts';
|
import { PleromaConfigDB } from '@/utils/PleromaConfigDB.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { getPleromaConfigs } from '@/utils/pleroma.ts';
|
import { getPleromaConfigs } from '@/utils/pleroma.ts';
|
||||||
|
|
||||||
let configDBCache: Promise<PleromaConfigDB> | undefined;
|
|
||||||
|
|
||||||
export const cspMiddleware = (): AppMiddleware => {
|
export const cspMiddleware = (): AppMiddleware => {
|
||||||
|
let configDBCache: Promise<PleromaConfigDB> | undefined;
|
||||||
|
|
||||||
return async (c, next) => {
|
return async (c, next) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay } = c.var;
|
||||||
const store = await Storages.db();
|
|
||||||
|
|
||||||
if (!configDBCache) {
|
if (!configDBCache) {
|
||||||
configDBCache = getPleromaConfigs(store);
|
configDBCache = getPleromaConfigs(relay);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { host, protocol, origin } = conf.url;
|
const { host, protocol, origin } = conf.url;
|
||||||
|
|
|
||||||
|
|
@ -1,368 +0,0 @@
|
||||||
import { DittoTables } from '@ditto/db';
|
|
||||||
import { pipelineEventsCounter, policyEventsCounter, webPushNotificationsCounter } from '@ditto/metrics';
|
|
||||||
import { NKinds, NostrEvent, NSchema as n } from '@nostrify/nostrify';
|
|
||||||
import { logi } from '@soapbox/logi';
|
|
||||||
import { Kysely, UpdateObject } from 'kysely';
|
|
||||||
import tldts from 'tldts';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { pipelineEncounters } from '@/caches/pipelineEncounters.ts';
|
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { DittoPush } from '@/DittoPush.ts';
|
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
|
||||||
import { RelayError } from '@/RelayError.ts';
|
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { eventAge, Time } from '@/utils.ts';
|
|
||||||
import { getAmount } from '@/utils/bolt11.ts';
|
|
||||||
import { faviconCache } from '@/utils/favicon.ts';
|
|
||||||
import { errorJson } from '@/utils/log.ts';
|
|
||||||
import { nip05Cache } from '@/utils/nip05.ts';
|
|
||||||
import { parseNoteContent, stripimeta } from '@/utils/note.ts';
|
|
||||||
import { purifyEvent } from '@/utils/purify.ts';
|
|
||||||
import { updateStats } from '@/utils/stats.ts';
|
|
||||||
import { getTagSet } from '@/utils/tags.ts';
|
|
||||||
import { unfurlCardCached } from '@/utils/unfurl.ts';
|
|
||||||
import { renderWebPushNotification } from '@/views/mastodon/push.ts';
|
|
||||||
import { policyWorker } from '@/workers/policy.ts';
|
|
||||||
import { verifyEventWorker } from '@/workers/verify.ts';
|
|
||||||
|
|
||||||
interface PipelineOpts {
|
|
||||||
signal: AbortSignal;
|
|
||||||
source: 'relay' | 'api' | 'firehose' | 'pipeline' | 'notify' | 'internal';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common pipeline function to process (and maybe store) events.
|
|
||||||
* It is idempotent, so it can be called multiple times for the same event.
|
|
||||||
*/
|
|
||||||
async function handleEvent(event: DittoEvent, opts: PipelineOpts): Promise<void> {
|
|
||||||
// Skip events that have already been encountered.
|
|
||||||
if (pipelineEncounters.get(event.id)) {
|
|
||||||
throw new RelayError('duplicate', 'already have this event');
|
|
||||||
}
|
|
||||||
// Reject events that are too far in the future.
|
|
||||||
if (eventAge(event) < -Time.minutes(1)) {
|
|
||||||
throw new RelayError('invalid', 'event too far in the future');
|
|
||||||
}
|
|
||||||
// Integer max value for Postgres.
|
|
||||||
if (event.kind >= 2_147_483_647) {
|
|
||||||
throw new RelayError('invalid', 'event kind too large');
|
|
||||||
}
|
|
||||||
// The only point of ephemeral events is to stream them,
|
|
||||||
// so throw an error if we're not even going to do that.
|
|
||||||
if (NKinds.ephemeral(event.kind) && !isFresh(event)) {
|
|
||||||
throw new RelayError('invalid', 'event too old');
|
|
||||||
}
|
|
||||||
// Block NIP-70 events, because we have no way to `AUTH`.
|
|
||||||
if (isProtectedEvent(event)) {
|
|
||||||
throw new RelayError('invalid', 'protected event');
|
|
||||||
}
|
|
||||||
// Validate the event's signature.
|
|
||||||
if (!(await verifyEventWorker(event))) {
|
|
||||||
throw new RelayError('invalid', 'invalid signature');
|
|
||||||
}
|
|
||||||
// Recheck encountered after async ops.
|
|
||||||
if (pipelineEncounters.has(event.id)) {
|
|
||||||
throw new RelayError('duplicate', 'already have this event');
|
|
||||||
}
|
|
||||||
// Set the event as encountered after verifying the signature.
|
|
||||||
pipelineEncounters.set(event.id, true);
|
|
||||||
|
|
||||||
// Log the event.
|
|
||||||
logi({ level: 'debug', ns: 'ditto.event', source: 'pipeline', id: event.id, kind: event.kind });
|
|
||||||
pipelineEventsCounter.inc({ kind: event.kind });
|
|
||||||
|
|
||||||
// NIP-46 events get special treatment.
|
|
||||||
// They are exempt from policies and other side-effects, and should be streamed out immediately.
|
|
||||||
// If streaming fails, an error should be returned.
|
|
||||||
if (event.kind === 24133) {
|
|
||||||
const store = await Storages.db();
|
|
||||||
await store.event(event, { signal: opts.signal });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the event doesn't violate the policy.
|
|
||||||
if (event.pubkey !== await Conf.signer.getPublicKey()) {
|
|
||||||
await policyFilter(event, opts.signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare the event for additional checks.
|
|
||||||
// FIXME: This is kind of hacky. Should be reorganized to fetch only what's needed for each stage.
|
|
||||||
await hydrateEvent(event, opts.signal);
|
|
||||||
|
|
||||||
// Ensure that the author is not banned.
|
|
||||||
const n = getTagSet(event.user?.tags ?? [], 'n');
|
|
||||||
if (n.has('disabled')) {
|
|
||||||
throw new RelayError('blocked', 'author is blocked');
|
|
||||||
}
|
|
||||||
|
|
||||||
const kysely = await Storages.kysely();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await storeEvent(purifyEvent(event), opts.signal);
|
|
||||||
} finally {
|
|
||||||
// This needs to run in steps, and should not block the API from responding.
|
|
||||||
Promise.allSettled([
|
|
||||||
handleZaps(kysely, event),
|
|
||||||
updateAuthorData(event, opts.signal),
|
|
||||||
prewarmLinkPreview(event, opts.signal),
|
|
||||||
generateSetEvents(event),
|
|
||||||
])
|
|
||||||
.then(() => webPush(event))
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function policyFilter(event: NostrEvent, signal: AbortSignal): Promise<void> {
|
|
||||||
try {
|
|
||||||
const result = await policyWorker.call(event, signal);
|
|
||||||
const [, , ok, reason] = result;
|
|
||||||
logi({ level: 'debug', ns: 'ditto.policy', id: event.id, kind: event.kind, ok, reason });
|
|
||||||
policyEventsCounter.inc({ ok: String(ok) });
|
|
||||||
RelayError.assert(result);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof RelayError) {
|
|
||||||
throw e;
|
|
||||||
} else {
|
|
||||||
logi({ level: 'error', ns: 'ditto.policy', id: event.id, kind: event.kind, error: errorJson(e) });
|
|
||||||
throw new RelayError('blocked', 'policy error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check whether the event has a NIP-70 `-` tag. */
|
|
||||||
function isProtectedEvent(event: NostrEvent): boolean {
|
|
||||||
return event.tags.some(([name]) => name === '-');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Hydrate the event with the user, if applicable. */
|
|
||||||
async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise<void> {
|
|
||||||
await hydrateEvents({ events: [event], relay: await Storages.db(), signal });
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Maybe store the event, if eligible. */
|
|
||||||
async function storeEvent(event: NostrEvent, signal?: AbortSignal): Promise<undefined> {
|
|
||||||
const store = await Storages.db();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await store.transaction(async (store, kysely) => {
|
|
||||||
if (!NKinds.ephemeral(event.kind)) {
|
|
||||||
await updateStats({ event, store, kysely });
|
|
||||||
}
|
|
||||||
await store.event(event, { signal });
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// If the failure is only because of updateStats (which runs first), insert the event anyway.
|
|
||||||
// We can't catch this in the transaction because the error aborts the transaction on the Postgres side.
|
|
||||||
if (e instanceof Error && e.message.includes('event_stats' satisfies keyof DittoTables)) {
|
|
||||||
await store.event(event, { signal });
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Parse kind 0 metadata and track indexes in the database. */
|
|
||||||
async function updateAuthorData(event: NostrEvent, signal: AbortSignal): Promise<void> {
|
|
||||||
if (event.kind !== 0) return;
|
|
||||||
|
|
||||||
// Parse metadata.
|
|
||||||
const metadata = n.json().pipe(n.metadata()).catch({}).safeParse(event.content);
|
|
||||||
if (!metadata.success) return;
|
|
||||||
|
|
||||||
const { name, nip05 } = metadata.data;
|
|
||||||
|
|
||||||
const kysely = await Storages.kysely();
|
|
||||||
|
|
||||||
const updates: UpdateObject<DittoTables, 'author_stats'> = {};
|
|
||||||
|
|
||||||
const authorStats = await kysely
|
|
||||||
.selectFrom('author_stats')
|
|
||||||
.selectAll()
|
|
||||||
.where('pubkey', '=', event.pubkey)
|
|
||||||
.executeTakeFirst();
|
|
||||||
|
|
||||||
const lastVerified = authorStats?.nip05_last_verified_at;
|
|
||||||
const eventNewer = !lastVerified || event.created_at > lastVerified;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (nip05 !== authorStats?.nip05 && eventNewer || !lastVerified) {
|
|
||||||
if (nip05) {
|
|
||||||
const tld = tldts.parse(nip05);
|
|
||||||
if (tld.isIcann && !tld.isIp && !tld.isPrivate) {
|
|
||||||
const pointer = await nip05Cache.fetch(nip05, { signal });
|
|
||||||
if (pointer.pubkey === event.pubkey) {
|
|
||||||
updates.nip05 = nip05;
|
|
||||||
updates.nip05_domain = tld.domain;
|
|
||||||
updates.nip05_hostname = tld.hostname;
|
|
||||||
updates.nip05_last_verified_at = event.created_at;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updates.nip05 = null;
|
|
||||||
updates.nip05_domain = null;
|
|
||||||
updates.nip05_hostname = null;
|
|
||||||
updates.nip05_last_verified_at = event.created_at;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Fallthrough.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch favicon.
|
|
||||||
const domain = nip05?.split('@')[1].toLowerCase();
|
|
||||||
if (domain) {
|
|
||||||
try {
|
|
||||||
await faviconCache.fetch(domain, { signal });
|
|
||||||
} catch {
|
|
||||||
// Fallthrough.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = [name, nip05].filter(Boolean).join(' ').trim();
|
|
||||||
|
|
||||||
if (search !== authorStats?.search) {
|
|
||||||
updates.search = search;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(updates).length) {
|
|
||||||
await kysely.insertInto('author_stats')
|
|
||||||
.values({
|
|
||||||
pubkey: event.pubkey,
|
|
||||||
followers_count: 0,
|
|
||||||
following_count: 0,
|
|
||||||
notes_count: 0,
|
|
||||||
search,
|
|
||||||
...updates,
|
|
||||||
})
|
|
||||||
.onConflict((oc) => oc.column('pubkey').doUpdateSet(updates))
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function prewarmLinkPreview(event: NostrEvent, signal: AbortSignal): Promise<void> {
|
|
||||||
const { firstUrl } = parseNoteContent(stripimeta(event.content, event.tags), []);
|
|
||||||
if (firstUrl) {
|
|
||||||
await unfurlCardCached(firstUrl, signal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Determine if the event is being received in a timely manner. */
|
|
||||||
function isFresh(event: NostrEvent): boolean {
|
|
||||||
return eventAge(event) < Time.minutes(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function webPush(event: NostrEvent): Promise<void> {
|
|
||||||
if (!isFresh(event)) {
|
|
||||||
throw new RelayError('invalid', 'event too old');
|
|
||||||
}
|
|
||||||
|
|
||||||
const kysely = await Storages.kysely();
|
|
||||||
const pubkeys = getTagSet(event.tags, 'p');
|
|
||||||
|
|
||||||
if (!pubkeys.size) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await kysely
|
|
||||||
.selectFrom('push_subscriptions')
|
|
||||||
.selectAll()
|
|
||||||
.where('pubkey', 'in', [...pubkeys])
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const viewerPubkey = row.pubkey;
|
|
||||||
|
|
||||||
if (viewerPubkey === event.pubkey) {
|
|
||||||
continue; // Don't notify authors about their own events.
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = await renderWebPushNotification(event, viewerPubkey);
|
|
||||||
if (!message) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscription = {
|
|
||||||
endpoint: row.endpoint,
|
|
||||||
keys: {
|
|
||||||
auth: row.auth,
|
|
||||||
p256dh: row.p256dh,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await DittoPush.push(subscription, message);
|
|
||||||
webPushNotificationsCounter.inc({ type: message.notification_type });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateSetEvents(event: NostrEvent): Promise<void> {
|
|
||||||
const signer = Conf.signer;
|
|
||||||
const pubkey = await signer.getPublicKey();
|
|
||||||
|
|
||||||
const tagsAdmin = event.tags.some(([name, value]) => ['p', 'P'].includes(name) && value === pubkey);
|
|
||||||
|
|
||||||
if (event.kind === 1984 && tagsAdmin) {
|
|
||||||
const rel = await signer.signEvent({
|
|
||||||
kind: 30383,
|
|
||||||
content: '',
|
|
||||||
tags: [
|
|
||||||
['d', event.id],
|
|
||||||
['p', event.pubkey],
|
|
||||||
['k', '1984'],
|
|
||||||
['n', 'open'],
|
|
||||||
...[...getTagSet(event.tags, 'p')].map((value) => ['P', value]),
|
|
||||||
...[...getTagSet(event.tags, 'e')].map((value) => ['e', value]),
|
|
||||||
],
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
});
|
|
||||||
|
|
||||||
await handleEvent(rel, { source: 'pipeline', signal: AbortSignal.timeout(1000) });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.kind === 3036 && tagsAdmin) {
|
|
||||||
const rel = await signer.signEvent({
|
|
||||||
kind: 30383,
|
|
||||||
content: '',
|
|
||||||
tags: [
|
|
||||||
['d', event.id],
|
|
||||||
['p', event.pubkey],
|
|
||||||
['k', '3036'],
|
|
||||||
['n', 'pending'],
|
|
||||||
],
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
});
|
|
||||||
|
|
||||||
await handleEvent(rel, { source: 'pipeline', signal: AbortSignal.timeout(1000) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Stores the event in the 'event_zaps' table */
|
|
||||||
async function handleZaps(kysely: Kysely<DittoTables>, event: NostrEvent) {
|
|
||||||
if (event.kind !== 9735) return;
|
|
||||||
|
|
||||||
const zapRequestString = event?.tags?.find(([name]) => name === 'description')?.[1];
|
|
||||||
if (!zapRequestString) return;
|
|
||||||
const zapRequest = n.json().pipe(n.event()).optional().catch(undefined).parse(zapRequestString);
|
|
||||||
if (!zapRequest) return;
|
|
||||||
|
|
||||||
const amountSchema = z.coerce.number().int().nonnegative().catch(0);
|
|
||||||
const amount_millisats = amountSchema.parse(getAmount(event?.tags.find(([name]) => name === 'bolt11')?.[1]));
|
|
||||||
if (!amount_millisats || amount_millisats < 1) return;
|
|
||||||
|
|
||||||
const zappedEventId = zapRequest.tags.find(([name]) => name === 'e')?.[1];
|
|
||||||
if (!zappedEventId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await kysely.insertInto('event_zaps').values({
|
|
||||||
receipt_id: event.id,
|
|
||||||
target_event_id: zappedEventId,
|
|
||||||
sender_pubkey: zapRequest.pubkey,
|
|
||||||
amount_millisats,
|
|
||||||
comment: zapRequest.content,
|
|
||||||
}).execute();
|
|
||||||
} catch {
|
|
||||||
// receipt_id is unique, do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { handleEvent, handleZaps, updateAuthorData };
|
|
||||||
|
|
@ -1,73 +1,55 @@
|
||||||
|
import { DittoDB } from '@ditto/db';
|
||||||
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify';
|
import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify';
|
||||||
|
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { fallbackAuthor } from '@/utils.ts';
|
|
||||||
import { findReplyTag, getTagSet } from '@/utils/tags.ts';
|
import { findReplyTag, getTagSet } from '@/utils/tags.ts';
|
||||||
|
|
||||||
interface GetEventOpts {
|
interface GetEventOpts {
|
||||||
/** Signal to abort the request. */
|
db: DittoDB;
|
||||||
|
conf: DittoConf;
|
||||||
|
relay: NStore;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
/** Event kind. */
|
|
||||||
kind?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a Nostr event by its ID.
|
* Get a Nostr event by its ID.
|
||||||
* @deprecated Use `relay.query` directly.
|
* @deprecated Use `relay.query` directly.
|
||||||
*/
|
*/
|
||||||
const getEvent = async (
|
async function getEvent(id: string, opts: GetEventOpts): Promise<DittoEvent | undefined> {
|
||||||
id: string,
|
|
||||||
opts: GetEventOpts = {},
|
|
||||||
): Promise<DittoEvent | undefined> => {
|
|
||||||
const relay = await Storages.db();
|
|
||||||
const { kind, signal = AbortSignal.timeout(1000) } = opts;
|
|
||||||
|
|
||||||
const filter: NostrFilter = { ids: [id], limit: 1 };
|
const filter: NostrFilter = { ids: [id], limit: 1 };
|
||||||
if (kind) {
|
const [event] = await opts.relay.query([filter], opts);
|
||||||
filter.kinds = [kind];
|
hydrateEvents({ ...opts, events: [event] });
|
||||||
}
|
return event;
|
||||||
|
}
|
||||||
return await relay.query([filter], { limit: 1, signal })
|
|
||||||
.then((events) => hydrateEvents({ events, relay, signal }))
|
|
||||||
.then(([event]) => event);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a Nostr `set_medatadata` event for a user's pubkey.
|
* Get a Nostr `set_medatadata` event for a user's pubkey.
|
||||||
* @deprecated Use `relay.query` directly.
|
* @deprecated Use `relay.query` directly.
|
||||||
*/
|
*/
|
||||||
async function getAuthor(pubkey: string, opts: GetEventOpts = {}): Promise<NostrEvent | undefined> {
|
async function getAuthor(pubkey: string, opts: GetEventOpts): Promise<NostrEvent | undefined> {
|
||||||
const relay = await Storages.db();
|
const [event] = await opts.relay.query([{ authors: [pubkey], kinds: [0], limit: 1 }], opts);
|
||||||
const { signal = AbortSignal.timeout(1000) } = opts;
|
hydrateEvents({ ...opts, events: [event] });
|
||||||
|
|
||||||
const events = await relay.query([{ authors: [pubkey], kinds: [0], limit: 1 }], { limit: 1, signal });
|
|
||||||
const event = events[0] ?? fallbackAuthor(pubkey);
|
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], relay, signal });
|
|
||||||
|
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get users the given pubkey follows. */
|
/** Get users the given pubkey follows. */
|
||||||
const getFollows = async (pubkey: string, signal?: AbortSignal): Promise<NostrEvent | undefined> => {
|
const getFollows = async (relay: NStore, pubkey: string, signal?: AbortSignal): Promise<NostrEvent | undefined> => {
|
||||||
const store = await Storages.db();
|
const [event] = await relay.query([{ authors: [pubkey], kinds: [3], limit: 1 }], { signal });
|
||||||
const [event] = await store.query([{ authors: [pubkey], kinds: [3], limit: 1 }], { limit: 1, signal });
|
|
||||||
return event;
|
return event;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Get pubkeys the user follows. */
|
/** Get pubkeys the user follows. */
|
||||||
async function getFollowedPubkeys(pubkey: string, signal?: AbortSignal): Promise<Set<string>> {
|
async function getFollowedPubkeys(relay: NStore, pubkey: string, signal?: AbortSignal): Promise<Set<string>> {
|
||||||
const event = await getFollows(pubkey, signal);
|
const event = await getFollows(relay, pubkey, signal);
|
||||||
if (!event) return new Set();
|
if (!event) return new Set();
|
||||||
return getTagSet(event.tags, 'p');
|
return getTagSet(event.tags, 'p');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get pubkeys the user follows, including the user's own pubkey. */
|
/** Get pubkeys the user follows, including the user's own pubkey. */
|
||||||
async function getFeedPubkeys(pubkey: string): Promise<Set<string>> {
|
async function getFeedPubkeys(relay: NStore, pubkey: string): Promise<Set<string>> {
|
||||||
const authors = await getFollowedPubkeys(pubkey);
|
const authors = await getFollowedPubkeys(relay, pubkey);
|
||||||
return authors.add(pubkey);
|
return authors.add(pubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,34 +74,11 @@ async function getAncestors(store: NStore, event: NostrEvent, result: NostrEvent
|
||||||
async function getDescendants(
|
async function getDescendants(
|
||||||
store: NStore,
|
store: NStore,
|
||||||
event: NostrEvent,
|
event: NostrEvent,
|
||||||
signal = AbortSignal.timeout(2000),
|
signal?: AbortSignal,
|
||||||
): Promise<NostrEvent[]> {
|
): Promise<NostrEvent[]> {
|
||||||
return await store
|
return await store
|
||||||
.query([{ kinds: [1], '#e': [event.id], since: event.created_at, limit: 200 }], { signal })
|
.query([{ kinds: [1], '#e': [event.id], since: event.created_at, limit: 200 }], { signal })
|
||||||
.then((events) => events.filter(({ tags }) => findReplyTag(tags)?.[1] === event.id));
|
.then((events) => events.filter(({ tags }) => findReplyTag(tags)?.[1] === event.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns whether the pubkey is followed by a local user. */
|
export { getAncestors, getAuthor, getDescendants, getEvent, getFeedPubkeys, getFollowedPubkeys, getFollows };
|
||||||
async function isLocallyFollowed(pubkey: string): Promise<boolean> {
|
|
||||||
const { host } = Conf.url;
|
|
||||||
|
|
||||||
const store = await Storages.db();
|
|
||||||
|
|
||||||
const [event] = await store.query(
|
|
||||||
[{ kinds: [3], '#p': [pubkey], search: `domain:${host}`, limit: 1 }],
|
|
||||||
{ limit: 1 },
|
|
||||||
);
|
|
||||||
|
|
||||||
return Boolean(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
getAncestors,
|
|
||||||
getAuthor,
|
|
||||||
getDescendants,
|
|
||||||
getEvent,
|
|
||||||
getFeedPubkeys,
|
|
||||||
getFollowedPubkeys,
|
|
||||||
getFollows,
|
|
||||||
isLocallyFollowed,
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
// deno-lint-ignore-file require-await
|
// deno-lint-ignore-file require-await
|
||||||
import { HTTPException } from '@hono/hono/http-exception';
|
import { HTTPException } from '@hono/hono/http-exception';
|
||||||
import { NConnectSigner, NostrEvent, NostrSigner } from '@nostrify/nostrify';
|
import { NConnectSigner, NostrEvent, NostrSigner, NRelay } from '@nostrify/nostrify';
|
||||||
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
|
|
||||||
interface ConnectSignerOpts {
|
interface ConnectSignerOpts {
|
||||||
bunkerPubkey: string;
|
bunkerPubkey: string;
|
||||||
userPubkey: string;
|
userPubkey: string;
|
||||||
signer: NostrSigner;
|
signer: NostrSigner;
|
||||||
|
relay: NRelay;
|
||||||
relays?: string[];
|
relays?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17,27 +16,23 @@ interface ConnectSignerOpts {
|
||||||
* Simple extension of nostrify's `NConnectSigner`, with our options to keep it DRY.
|
* Simple extension of nostrify's `NConnectSigner`, with our options to keep it DRY.
|
||||||
*/
|
*/
|
||||||
export class ConnectSigner implements NostrSigner {
|
export class ConnectSigner implements NostrSigner {
|
||||||
private signer: Promise<NConnectSigner>;
|
private signer: NConnectSigner;
|
||||||
|
|
||||||
constructor(private opts: ConnectSignerOpts) {
|
constructor(private opts: ConnectSignerOpts) {
|
||||||
this.signer = this.init(opts.signer);
|
const { relay, signer } = this.opts;
|
||||||
}
|
|
||||||
|
|
||||||
async init(signer: NostrSigner): Promise<NConnectSigner> {
|
this.signer = new NConnectSigner({
|
||||||
return new NConnectSigner({
|
|
||||||
encryption: 'nip44',
|
encryption: 'nip44',
|
||||||
pubkey: this.opts.bunkerPubkey,
|
pubkey: this.opts.bunkerPubkey,
|
||||||
// TODO: use a remote relay for `nprofile` signing (if present and `Conf.relay` isn't already in the list)
|
relay,
|
||||||
relay: await Storages.db(),
|
|
||||||
signer,
|
signer,
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async signEvent(event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>): Promise<NostrEvent> {
|
async signEvent(event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>): Promise<NostrEvent> {
|
||||||
const signer = await this.signer;
|
|
||||||
try {
|
try {
|
||||||
return await signer.signEvent(event);
|
return await this.signer.signEvent(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.name === 'AbortError') {
|
if (e instanceof Error && e.name === 'AbortError') {
|
||||||
throw new HTTPException(408, { message: 'The event was not signed quickly enough' });
|
throw new HTTPException(408, { message: 'The event was not signed quickly enough' });
|
||||||
|
|
@ -49,9 +44,8 @@ export class ConnectSigner implements NostrSigner {
|
||||||
|
|
||||||
readonly nip04 = {
|
readonly nip04 = {
|
||||||
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
|
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
|
||||||
const signer = await this.signer;
|
|
||||||
try {
|
try {
|
||||||
return await signer.nip04.encrypt(pubkey, plaintext);
|
return await this.signer.nip04.encrypt(pubkey, plaintext);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.name === 'AbortError') {
|
if (e instanceof Error && e.name === 'AbortError') {
|
||||||
throw new HTTPException(408, {
|
throw new HTTPException(408, {
|
||||||
|
|
@ -64,9 +58,8 @@ export class ConnectSigner implements NostrSigner {
|
||||||
},
|
},
|
||||||
|
|
||||||
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
|
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
|
||||||
const signer = await this.signer;
|
|
||||||
try {
|
try {
|
||||||
return await signer.nip04.decrypt(pubkey, ciphertext);
|
return await this.signer.nip04.decrypt(pubkey, ciphertext);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.name === 'AbortError') {
|
if (e instanceof Error && e.name === 'AbortError') {
|
||||||
throw new HTTPException(408, {
|
throw new HTTPException(408, {
|
||||||
|
|
@ -81,9 +74,8 @@ export class ConnectSigner implements NostrSigner {
|
||||||
|
|
||||||
readonly nip44 = {
|
readonly nip44 = {
|
||||||
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
|
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
|
||||||
const signer = await this.signer;
|
|
||||||
try {
|
try {
|
||||||
return await signer.nip44.encrypt(pubkey, plaintext);
|
return await this.signer.nip44.encrypt(pubkey, plaintext);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.name === 'AbortError') {
|
if (e instanceof Error && e.name === 'AbortError') {
|
||||||
throw new HTTPException(408, {
|
throw new HTTPException(408, {
|
||||||
|
|
@ -96,9 +88,8 @@ export class ConnectSigner implements NostrSigner {
|
||||||
},
|
},
|
||||||
|
|
||||||
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
|
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
|
||||||
const signer = await this.signer;
|
|
||||||
try {
|
try {
|
||||||
return await signer.nip44.decrypt(pubkey, ciphertext);
|
return await this.signer.nip44.decrypt(pubkey, ciphertext);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.name === 'AbortError') {
|
if (e instanceof Error && e.name === 'AbortError') {
|
||||||
throw new HTTPException(408, {
|
throw new HTTPException(408, {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
// Starts up applications required to run before the HTTP server is on.
|
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { cron } from '@/cron.ts';
|
|
||||||
import { startFirehose } from '@/firehose.ts';
|
|
||||||
|
|
||||||
if (Conf.firehoseEnabled) {
|
|
||||||
startFirehose();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Conf.cronEnabled) {
|
|
||||||
cron();
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
// deno-lint-ignore-file require-await
|
|
||||||
import { type DittoDB, DittoPolyPg } from '@ditto/db';
|
|
||||||
import { NPool, NRelay1 } from '@nostrify/nostrify';
|
|
||||||
|
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { DittoPgStore } from '@/storages/DittoPgStore.ts';
|
|
||||||
import { seedZapSplits } from '@/utils/zap-split.ts';
|
|
||||||
import { DittoPool } from '@/storages/DittoPool.ts';
|
|
||||||
|
|
||||||
export class Storages {
|
|
||||||
private static _db: Promise<DittoPgStore> | undefined;
|
|
||||||
private static _database: Promise<DittoDB> | undefined;
|
|
||||||
private static _client: Promise<NPool<NRelay1>> | undefined;
|
|
||||||
|
|
||||||
public static async database(): Promise<DittoDB> {
|
|
||||||
if (!this._database) {
|
|
||||||
this._database = (async () => {
|
|
||||||
const db = DittoPolyPg.create(Conf.databaseUrl, {
|
|
||||||
poolSize: Conf.pg.poolSize,
|
|
||||||
debug: Conf.pgliteDebug,
|
|
||||||
});
|
|
||||||
await DittoPolyPg.migrate(db.kysely);
|
|
||||||
return db;
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
return this._database;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async kysely(): Promise<DittoDB['kysely']> {
|
|
||||||
const { kysely } = await this.database();
|
|
||||||
return kysely;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** SQL database to store events this Ditto server cares about. */
|
|
||||||
public static async db(): Promise<DittoPgStore> {
|
|
||||||
if (!this._db) {
|
|
||||||
this._db = (async () => {
|
|
||||||
const db = await this.database();
|
|
||||||
const store = new DittoPgStore({
|
|
||||||
db,
|
|
||||||
pubkey: await Conf.signer.getPublicKey(),
|
|
||||||
timeout: Conf.db.timeouts.default,
|
|
||||||
notify: Conf.notifyEnabled,
|
|
||||||
});
|
|
||||||
await seedZapSplits(store);
|
|
||||||
return store;
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
return this._db;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Relay pool storage. */
|
|
||||||
public static async client(): Promise<NPool<NRelay1>> {
|
|
||||||
if (!this._client) {
|
|
||||||
this._client = (async () => {
|
|
||||||
const relay = await this.db();
|
|
||||||
return new DittoPool({ conf: Conf, relay });
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
return this._client;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import { DittoConf } from '@ditto/conf';
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { DittoDB, DittoTables } from '@ditto/db';
|
import { DittoDB, DittoTables } from '@ditto/db';
|
||||||
import { pipelineEventsCounter, policyEventsCounter, webPushNotificationsCounter } from '@ditto/metrics';
|
import {
|
||||||
|
cachedFaviconsSizeGauge,
|
||||||
|
cachedNip05sSizeGauge,
|
||||||
|
pipelineEventsCounter,
|
||||||
|
policyEventsCounter,
|
||||||
|
webPushNotificationsCounter,
|
||||||
|
} from '@ditto/metrics';
|
||||||
import {
|
import {
|
||||||
NKinds,
|
NKinds,
|
||||||
NostrEvent,
|
NostrEvent,
|
||||||
|
|
@ -22,18 +28,20 @@ import { DittoPush } from '@/DittoPush.ts';
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { RelayError } from '@/RelayError.ts';
|
import { RelayError } from '@/RelayError.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { eventAge, Time } from '@/utils.ts';
|
import { eventAge, nostrNow, Time } from '@/utils.ts';
|
||||||
import { getAmount } from '@/utils/bolt11.ts';
|
import { getAmount } from '@/utils/bolt11.ts';
|
||||||
import { errorJson } from '@/utils/log.ts';
|
import { errorJson } from '@/utils/log.ts';
|
||||||
import { purifyEvent } from '@/utils/purify.ts';
|
import { purifyEvent } from '@/utils/purify.ts';
|
||||||
import { getTagSet } from '@/utils/tags.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
import { policyWorker } from '@/workers/policy.ts';
|
import { policyWorker } from '@/workers/policy.ts';
|
||||||
import { verifyEventWorker } from '@/workers/verify.ts';
|
import { verifyEventWorker } from '@/workers/verify.ts';
|
||||||
import { faviconCache } from '@/utils/favicon.ts';
|
import { fetchFavicon, insertFavicon, queryFavicon } from '@/utils/favicon.ts';
|
||||||
|
import { lookupNip05 } from '@/utils/nip05.ts';
|
||||||
import { parseNoteContent, stripimeta } from '@/utils/note.ts';
|
import { parseNoteContent, stripimeta } from '@/utils/note.ts';
|
||||||
|
import { SimpleLRU } from '@/utils/SimpleLRU.ts';
|
||||||
import { unfurlCardCached } from '@/utils/unfurl.ts';
|
import { unfurlCardCached } from '@/utils/unfurl.ts';
|
||||||
import { nip05Cache } from '@/utils/nip05.ts';
|
|
||||||
import { renderWebPushNotification } from '@/views/mastodon/push.ts';
|
import { renderWebPushNotification } from '@/views/mastodon/push.ts';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
interface DittoAPIStoreOpts {
|
interface DittoAPIStoreOpts {
|
||||||
db: DittoDB;
|
db: DittoDB;
|
||||||
|
|
@ -43,15 +51,45 @@ interface DittoAPIStoreOpts {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DittoAPIStore implements NRelay {
|
export class DittoAPIStore implements NRelay {
|
||||||
|
private push: DittoPush;
|
||||||
private encounters = new LRUCache<string, true>({ max: 5000 });
|
private encounters = new LRUCache<string, true>({ max: 5000 });
|
||||||
private controller = new AbortController();
|
private controller = new AbortController();
|
||||||
|
|
||||||
|
private faviconCache: SimpleLRU<string, URL>;
|
||||||
|
private nip05Cache: SimpleLRU<string, nip19.ProfilePointer>;
|
||||||
|
|
||||||
private ns = 'ditto.apistore';
|
private ns = 'ditto.apistore';
|
||||||
|
|
||||||
constructor(private opts: DittoAPIStoreOpts) {
|
constructor(private opts: DittoAPIStoreOpts) {
|
||||||
|
const { conf, db } = this.opts;
|
||||||
|
|
||||||
|
this.push = new DittoPush(opts);
|
||||||
|
|
||||||
this.listen().catch((e: unknown) => {
|
this.listen().catch((e: unknown) => {
|
||||||
logi({ level: 'error', ns: this.ns, source: 'listen', error: errorJson(e) });
|
logi({ level: 'error', ns: this.ns, source: 'listen', error: errorJson(e) });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.faviconCache = new SimpleLRU<string, URL>(
|
||||||
|
async (domain, { signal }) => {
|
||||||
|
const row = await queryFavicon(db.kysely, domain);
|
||||||
|
|
||||||
|
if (row && (nostrNow() - row.last_updated_at) < (conf.caches.favicon.ttl / 1000)) {
|
||||||
|
return new URL(row.favicon);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await fetchFavicon(domain, signal);
|
||||||
|
await insertFavicon(db.kysely, domain, url.href);
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
{ ...conf.caches.favicon, gauge: cachedFaviconsSizeGauge },
|
||||||
|
);
|
||||||
|
|
||||||
|
this.nip05Cache = new SimpleLRU<string, nip19.ProfilePointer>(
|
||||||
|
(nip05, { signal }) => {
|
||||||
|
return lookupNip05(nip05, { ...this.opts, signal });
|
||||||
|
},
|
||||||
|
{ ...conf.caches.nip05, gauge: cachedNip05sSizeGauge },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
req(
|
req(
|
||||||
|
|
@ -220,7 +258,7 @@ export class DittoAPIStore implements NRelay {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Parse kind 0 metadata and track indexes in the database. */
|
/** Parse kind 0 metadata and track indexes in the database. */
|
||||||
private async updateAuthorData(event: NostrEvent, signal?: AbortSignal): Promise<void> {
|
async updateAuthorData(event: NostrEvent, signal?: AbortSignal): Promise<void> {
|
||||||
if (event.kind !== 0) return;
|
if (event.kind !== 0) return;
|
||||||
|
|
||||||
const { db } = this.opts;
|
const { db } = this.opts;
|
||||||
|
|
@ -247,7 +285,7 @@ export class DittoAPIStore implements NRelay {
|
||||||
if (nip05) {
|
if (nip05) {
|
||||||
const tld = tldts.parse(nip05);
|
const tld = tldts.parse(nip05);
|
||||||
if (tld.isIcann && !tld.isIp && !tld.isPrivate) {
|
if (tld.isIcann && !tld.isIp && !tld.isPrivate) {
|
||||||
const pointer = await nip05Cache.fetch(nip05, { signal });
|
const pointer = await this.nip05Cache.fetch(nip05, { signal });
|
||||||
if (pointer.pubkey === event.pubkey) {
|
if (pointer.pubkey === event.pubkey) {
|
||||||
updates.nip05 = nip05;
|
updates.nip05 = nip05;
|
||||||
updates.nip05_domain = tld.domain;
|
updates.nip05_domain = tld.domain;
|
||||||
|
|
@ -270,7 +308,7 @@ export class DittoAPIStore implements NRelay {
|
||||||
const domain = nip05?.split('@')[1].toLowerCase();
|
const domain = nip05?.split('@')[1].toLowerCase();
|
||||||
if (domain) {
|
if (domain) {
|
||||||
try {
|
try {
|
||||||
await faviconCache.fetch(domain, { signal });
|
await this.faviconCache.fetch(domain, { signal });
|
||||||
} catch {
|
} catch {
|
||||||
// Fallthrough.
|
// Fallthrough.
|
||||||
}
|
}
|
||||||
|
|
@ -352,7 +390,7 @@ export class DittoAPIStore implements NRelay {
|
||||||
throw new RelayError('invalid', 'event too old');
|
throw new RelayError('invalid', 'event too old');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { db } = this.opts;
|
const { db, relay } = this.opts;
|
||||||
const pubkeys = getTagSet(event.tags, 'p');
|
const pubkeys = getTagSet(event.tags, 'p');
|
||||||
|
|
||||||
if (!pubkeys.size) {
|
if (!pubkeys.size) {
|
||||||
|
|
@ -372,7 +410,7 @@ export class DittoAPIStore implements NRelay {
|
||||||
continue; // Don't notify authors about their own events.
|
continue; // Don't notify authors about their own events.
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = await renderWebPushNotification(event, viewerPubkey);
|
const message = await renderWebPushNotification(relay, event, viewerPubkey);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -385,15 +423,14 @@ export class DittoAPIStore implements NRelay {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await DittoPush.push(subscription, message);
|
await this.push.push(subscription, message);
|
||||||
webPushNotificationsCounter.inc({ type: message.notification_type });
|
webPushNotificationsCounter.inc({ type: message.notification_type });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hydrate the event with the user, if applicable. */
|
/** Hydrate the event with the user, if applicable. */
|
||||||
private async hydrateEvent(event: NostrEvent, signal?: AbortSignal): Promise<DittoEvent> {
|
private async hydrateEvent(event: NostrEvent, signal?: AbortSignal): Promise<DittoEvent> {
|
||||||
const { relay } = this.opts;
|
const [hydrated] = await hydrateEvents({ ...this.opts, events: [event], signal });
|
||||||
const [hydrated] = await hydrateEvents({ events: [event], relay, signal });
|
|
||||||
return hydrated;
|
return hydrated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -402,9 +439,17 @@ export class DittoAPIStore implements NRelay {
|
||||||
return eventAge(event) < Time.minutes(1);
|
return eventAge(event) < Time.minutes(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
query(filters: NostrFilter[], opts?: { signal?: AbortSignal }): Promise<NostrEvent[]> {
|
async query(filters: NostrFilter[], opts: { pure?: boolean; signal?: AbortSignal } = {}): Promise<DittoEvent[]> {
|
||||||
const { relay } = this.opts;
|
const { relay } = this.opts;
|
||||||
return relay.query(filters, opts);
|
const { pure = true, signal } = opts; // TODO: make pure `false` by default
|
||||||
|
|
||||||
|
const events = await relay.query(filters, opts);
|
||||||
|
|
||||||
|
if (!pure) {
|
||||||
|
return hydrateEvents({ ...this.opts, events, signal });
|
||||||
|
}
|
||||||
|
|
||||||
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
count(filters: NostrFilter[], opts?: { signal?: AbortSignal }): Promise<NostrRelayCOUNT[2]> {
|
count(filters: NostrFilter[], opts?: { signal?: AbortSignal }): Promise<NostrRelayCOUNT[2]> {
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ interface DittoPgStoreOpts {
|
||||||
/** Pubkey of the admin account. */
|
/** Pubkey of the admin account. */
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
/** 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. */
|
||||||
pure?: boolean;
|
pure?: boolean;
|
||||||
/** Chunk size for streaming events. Defaults to 20. */
|
/** Chunk size for streaming events. Defaults to 20. */
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
|
import { DittoConf } from '@ditto/conf';
|
||||||
|
import { DummyDB } from '@ditto/db';
|
||||||
import { MockRelay } from '@nostrify/nostrify/test';
|
import { MockRelay } from '@nostrify/nostrify/test';
|
||||||
import { assertEquals } from '@std/assert';
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { createTestDB, eventFixture } from '@/test.ts';
|
import { eventFixture } from '@/test.ts';
|
||||||
|
|
||||||
Deno.test('hydrateEvents(): author --- WITHOUT stats', async () => {
|
Deno.test('hydrateEvents(): author --- WITHOUT stats', async () => {
|
||||||
const relay = new MockRelay();
|
const opts = setupTest();
|
||||||
await using db = await createTestDB();
|
const { relay } = opts;
|
||||||
|
|
||||||
const event0 = await eventFixture('event-0');
|
const event0 = await eventFixture('event-0');
|
||||||
const event1 = await eventFixture('event-1');
|
const event1 = await eventFixture('event-1');
|
||||||
|
|
@ -16,19 +18,15 @@ Deno.test('hydrateEvents(): author --- WITHOUT stats', async () => {
|
||||||
await relay.event(event0);
|
await relay.event(event0);
|
||||||
await relay.event(event1);
|
await relay.event(event1);
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({ ...opts, events: [event1] });
|
||||||
events: [event1],
|
|
||||||
relay,
|
|
||||||
kysely: db.kysely,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedEvent = { ...event1, author: event0 };
|
const expectedEvent = { ...event1, author: event0 };
|
||||||
assertEquals(event1, expectedEvent);
|
assertEquals(event1, expectedEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('hydrateEvents(): repost --- WITHOUT stats', async () => {
|
Deno.test('hydrateEvents(): repost --- WITHOUT stats', async () => {
|
||||||
const relay = new MockRelay();
|
const opts = setupTest();
|
||||||
await using db = await createTestDB();
|
const { relay } = opts;
|
||||||
|
|
||||||
const event0madePost = await eventFixture('event-0-the-one-who-post-and-users-repost');
|
const event0madePost = await eventFixture('event-0-the-one-who-post-and-users-repost');
|
||||||
const event0madeRepost = await eventFixture('event-0-the-one-who-repost');
|
const event0madeRepost = await eventFixture('event-0-the-one-who-repost');
|
||||||
|
|
@ -41,23 +39,20 @@ Deno.test('hydrateEvents(): repost --- WITHOUT stats', async () => {
|
||||||
await relay.event(event1reposted);
|
await relay.event(event1reposted);
|
||||||
await relay.event(event6);
|
await relay.event(event6);
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({ ...opts, events: [event6] });
|
||||||
events: [event6],
|
|
||||||
relay,
|
|
||||||
kysely: db.kysely,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedEvent6 = {
|
const expectedEvent6 = {
|
||||||
...event6,
|
...event6,
|
||||||
author: event0madeRepost,
|
author: event0madeRepost,
|
||||||
repost: { ...event1reposted, author: event0madePost },
|
repost: { ...event1reposted, author: event0madePost },
|
||||||
};
|
};
|
||||||
|
|
||||||
assertEquals(event6, expectedEvent6);
|
assertEquals(event6, expectedEvent6);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('hydrateEvents(): quote repost --- WITHOUT stats', async () => {
|
Deno.test('hydrateEvents(): quote repost --- WITHOUT stats', async () => {
|
||||||
const relay = new MockRelay();
|
const opts = setupTest();
|
||||||
await using db = await createTestDB();
|
const { relay } = opts;
|
||||||
|
|
||||||
const event0madeQuoteRepost = await eventFixture('event-0-the-one-who-quote-repost');
|
const event0madeQuoteRepost = await eventFixture('event-0-the-one-who-quote-repost');
|
||||||
const event0 = await eventFixture('event-0');
|
const event0 = await eventFixture('event-0');
|
||||||
|
|
@ -70,11 +65,7 @@ Deno.test('hydrateEvents(): quote repost --- WITHOUT stats', async () => {
|
||||||
await relay.event(event1quoteRepost);
|
await relay.event(event1quoteRepost);
|
||||||
await relay.event(event1willBeQuoteReposted);
|
await relay.event(event1willBeQuoteReposted);
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({ ...opts, events: [event1quoteRepost] });
|
||||||
events: [event1quoteRepost],
|
|
||||||
relay,
|
|
||||||
kysely: db.kysely,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedEvent1quoteRepost = {
|
const expectedEvent1quoteRepost = {
|
||||||
...event1quoteRepost,
|
...event1quoteRepost,
|
||||||
|
|
@ -86,8 +77,8 @@ Deno.test('hydrateEvents(): quote repost --- WITHOUT stats', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('hydrateEvents(): repost of quote repost --- WITHOUT stats', async () => {
|
Deno.test('hydrateEvents(): repost of quote repost --- WITHOUT stats', async () => {
|
||||||
const relay = new MockRelay();
|
const opts = setupTest();
|
||||||
await using db = await createTestDB();
|
const { relay } = opts;
|
||||||
|
|
||||||
const author = await eventFixture('event-0-makes-repost-with-quote-repost');
|
const author = await eventFixture('event-0-makes-repost-with-quote-repost');
|
||||||
const event1 = await eventFixture('event-1-will-be-reposted-with-quote-repost');
|
const event1 = await eventFixture('event-1-will-be-reposted-with-quote-repost');
|
||||||
|
|
@ -100,23 +91,20 @@ Deno.test('hydrateEvents(): repost of quote repost --- WITHOUT stats', async ()
|
||||||
await relay.event(event1quote);
|
await relay.event(event1quote);
|
||||||
await relay.event(event6);
|
await relay.event(event6);
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({ ...opts, events: [event6] });
|
||||||
events: [event6],
|
|
||||||
relay,
|
|
||||||
kysely: db.kysely,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedEvent6 = {
|
const expectedEvent6 = {
|
||||||
...event6,
|
...event6,
|
||||||
author,
|
author,
|
||||||
repost: { ...event1quote, author, quote: { author, ...event1 } },
|
repost: { ...event1quote, author, quote: { author, ...event1 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
assertEquals(event6, expectedEvent6);
|
assertEquals(event6, expectedEvent6);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('hydrateEvents(): report pubkey and post // kind 1984 --- WITHOUT stats', async () => {
|
Deno.test('hydrateEvents(): report pubkey and post // kind 1984 --- WITHOUT stats', async () => {
|
||||||
const relay = new MockRelay();
|
const opts = setupTest();
|
||||||
await using db = await createTestDB();
|
const { relay } = opts;
|
||||||
|
|
||||||
const authorDictator = await eventFixture('kind-0-dictator');
|
const authorDictator = await eventFixture('kind-0-dictator');
|
||||||
const authorVictim = await eventFixture('kind-0-george-orwell');
|
const authorVictim = await eventFixture('kind-0-george-orwell');
|
||||||
|
|
@ -129,11 +117,7 @@ Deno.test('hydrateEvents(): report pubkey and post // kind 1984 --- WITHOUT stat
|
||||||
await relay.event(reportEvent);
|
await relay.event(reportEvent);
|
||||||
await relay.event(event1);
|
await relay.event(event1);
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({ ...opts, events: [reportEvent] });
|
||||||
events: [reportEvent],
|
|
||||||
relay,
|
|
||||||
kysely: db.kysely,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedEvent: DittoEvent = {
|
const expectedEvent: DittoEvent = {
|
||||||
...reportEvent,
|
...reportEvent,
|
||||||
|
|
@ -141,12 +125,13 @@ Deno.test('hydrateEvents(): report pubkey and post // kind 1984 --- WITHOUT stat
|
||||||
reported_notes: [event1],
|
reported_notes: [event1],
|
||||||
reported_profile: authorVictim,
|
reported_profile: authorVictim,
|
||||||
};
|
};
|
||||||
|
|
||||||
assertEquals(reportEvent, expectedEvent);
|
assertEquals(reportEvent, expectedEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('hydrateEvents(): zap sender, zap amount, zapped post // kind 9735 --- WITHOUT stats', async () => {
|
Deno.test('hydrateEvents(): zap sender, zap amount, zapped post // kind 9735 --- WITHOUT stats', async () => {
|
||||||
const relay = new MockRelay();
|
const opts = setupTest();
|
||||||
await using db = await createTestDB();
|
const { relay } = opts;
|
||||||
|
|
||||||
const zapSender = await eventFixture('kind-0-jack');
|
const zapSender = await eventFixture('kind-0-jack');
|
||||||
const zapReceipt = await eventFixture('kind-9735-jack-zap-patrick');
|
const zapReceipt = await eventFixture('kind-9735-jack-zap-patrick');
|
||||||
|
|
@ -159,11 +144,7 @@ Deno.test('hydrateEvents(): zap sender, zap amount, zapped post // kind 9735 ---
|
||||||
await relay.event(zappedPost);
|
await relay.event(zappedPost);
|
||||||
await relay.event(zapReceiver);
|
await relay.event(zapReceiver);
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({ ...opts, events: [zapReceipt] });
|
||||||
events: [zapReceipt],
|
|
||||||
relay,
|
|
||||||
kysely: db.kysely,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedEvent: DittoEvent = {
|
const expectedEvent: DittoEvent = {
|
||||||
...zapReceipt,
|
...zapReceipt,
|
||||||
|
|
@ -175,5 +156,14 @@ Deno.test('hydrateEvents(): zap sender, zap amount, zapped post // kind 9735 ---
|
||||||
zap_amount: 5225000, // millisats
|
zap_amount: 5225000, // millisats
|
||||||
zap_message: '🫂',
|
zap_message: '🫂',
|
||||||
};
|
};
|
||||||
|
|
||||||
assertEquals(zapReceipt, expectedEvent);
|
assertEquals(zapReceipt, expectedEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setupTest() {
|
||||||
|
const db = new DummyDB();
|
||||||
|
const conf = new DittoConf(new Map());
|
||||||
|
const relay = new MockRelay();
|
||||||
|
|
||||||
|
return { conf, db, relay };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
import { DittoTables } from '@ditto/db';
|
import { DittoDB, DittoTables } from '@ditto/db';
|
||||||
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { NStore } from '@nostrify/nostrify';
|
import { NStore } from '@nostrify/nostrify';
|
||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import { matchFilter } from 'nostr-tools';
|
import { matchFilter } from 'nostr-tools';
|
||||||
import { NSchema as n } from '@nostrify/nostrify';
|
import { NSchema as n } from '@nostrify/nostrify';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { fallbackAuthor } from '@/utils.ts';
|
import { fallbackAuthor } from '@/utils.ts';
|
||||||
import { findQuoteTag } from '@/utils/tags.ts';
|
import { findQuoteTag } from '@/utils/tags.ts';
|
||||||
import { findQuoteInContent } from '@/utils/note.ts';
|
import { findQuoteInContent } from '@/utils/note.ts';
|
||||||
import { getAmount } from '@/utils/bolt11.ts';
|
import { getAmount } from '@/utils/bolt11.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
|
|
||||||
interface HydrateOpts {
|
interface HydrateOpts {
|
||||||
events: DittoEvent[];
|
db: DittoDB;
|
||||||
|
conf: DittoConf;
|
||||||
relay: NStore;
|
relay: NStore;
|
||||||
|
events: DittoEvent[];
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
kysely?: Kysely<DittoTables>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hydrate events using the provided storage. */
|
/** Hydrate events using the provided storage. */
|
||||||
async function hydrateEvents(opts: HydrateOpts): Promise<DittoEvent[]> {
|
async function hydrateEvents(opts: HydrateOpts): Promise<DittoEvent[]> {
|
||||||
const { events, relay, signal, kysely = await Storages.kysely() } = opts;
|
const { conf, db, events } = opts;
|
||||||
|
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
return events;
|
return events;
|
||||||
|
|
@ -30,28 +30,28 @@ async function hydrateEvents(opts: HydrateOpts): Promise<DittoEvent[]> {
|
||||||
|
|
||||||
const cache = [...events];
|
const cache = [...events];
|
||||||
|
|
||||||
for (const event of await gatherRelatedEvents({ events: cache, relay, signal })) {
|
for (const event of await gatherRelatedEvents({ ...opts, events: cache })) {
|
||||||
cache.push(event);
|
cache.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of await gatherQuotes({ events: cache, relay, signal })) {
|
for (const event of await gatherQuotes({ ...opts, events: cache })) {
|
||||||
cache.push(event);
|
cache.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of await gatherProfiles({ events: cache, relay, signal })) {
|
for (const event of await gatherProfiles({ ...opts, events: cache })) {
|
||||||
cache.push(event);
|
cache.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of await gatherUsers({ events: cache, relay, signal })) {
|
for (const event of await gatherUsers({ ...opts, events: cache })) {
|
||||||
cache.push(event);
|
cache.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of await gatherInfo({ events: cache, relay, signal })) {
|
for (const event of await gatherInfo({ ...opts, events: cache })) {
|
||||||
cache.push(event);
|
cache.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorStats = await gatherAuthorStats(cache, kysely as Kysely<DittoTables>);
|
const authorStats = await gatherAuthorStats(cache, db.kysely);
|
||||||
const eventStats = await gatherEventStats(cache, kysely as Kysely<DittoTables>);
|
const eventStats = await gatherEventStats(cache, db.kysely);
|
||||||
|
|
||||||
const domains = authorStats.reduce((result, { nip05_hostname }) => {
|
const domains = authorStats.reduce((result, { nip05_hostname }) => {
|
||||||
if (nip05_hostname) result.add(nip05_hostname);
|
if (nip05_hostname) result.add(nip05_hostname);
|
||||||
|
|
@ -59,7 +59,7 @@ async function hydrateEvents(opts: HydrateOpts): Promise<DittoEvent[]> {
|
||||||
}, new Set<string>());
|
}, new Set<string>());
|
||||||
|
|
||||||
const favicons = (
|
const favicons = (
|
||||||
await kysely
|
await db.kysely
|
||||||
.selectFrom('domain_favicons')
|
.selectFrom('domain_favicons')
|
||||||
.select(['domain', 'favicon'])
|
.select(['domain', 'favicon'])
|
||||||
.where('domain', 'in', [...domains])
|
.where('domain', 'in', [...domains])
|
||||||
|
|
@ -79,7 +79,7 @@ async function hydrateEvents(opts: HydrateOpts): Promise<DittoEvent[]> {
|
||||||
// Dedupe events.
|
// Dedupe events.
|
||||||
const results = [...new Map(cache.map((event) => [event.id, event])).values()];
|
const results = [...new Map(cache.map((event) => [event.id, event])).values()];
|
||||||
|
|
||||||
const admin = await Conf.signer.getPublicKey();
|
const admin = await conf.signer.getPublicKey();
|
||||||
|
|
||||||
// First connect all the events to each-other, then connect the connected events to the original list.
|
// First connect all the events to each-other, then connect the connected events to the original list.
|
||||||
assembleEvents(admin, results, results, stats);
|
assembleEvents(admin, results, results, stats);
|
||||||
|
|
@ -317,7 +317,7 @@ async function gatherProfiles({ events, relay, signal }: HydrateOpts): Promise<D
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Collect users from the events. */
|
/** Collect users from the events. */
|
||||||
async function gatherUsers({ events, relay, signal }: HydrateOpts): Promise<DittoEvent[]> {
|
async function gatherUsers({ conf, events, relay, signal }: HydrateOpts): Promise<DittoEvent[]> {
|
||||||
const pubkeys = new Set(events.map((event) => event.pubkey));
|
const pubkeys = new Set(events.map((event) => event.pubkey));
|
||||||
|
|
||||||
if (!pubkeys.size) {
|
if (!pubkeys.size) {
|
||||||
|
|
@ -325,13 +325,13 @@ async function gatherUsers({ events, relay, signal }: HydrateOpts): Promise<Ditt
|
||||||
}
|
}
|
||||||
|
|
||||||
return relay.query(
|
return relay.query(
|
||||||
[{ kinds: [30382], authors: [await Conf.signer.getPublicKey()], '#d': [...pubkeys], limit: pubkeys.size }],
|
[{ kinds: [30382], authors: [await conf.signer.getPublicKey()], '#d': [...pubkeys], limit: pubkeys.size }],
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Collect info events from the events. */
|
/** Collect info events from the events. */
|
||||||
async function gatherInfo({ events, relay, signal }: HydrateOpts): Promise<DittoEvent[]> {
|
async function gatherInfo({ conf, events, relay, signal }: HydrateOpts): Promise<DittoEvent[]> {
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
|
|
@ -345,7 +345,7 @@ async function gatherInfo({ events, relay, signal }: HydrateOpts): Promise<Ditto
|
||||||
}
|
}
|
||||||
|
|
||||||
return relay.query(
|
return relay.query(
|
||||||
[{ kinds: [30383], authors: [await Conf.signer.getPublicKey()], '#d': [...ids], limit: ids.size }],
|
[{ kinds: [30383], authors: [await conf.signer.getPublicKey()], '#d': [...ids], limit: ids.size }],
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,8 @@ 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 = DittoPolyPg.create(Conf.databaseUrl, { poolSize: 1 });
|
const db = new DittoPolyPg(Conf.databaseUrl, { poolSize: 1 });
|
||||||
|
await db.migrate();
|
||||||
await DittoPolyPg.migrate(db.kysely);
|
|
||||||
|
|
||||||
const store = new DittoPgStore({
|
const store = new DittoPgStore({
|
||||||
db,
|
db,
|
||||||
|
|
@ -26,8 +25,10 @@ export async function createTestDB(opts?: { pure?: boolean }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
db,
|
||||||
...db,
|
...db,
|
||||||
store,
|
store,
|
||||||
|
kysely: db.kysely,
|
||||||
[Symbol.asyncDispose]: async () => {
|
[Symbol.asyncDispose]: async () => {
|
||||||
const { rows } = await sql<
|
const { rows } = await sql<
|
||||||
{ tablename: string }
|
{ tablename: string }
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { DittoTables } from '@ditto/db';
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { NostrFilter } from '@nostrify/nostrify';
|
import { DittoDB, DittoTables } from '@ditto/db';
|
||||||
|
import { NostrFilter, NStore } from '@nostrify/nostrify';
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
import { Kysely, sql } from 'kysely';
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { handleEvent } from '@/pipeline.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { errorJson } from '@/utils/log.ts';
|
import { errorJson } from '@/utils/log.ts';
|
||||||
import { Time } from '@/utils/time.ts';
|
import { Time } from '@/utils/time.ts';
|
||||||
|
|
||||||
|
|
@ -63,8 +61,15 @@ export async function getTrendingTagValues(
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TrendsCtx {
|
||||||
|
conf: DittoConf;
|
||||||
|
db: DittoDB;
|
||||||
|
relay: NStore;
|
||||||
|
}
|
||||||
|
|
||||||
/** Get trending tags and publish an event with them. */
|
/** Get trending tags and publish an event with them. */
|
||||||
export async function updateTrendingTags(
|
export async function updateTrendingTags(
|
||||||
|
ctx: TrendsCtx,
|
||||||
l: string,
|
l: string,
|
||||||
tagName: string,
|
tagName: string,
|
||||||
kinds: number[],
|
kinds: number[],
|
||||||
|
|
@ -73,10 +78,11 @@ export async function updateTrendingTags(
|
||||||
aliases?: string[],
|
aliases?: string[],
|
||||||
values?: string[],
|
values?: string[],
|
||||||
) {
|
) {
|
||||||
|
const { conf, db, relay } = ctx;
|
||||||
const params = { l, tagName, kinds, limit, extra, aliases, values };
|
const params = { l, tagName, kinds, limit, extra, aliases, values };
|
||||||
|
|
||||||
logi({ level: 'info', ns: 'ditto.trends', msg: 'Updating trending', ...params });
|
logi({ level: 'info', ns: 'ditto.trends', msg: 'Updating trending', ...params });
|
||||||
|
|
||||||
const kysely = await Storages.kysely();
|
|
||||||
const signal = AbortSignal.timeout(1000);
|
const signal = AbortSignal.timeout(1000);
|
||||||
|
|
||||||
const yesterday = Math.floor((Date.now() - Time.days(1)) / 1000);
|
const yesterday = Math.floor((Date.now() - Time.days(1)) / 1000);
|
||||||
|
|
@ -85,7 +91,7 @@ export async function updateTrendingTags(
|
||||||
const tagNames = aliases ? [tagName, ...aliases] : [tagName];
|
const tagNames = aliases ? [tagName, ...aliases] : [tagName];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const trends = await getTrendingTagValues(kysely, tagNames, {
|
const trends = await getTrendingTagValues(db.kysely, tagNames, {
|
||||||
kinds,
|
kinds,
|
||||||
since: yesterday,
|
since: yesterday,
|
||||||
until: now,
|
until: now,
|
||||||
|
|
@ -99,7 +105,7 @@ export async function updateTrendingTags(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const signer = Conf.signer;
|
const signer = conf.signer;
|
||||||
|
|
||||||
const label = await signer.signEvent({
|
const label = await signer.signEvent({
|
||||||
kind: 1985,
|
kind: 1985,
|
||||||
|
|
@ -112,7 +118,7 @@ export async function updateTrendingTags(
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
});
|
});
|
||||||
|
|
||||||
await handleEvent(label, { source: 'internal', signal });
|
await relay.event(label, { signal });
|
||||||
logi({ level: 'info', ns: 'ditto.trends', msg: 'Trends updated', ...params });
|
logi({ level: 'info', ns: 'ditto.trends', msg: 'Trends updated', ...params });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logi({ level: 'error', ns: 'ditto.trends', msg: 'Error updating trends', ...params, error: errorJson(e) });
|
logi({ level: 'error', ns: 'ditto.trends', msg: 'Error updating trends', ...params, error: errorJson(e) });
|
||||||
|
|
@ -120,28 +126,28 @@ export async function updateTrendingTags(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update trending pubkeys. */
|
/** Update trending pubkeys. */
|
||||||
export function updateTrendingPubkeys(): Promise<void> {
|
export function updateTrendingPubkeys(ctx: TrendsCtx): Promise<void> {
|
||||||
return updateTrendingTags('#p', 'p', [1, 3, 6, 7, 9735], 40, Conf.relay);
|
return updateTrendingTags(ctx, '#p', 'p', [1, 3, 6, 7, 9735], 40, ctx.conf.relay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update trending zapped events. */
|
/** Update trending zapped events. */
|
||||||
export function updateTrendingZappedEvents(): Promise<void> {
|
export function updateTrendingZappedEvents(ctx: TrendsCtx): Promise<void> {
|
||||||
return updateTrendingTags('zapped', 'e', [9735], 40, Conf.relay, ['q']);
|
return updateTrendingTags(ctx, 'zapped', 'e', [9735], 40, ctx.conf.relay, ['q']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update trending events. */
|
/** Update trending events. */
|
||||||
export async function updateTrendingEvents(): Promise<void> {
|
export async function updateTrendingEvents(ctx: TrendsCtx): Promise<void> {
|
||||||
|
const { conf, db } = ctx;
|
||||||
|
|
||||||
const results: Promise<void>[] = [
|
const results: Promise<void>[] = [
|
||||||
updateTrendingTags('#e', 'e', [1, 6, 7, 9735], 40, Conf.relay, ['q']),
|
updateTrendingTags(ctx, '#e', 'e', [1, 6, 7, 9735], 40, ctx.conf.relay, ['q']),
|
||||||
];
|
];
|
||||||
|
|
||||||
const kysely = await Storages.kysely();
|
for (const language of conf.preferredLanguages ?? []) {
|
||||||
|
|
||||||
for (const language of Conf.preferredLanguages ?? []) {
|
|
||||||
const yesterday = Math.floor((Date.now() - Time.days(1)) / 1000);
|
const yesterday = Math.floor((Date.now() - Time.days(1)) / 1000);
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
const rows = await kysely
|
const rows = await db.kysely
|
||||||
.selectFrom('nostr_events')
|
.selectFrom('nostr_events')
|
||||||
.select('nostr_events.id')
|
.select('nostr_events.id')
|
||||||
.where(sql`nostr_events.search_ext->>'language'`, '=', language)
|
.where(sql`nostr_events.search_ext->>'language'`, '=', language)
|
||||||
|
|
@ -151,18 +157,20 @@ export async function updateTrendingEvents(): Promise<void> {
|
||||||
|
|
||||||
const ids = rows.map((row) => row.id);
|
const ids = rows.map((row) => row.id);
|
||||||
|
|
||||||
results.push(updateTrendingTags(`#e.${language}`, 'e', [1, 6, 7, 9735], 40, Conf.relay, ['q'], ids));
|
results.push(
|
||||||
|
updateTrendingTags(ctx, `#e.${language}`, 'e', [1, 6, 7, 9735], 40, conf.relay, ['q'], ids),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.allSettled(results);
|
await Promise.allSettled(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update trending hashtags. */
|
/** Update trending hashtags. */
|
||||||
export function updateTrendingHashtags(): Promise<void> {
|
export function updateTrendingHashtags(ctx: TrendsCtx): Promise<void> {
|
||||||
return updateTrendingTags('#t', 't', [1], 20);
|
return updateTrendingTags(ctx, '#t', 't', [1], 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update trending links. */
|
/** Update trending links. */
|
||||||
export function updateTrendingLinks(): Promise<void> {
|
export function updateTrendingLinks(ctx: TrendsCtx): Promise<void> {
|
||||||
return updateTrendingTags('#r', 'r', [1], 20);
|
return updateTrendingTags(ctx, '#r', 'r', [1], 20);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,18 @@
|
||||||
import { HTTPException } from '@hono/hono/http-exception';
|
import { HTTPException } from '@hono/hono/http-exception';
|
||||||
import { NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
import { NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
||||||
import { logi } from '@soapbox/logi';
|
|
||||||
import { EventTemplate } from 'nostr-tools';
|
import { EventTemplate } from 'nostr-tools';
|
||||||
import * as TypeFest from 'type-fest';
|
import * as TypeFest from 'type-fest';
|
||||||
|
|
||||||
import { type AppContext } from '@/app.ts';
|
import { type AppContext } from '@/app.ts';
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import * as pipeline from '@/pipeline.ts';
|
|
||||||
import { RelayError } from '@/RelayError.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { nostrNow } from '@/utils.ts';
|
import { nostrNow } from '@/utils.ts';
|
||||||
import { parseFormData } from '@/utils/formdata.ts';
|
import { parseFormData } from '@/utils/formdata.ts';
|
||||||
import { errorJson } from '@/utils/log.ts';
|
|
||||||
import { purifyEvent } from '@/utils/purify.ts';
|
|
||||||
|
|
||||||
/** EventTemplate with defaults. */
|
/** EventTemplate with defaults. */
|
||||||
type EventStub = TypeFest.SetOptional<EventTemplate, 'content' | 'created_at' | 'tags'>;
|
type EventStub = TypeFest.SetOptional<EventTemplate, 'content' | 'created_at' | 'tags'>;
|
||||||
|
|
||||||
/** Publish an event through the pipeline. */
|
/** Publish an event through the pipeline. */
|
||||||
async function createEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
async function createEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
||||||
const { user } = c.var;
|
const { user, relay, signal } = c.var;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new HTTPException(401, {
|
throw new HTTPException(401, {
|
||||||
|
|
@ -34,7 +27,8 @@ async function createEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
||||||
...t,
|
...t,
|
||||||
});
|
});
|
||||||
|
|
||||||
return publishEvent(event, c);
|
await relay.event(event, { signal });
|
||||||
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Filter for fetching an existing event to update. */
|
/** Filter for fetching an existing event to update. */
|
||||||
|
|
@ -49,9 +43,9 @@ async function updateEvent<E extends EventStub>(
|
||||||
fn: (prev: NostrEvent) => E | Promise<E>,
|
fn: (prev: NostrEvent) => E | Promise<E>,
|
||||||
c: AppContext,
|
c: AppContext,
|
||||||
): Promise<NostrEvent> {
|
): Promise<NostrEvent> {
|
||||||
const store = await Storages.db();
|
const { relay } = c.var;
|
||||||
|
|
||||||
const [prev] = await store.query(
|
const [prev] = await relay.query(
|
||||||
[filter],
|
[filter],
|
||||||
{ signal: c.req.raw.signal },
|
{ signal: c.req.raw.signal },
|
||||||
);
|
);
|
||||||
|
|
@ -80,16 +74,17 @@ function updateListEvent(
|
||||||
|
|
||||||
/** Publish an admin event through the pipeline. */
|
/** Publish an admin event through the pipeline. */
|
||||||
async function createAdminEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
async function createAdminEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
||||||
const signer = Conf.signer;
|
const { conf, relay, signal } = c.var;
|
||||||
|
|
||||||
const event = await signer.signEvent({
|
const event = await conf.signer.signEvent({
|
||||||
content: '',
|
content: '',
|
||||||
created_at: nostrNow(),
|
created_at: nostrNow(),
|
||||||
tags: [],
|
tags: [],
|
||||||
...t,
|
...t,
|
||||||
});
|
});
|
||||||
|
|
||||||
return publishEvent(event, c);
|
await relay.event(event, { signal });
|
||||||
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch existing event, update its tags, then publish the new admin event. */
|
/** Fetch existing event, update its tags, then publish the new admin event. */
|
||||||
|
|
@ -111,8 +106,8 @@ async function updateAdminEvent<E extends EventStub>(
|
||||||
fn: (prev: NostrEvent | undefined) => E,
|
fn: (prev: NostrEvent | undefined) => E,
|
||||||
c: AppContext,
|
c: AppContext,
|
||||||
): Promise<NostrEvent> {
|
): Promise<NostrEvent> {
|
||||||
const store = await Storages.db();
|
const { relay, signal } = c.var;
|
||||||
const [prev] = await store.query([filter], { limit: 1, signal: c.req.raw.signal });
|
const [prev] = await relay.query([filter], { signal });
|
||||||
return createAdminEvent(fn(prev), c);
|
return createAdminEvent(fn(prev), c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,8 +120,8 @@ function updateEventInfo(id: string, n: Record<string, boolean>, c: AppContext):
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateNames(k: number, d: string, n: Record<string, boolean>, c: AppContext): Promise<NostrEvent> {
|
async function updateNames(k: number, d: string, n: Record<string, boolean>, c: AppContext): Promise<NostrEvent> {
|
||||||
const signer = Conf.signer;
|
const { conf } = c.var;
|
||||||
const admin = await signer.getPublicKey();
|
const admin = await conf.signer.getPublicKey();
|
||||||
|
|
||||||
return updateAdminEvent(
|
return updateAdminEvent(
|
||||||
{ kinds: [k], authors: [admin], '#d': [d], limit: 1 },
|
{ kinds: [k], authors: [admin], '#d': [d], limit: 1 },
|
||||||
|
|
@ -154,33 +149,6 @@ async function updateNames(k: number, d: string, n: Record<string, boolean>, c:
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Push the event through the pipeline, rethrowing any RelayError. */
|
|
||||||
async function publishEvent(event: NostrEvent, c: AppContext): Promise<NostrEvent> {
|
|
||||||
logi({ level: 'info', ns: 'ditto.event', source: 'api', id: event.id, kind: event.kind });
|
|
||||||
try {
|
|
||||||
const promise = pipeline.handleEvent(event, { source: 'api', signal: c.req.raw.signal });
|
|
||||||
|
|
||||||
promise.then(async () => {
|
|
||||||
const client = await Storages.client();
|
|
||||||
await client.event(purifyEvent(event));
|
|
||||||
}).catch((e: unknown) => {
|
|
||||||
logi({ level: 'error', ns: 'ditto.pool', id: event.id, kind: event.kind, error: errorJson(e) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await promise;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof RelayError) {
|
|
||||||
throw new HTTPException(422, {
|
|
||||||
res: c.json({ error: e.message }, 422),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Parse request body to JSON, depending on the content-type of the request. */
|
/** Parse request body to JSON, depending on the content-type of the request. */
|
||||||
async function parseBody(req: Request): Promise<unknown> {
|
async function parseBody(req: Request): Promise<unknown> {
|
||||||
switch (req.headers.get('content-type')?.split(';')[0]) {
|
switch (req.headers.get('content-type')?.split(';')[0]) {
|
||||||
|
|
@ -196,74 +164,8 @@ async function parseBody(req: Request): Promise<unknown> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build HTTP Link header for Mastodon API pagination. */
|
|
||||||
function buildLinkHeader(url: string, events: NostrEvent[]): string | undefined {
|
|
||||||
if (events.length <= 1) return;
|
|
||||||
const firstEvent = events[0];
|
|
||||||
const lastEvent = events[events.length - 1];
|
|
||||||
|
|
||||||
const { origin } = Conf.url;
|
|
||||||
const { pathname, search } = new URL(url);
|
|
||||||
const next = new URL(pathname + search, origin);
|
|
||||||
const prev = new URL(pathname + search, origin);
|
|
||||||
|
|
||||||
next.searchParams.set('until', String(lastEvent.created_at));
|
|
||||||
prev.searchParams.set('since', String(firstEvent.created_at));
|
|
||||||
|
|
||||||
return `<${next}>; rel="next", <${prev}>; rel="prev"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
type HeaderRecord = Record<string, string | string[]>;
|
type HeaderRecord = Record<string, string | string[]>;
|
||||||
|
|
||||||
/** Return results with pagination headers. Assumes chronological sorting of events. */
|
|
||||||
function paginated(c: AppContext, events: NostrEvent[], body: object | unknown[], headers: HeaderRecord = {}) {
|
|
||||||
const link = buildLinkHeader(c.req.url, events);
|
|
||||||
|
|
||||||
if (link) {
|
|
||||||
headers.link = link;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out undefined entities.
|
|
||||||
const results = Array.isArray(body) ? body.filter(Boolean) : body;
|
|
||||||
return c.json(results, 200, headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Build HTTP Link header for paginating Nostr lists. */
|
|
||||||
function buildListLinkHeader(url: string, params: { offset: number; limit: number }): string | undefined {
|
|
||||||
const { origin } = Conf.url;
|
|
||||||
const { pathname, search } = new URL(url);
|
|
||||||
const { offset, limit } = params;
|
|
||||||
const next = new URL(pathname + search, origin);
|
|
||||||
const prev = new URL(pathname + search, origin);
|
|
||||||
|
|
||||||
next.searchParams.set('offset', String(offset + limit));
|
|
||||||
prev.searchParams.set('offset', String(Math.max(offset - limit, 0)));
|
|
||||||
|
|
||||||
next.searchParams.set('limit', String(limit));
|
|
||||||
prev.searchParams.set('limit', String(limit));
|
|
||||||
|
|
||||||
return `<${next}>; rel="next", <${prev}>; rel="prev"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** paginate a list of tags. */
|
|
||||||
function paginatedList(
|
|
||||||
c: AppContext,
|
|
||||||
params: { offset: number; limit: number },
|
|
||||||
body: object | unknown[],
|
|
||||||
headers: HeaderRecord = {},
|
|
||||||
) {
|
|
||||||
const link = buildListLinkHeader(c.req.url, params);
|
|
||||||
const hasMore = Array.isArray(body) ? body.length > 0 : true;
|
|
||||||
|
|
||||||
if (link) {
|
|
||||||
headers.link = hasMore ? link : link.split(', ').find((link) => link.endsWith('; rel="prev"'))!;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out undefined entities.
|
|
||||||
const results = Array.isArray(body) ? body.filter(Boolean) : body;
|
|
||||||
return c.json(results, 200, headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Actors with Bluesky's `!no-unauthenticated` self-label should require authorization to view. */
|
/** Actors with Bluesky's `!no-unauthenticated` self-label should require authorization to view. */
|
||||||
function assertAuthenticated(c: AppContext, author: NostrEvent): void {
|
function assertAuthenticated(c: AppContext, author: NostrEvent): void {
|
||||||
if (
|
if (
|
||||||
|
|
@ -282,8 +184,6 @@ export {
|
||||||
createAdminEvent,
|
createAdminEvent,
|
||||||
createEvent,
|
createEvent,
|
||||||
type EventStub,
|
type EventStub,
|
||||||
paginated,
|
|
||||||
paginatedList,
|
|
||||||
parseBody,
|
parseBody,
|
||||||
updateAdminEvent,
|
updateAdminEvent,
|
||||||
updateEvent,
|
updateEvent,
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { getInstanceMetadata } from '@/utils/instance.ts';
|
|
||||||
|
|
||||||
/** NIP-46 client-connect metadata. */
|
|
||||||
interface ConnectMetadata {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get NIP-46 `nostrconnect://` URI for the Ditto server. */
|
|
||||||
export async function getClientConnectUri(signal?: AbortSignal): Promise<string> {
|
|
||||||
const uri = new URL('nostrconnect://');
|
|
||||||
const { name, tagline } = await getInstanceMetadata(await Storages.db(), signal);
|
|
||||||
|
|
||||||
const metadata: ConnectMetadata = {
|
|
||||||
name,
|
|
||||||
description: tagline,
|
|
||||||
url: Conf.localDomain,
|
|
||||||
};
|
|
||||||
|
|
||||||
uri.host = await Conf.signer.getPublicKey();
|
|
||||||
uri.searchParams.set('relay', Conf.relay);
|
|
||||||
uri.searchParams.set('metadata', JSON.stringify(metadata));
|
|
||||||
|
|
||||||
return uri.toString();
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +1,13 @@
|
||||||
import { DOMParser } from '@b-fuze/deno-dom';
|
import { DOMParser } from '@b-fuze/deno-dom';
|
||||||
import { DittoTables } from '@ditto/db';
|
import { DittoTables } from '@ditto/db';
|
||||||
import { cachedFaviconsSizeGauge } from '@ditto/metrics';
|
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
import { safeFetch } from '@soapbox/safe-fetch';
|
import { safeFetch } from '@soapbox/safe-fetch';
|
||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import tldts from 'tldts';
|
import tldts from 'tldts';
|
||||||
|
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { nostrNow } from '@/utils.ts';
|
import { nostrNow } from '@/utils.ts';
|
||||||
import { SimpleLRU } from '@/utils/SimpleLRU.ts';
|
|
||||||
|
|
||||||
export const faviconCache = new SimpleLRU<string, URL>(
|
export async function queryFavicon(
|
||||||
async (domain, { signal }) => {
|
|
||||||
const kysely = await Storages.kysely();
|
|
||||||
|
|
||||||
const row = await queryFavicon(kysely, domain);
|
|
||||||
|
|
||||||
if (row && (nostrNow() - row.last_updated_at) < (Conf.caches.favicon.ttl / 1000)) {
|
|
||||||
return new URL(row.favicon);
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = await fetchFavicon(domain, signal);
|
|
||||||
|
|
||||||
await insertFavicon(kysely, domain, url.href);
|
|
||||||
|
|
||||||
return url;
|
|
||||||
},
|
|
||||||
{ ...Conf.caches.favicon, gauge: cachedFaviconsSizeGauge },
|
|
||||||
);
|
|
||||||
|
|
||||||
async function queryFavicon(
|
|
||||||
kysely: Kysely<DittoTables>,
|
kysely: Kysely<DittoTables>,
|
||||||
domain: string,
|
domain: string,
|
||||||
): Promise<DittoTables['domain_favicons'] | undefined> {
|
): Promise<DittoTables['domain_favicons'] | undefined> {
|
||||||
|
|
@ -41,7 +18,7 @@ async function queryFavicon(
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function insertFavicon(kysely: Kysely<DittoTables>, domain: string, favicon: string): Promise<void> {
|
export async function insertFavicon(kysely: Kysely<DittoTables>, domain: string, favicon: string): Promise<void> {
|
||||||
await kysely
|
await kysely
|
||||||
.insertInto('domain_favicons')
|
.insertInto('domain_favicons')
|
||||||
.values({ domain, favicon, last_updated_at: nostrNow() })
|
.values({ domain, favicon, last_updated_at: nostrNow() })
|
||||||
|
|
@ -49,7 +26,7 @@ async function insertFavicon(kysely: Kysely<DittoTables>, domain: string, favico
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFavicon(domain: string, signal?: AbortSignal): Promise<URL> {
|
export async function fetchFavicon(domain: string, signal?: AbortSignal): Promise<URL> {
|
||||||
logi({ level: 'info', ns: 'ditto.favicon', domain, state: 'started' });
|
logi({ level: 'info', ns: 'ditto.favicon', domain, state: 'started' });
|
||||||
const tld = tldts.parse(domain);
|
const tld = tldts.parse(domain);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,42 @@
|
||||||
import { NostrEvent, NSchema as n } from '@nostrify/nostrify';
|
import { NostrEvent, NSchema as n, NStore } from '@nostrify/nostrify';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { match } from 'path-to-regexp';
|
import { match } from 'path-to-regexp';
|
||||||
import tldts from 'tldts';
|
import tldts from 'tldts';
|
||||||
|
|
||||||
import { getAuthor } from '@/queries.ts';
|
import { getAuthor } from '@/queries.ts';
|
||||||
import { bech32ToPubkey } from '@/utils.ts';
|
import { bech32ToPubkey } from '@/utils.ts';
|
||||||
import { nip05Cache } from '@/utils/nip05.ts';
|
import { lookupNip05 } from '@/utils/nip05.ts';
|
||||||
|
|
||||||
|
import type { DittoConf } from '@ditto/conf';
|
||||||
|
import type { DittoDB } from '@ditto/db';
|
||||||
|
|
||||||
|
interface LookupAccountOpts {
|
||||||
|
db: DittoDB;
|
||||||
|
conf: DittoConf;
|
||||||
|
relay: NStore;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
/** Resolve a bech32 or NIP-05 identifier to an account. */
|
/** Resolve a bech32 or NIP-05 identifier to an account. */
|
||||||
export async function lookupAccount(
|
export async function lookupAccount(
|
||||||
value: string,
|
value: string,
|
||||||
signal = AbortSignal.timeout(3000),
|
opts: LookupAccountOpts,
|
||||||
): Promise<NostrEvent | undefined> {
|
): Promise<NostrEvent | undefined> {
|
||||||
const pubkey = await lookupPubkey(value, signal);
|
const pubkey = await lookupPubkey(value, opts);
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
return getAuthor(pubkey);
|
return getAuthor(pubkey, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve a bech32 or NIP-05 identifier to a pubkey. */
|
/** Resolve a bech32 or NIP-05 identifier to a pubkey. */
|
||||||
export async function lookupPubkey(value: string, signal?: AbortSignal): Promise<string | undefined> {
|
export async function lookupPubkey(value: string, opts: LookupAccountOpts): Promise<string | undefined> {
|
||||||
if (n.bech32().safeParse(value).success) {
|
if (n.bech32().safeParse(value).success) {
|
||||||
return bech32ToPubkey(value);
|
return bech32ToPubkey(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { pubkey } = await nip05Cache.fetch(value, { signal });
|
const { pubkey } = await lookupNip05(value, opts);
|
||||||
return pubkey;
|
return pubkey;
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,20 @@
|
||||||
import { cachedNip05sSizeGauge } from '@ditto/metrics';
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { NIP05, NStore } from '@nostrify/nostrify';
|
import { NIP05, NStore } from '@nostrify/nostrify';
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
import { safeFetch } from '@soapbox/safe-fetch';
|
import { safeFetch } from '@soapbox/safe-fetch';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import tldts from 'tldts';
|
import tldts from 'tldts';
|
||||||
|
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { errorJson } from '@/utils/log.ts';
|
import { errorJson } from '@/utils/log.ts';
|
||||||
import { SimpleLRU } from '@/utils/SimpleLRU.ts';
|
|
||||||
|
|
||||||
export const nip05Cache = new SimpleLRU<string, nip19.ProfilePointer>(
|
interface GetNip05Opts {
|
||||||
async (nip05, { signal }) => {
|
conf: DittoConf;
|
||||||
const store = await Storages.db();
|
relay: NStore;
|
||||||
return getNip05(store, nip05, signal);
|
signal?: AbortSignal;
|
||||||
},
|
}
|
||||||
{ ...Conf.caches.nip05, gauge: cachedNip05sSizeGauge },
|
|
||||||
);
|
|
||||||
|
|
||||||
async function getNip05(
|
export async function lookupNip05(nip05: string, opts: GetNip05Opts): Promise<nip19.ProfilePointer> {
|
||||||
store: NStore,
|
const { conf, signal } = opts;
|
||||||
nip05: string,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
): Promise<nip19.ProfilePointer> {
|
|
||||||
const tld = tldts.parse(nip05);
|
const tld = tldts.parse(nip05);
|
||||||
|
|
||||||
if (!tld.isIcann || tld.isIp || tld.isPrivate) {
|
if (!tld.isIcann || tld.isIp || tld.isPrivate) {
|
||||||
|
|
@ -34,8 +26,8 @@ async function getNip05(
|
||||||
const [name, domain] = nip05.split('@');
|
const [name, domain] = nip05.split('@');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (domain === Conf.url.host) {
|
if (domain === conf.url.host) {
|
||||||
const pointer = await localNip05Lookup(store, name);
|
const pointer = await localNip05Lookup(name, opts);
|
||||||
if (pointer) {
|
if (pointer) {
|
||||||
logi({ level: 'info', ns: 'ditto.nip05', nip05, state: 'found', source: 'local', pubkey: pointer.pubkey });
|
logi({ level: 'info', ns: 'ditto.nip05', nip05, state: 'found', source: 'local', pubkey: pointer.pubkey });
|
||||||
return pointer;
|
return pointer;
|
||||||
|
|
@ -53,19 +45,24 @@ async function getNip05(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function localNip05Lookup(store: NStore, localpart: string): Promise<nip19.ProfilePointer | undefined> {
|
export async function localNip05Lookup(
|
||||||
const name = `${localpart}@${Conf.url.host}`;
|
localpart: string,
|
||||||
|
opts: GetNip05Opts,
|
||||||
|
): Promise<nip19.ProfilePointer | undefined> {
|
||||||
|
const { conf, relay, signal } = opts;
|
||||||
|
|
||||||
const [grant] = await store.query([{
|
const name = `${localpart}@${conf.url.host}`;
|
||||||
|
|
||||||
|
const [grant] = await relay.query([{
|
||||||
kinds: [30360],
|
kinds: [30360],
|
||||||
'#d': [name, name.toLowerCase()],
|
'#d': [name, name.toLowerCase()],
|
||||||
authors: [await Conf.signer.getPublicKey()],
|
authors: [await conf.signer.getPublicKey()],
|
||||||
limit: 1,
|
limit: 1,
|
||||||
}]);
|
}], { signal });
|
||||||
|
|
||||||
const pubkey = grant?.tags.find(([name]) => name === 'p')?.[1];
|
const pubkey = grant?.tags.find(([name]) => name === 'p')?.[1];
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
return { pubkey, relays: [Conf.relay] };
|
return { pubkey, relays: [conf.relay] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
|
import { paginated, paginatedList } from '@ditto/mastoapi/pagination';
|
||||||
import { NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
import { NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
||||||
|
|
||||||
import { AppContext } from '@/app.ts';
|
import { AppContext } from '@/app.ts';
|
||||||
import { paginationSchema } from '@/schemas/pagination.ts';
|
import { paginationSchema } from '@/schemas/pagination.ts';
|
||||||
import { renderAccount } from '@/views/mastodon/accounts.ts';
|
import { renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
import { paginated, paginatedList } from '@/utils/api.ts';
|
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { accountFromPubkey } from '@/views/mastodon/accounts.ts';
|
import { accountFromPubkey } from '@/views/mastodon/accounts.ts';
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], opts?:
|
||||||
const events = await relay.query(filters, { signal })
|
const events = await relay.query(filters, { signal })
|
||||||
// Deduplicate by author.
|
// Deduplicate by author.
|
||||||
.then((events) => Array.from(new Map(events.map((event) => [event.pubkey, event])).values()))
|
.then((events) => Array.from(new Map(events.map((event) => [event.pubkey, event])).values()))
|
||||||
.then((events) => hydrateEvents({ events, relay, signal }))
|
.then((events) => hydrateEvents({ ...c.var, events, relay, signal }))
|
||||||
.then((events) => filterFn ? events.filter(filterFn) : events);
|
.then((events) => filterFn ? events.filter(filterFn) : events);
|
||||||
|
|
||||||
const accounts = await Promise.all(
|
const accounts = await Promise.all(
|
||||||
|
|
@ -48,7 +48,7 @@ async function renderAccounts(c: AppContext, pubkeys: string[]) {
|
||||||
const { relay, signal } = c.var;
|
const { relay, signal } = c.var;
|
||||||
|
|
||||||
const events = await relay.query([{ kinds: [0], authors }], { signal })
|
const events = await relay.query([{ kinds: [0], authors }], { signal })
|
||||||
.then((events) => hydrateEvents({ events, relay, signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
const accounts = await Promise.all(
|
const accounts = await Promise.all(
|
||||||
authors.map((pubkey) => {
|
authors.map((pubkey) => {
|
||||||
|
|
@ -74,7 +74,7 @@ async function renderStatuses(c: AppContext, ids: string[], signal = AbortSignal
|
||||||
const { limit } = pagination;
|
const { limit } = pagination;
|
||||||
|
|
||||||
const events = await relay.query([{ kinds: [1, 20], ids, limit }], { signal })
|
const events = await relay.query([{ kinds: [1, 20], ids, limit }], { signal })
|
||||||
.then((events) => hydrateEvents({ events, relay, signal }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
|
|
@ -85,7 +85,7 @@ async function renderStatuses(c: AppContext, ids: string[], signal = AbortSignal
|
||||||
const viewerPubkey = await user?.signer.getPublicKey();
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
|
|
||||||
const statuses = await Promise.all(
|
const statuses = await Promise.all(
|
||||||
sortedEvents.map((event) => renderStatus(event, { viewerPubkey })),
|
sortedEvents.map((event) => renderStatus(relay, event, { viewerPubkey })),
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: pagination with min_id and max_id based on the order of `ids`.
|
// TODO: pagination with min_id and max_id based on the order of `ids`.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { NostrEvent } from '@nostrify/nostrify';
|
import { NostrEvent, NStore } from '@nostrify/nostrify';
|
||||||
|
|
||||||
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
|
|
@ -10,23 +10,23 @@ interface RenderNotificationOpts {
|
||||||
viewerPubkey: string;
|
viewerPubkey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderNotification(event: DittoEvent, opts: RenderNotificationOpts) {
|
async function renderNotification(store: NStore, event: DittoEvent, opts: RenderNotificationOpts) {
|
||||||
const mentioned = !!event.tags.find(([name, value]) => name === 'p' && value === opts.viewerPubkey);
|
const mentioned = !!event.tags.find(([name, value]) => name === 'p' && value === opts.viewerPubkey);
|
||||||
|
|
||||||
if (event.kind === 1 && mentioned) {
|
if (event.kind === 1 && mentioned) {
|
||||||
return renderMention(event, opts);
|
return renderMention(store, event, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === 6) {
|
if (event.kind === 6) {
|
||||||
return renderReblog(event, opts);
|
return renderReblog(store, event, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === 7 && event.content === '+') {
|
if (event.kind === 7 && event.content === '+') {
|
||||||
return renderFavourite(event, opts);
|
return renderFavourite(store, event, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === 7) {
|
if (event.kind === 7) {
|
||||||
return renderReaction(event, opts);
|
return renderReaction(store, event, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === 30360 && event.pubkey === await Conf.signer.getPublicKey()) {
|
if (event.kind === 30360 && event.pubkey === await Conf.signer.getPublicKey()) {
|
||||||
|
|
@ -34,12 +34,12 @@ async function renderNotification(event: DittoEvent, opts: RenderNotificationOpt
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === 9735) {
|
if (event.kind === 9735) {
|
||||||
return renderZap(event, opts);
|
return renderZap(store, event, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderMention(event: DittoEvent, opts: RenderNotificationOpts) {
|
async function renderMention(store: NStore, event: DittoEvent, opts: RenderNotificationOpts) {
|
||||||
const status = await renderStatus(event, opts);
|
const status = await renderStatus(store, event, opts);
|
||||||
if (!status) return;
|
if (!status) return;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -51,9 +51,9 @@ async function renderMention(event: DittoEvent, opts: RenderNotificationOpts) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderReblog(event: DittoEvent, opts: RenderNotificationOpts) {
|
async function renderReblog(store: NStore, event: DittoEvent, opts: RenderNotificationOpts) {
|
||||||
if (event.repost?.kind !== 1) return;
|
if (event.repost?.kind !== 1) return;
|
||||||
const status = await renderStatus(event.repost, opts);
|
const status = await renderStatus(store, event.repost, opts);
|
||||||
if (!status) return;
|
if (!status) return;
|
||||||
const account = event.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey);
|
const account = event.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey);
|
||||||
|
|
||||||
|
|
@ -66,9 +66,9 @@ async function renderReblog(event: DittoEvent, opts: RenderNotificationOpts) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderFavourite(event: DittoEvent, opts: RenderNotificationOpts) {
|
async function renderFavourite(store: NStore, event: DittoEvent, opts: RenderNotificationOpts) {
|
||||||
if (event.reacted?.kind !== 1) return;
|
if (event.reacted?.kind !== 1) return;
|
||||||
const status = await renderStatus(event.reacted, opts);
|
const status = await renderStatus(store, event.reacted, opts);
|
||||||
if (!status) return;
|
if (!status) return;
|
||||||
const account = event.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey);
|
const account = event.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey);
|
||||||
|
|
||||||
|
|
@ -81,9 +81,9 @@ async function renderFavourite(event: DittoEvent, opts: RenderNotificationOpts)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderReaction(event: DittoEvent, opts: RenderNotificationOpts) {
|
async function renderReaction(store: NStore, event: DittoEvent, opts: RenderNotificationOpts) {
|
||||||
if (event.reacted?.kind !== 1) return;
|
if (event.reacted?.kind !== 1) return;
|
||||||
const status = await renderStatus(event.reacted, opts);
|
const status = await renderStatus(store, event.reacted, opts);
|
||||||
if (!status) return;
|
if (!status) return;
|
||||||
const account = event.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey);
|
const account = event.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey);
|
||||||
|
|
||||||
|
|
@ -116,7 +116,7 @@ async function renderNameGrant(event: DittoEvent) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderZap(event: DittoEvent, opts: RenderNotificationOpts) {
|
async function renderZap(store: NStore, event: DittoEvent, opts: RenderNotificationOpts) {
|
||||||
if (!event.zap_sender) return;
|
if (!event.zap_sender) return;
|
||||||
|
|
||||||
const { zap_amount = 0, zap_message = '' } = event;
|
const { zap_amount = 0, zap_message = '' } = event;
|
||||||
|
|
@ -133,7 +133,7 @@ async function renderZap(event: DittoEvent, opts: RenderNotificationOpts) {
|
||||||
message: zap_message,
|
message: zap_message,
|
||||||
created_at: nostrDate(event.created_at).toISOString(),
|
created_at: nostrDate(event.created_at).toISOString(),
|
||||||
account,
|
account,
|
||||||
...(event.zapped ? { status: await renderStatus(event.zapped, opts) } : {}),
|
...(event.zapped ? { status: await renderStatus(store, event.zapped, opts) } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { NostrEvent } from '@nostrify/nostrify';
|
import type { NostrEvent, NStore } from '@nostrify/nostrify';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
import { MastodonPush } from '@/types/MastodonPush.ts';
|
import { MastodonPush } from '@/types/MastodonPush.ts';
|
||||||
|
|
@ -9,10 +9,11 @@ import { renderNotification } from '@/views/mastodon/notifications.ts';
|
||||||
* Unlike other views, only one will be rendered at a time, so making use of async calls is okay.
|
* Unlike other views, only one will be rendered at a time, so making use of async calls is okay.
|
||||||
*/
|
*/
|
||||||
export async function renderWebPushNotification(
|
export async function renderWebPushNotification(
|
||||||
|
store: NStore,
|
||||||
event: NostrEvent,
|
event: NostrEvent,
|
||||||
viewerPubkey: string,
|
viewerPubkey: string,
|
||||||
): Promise<MastodonPush | undefined> {
|
): Promise<MastodonPush | undefined> {
|
||||||
const notification = await renderNotification(event, { viewerPubkey });
|
const notification = await renderNotification(store, event, { viewerPubkey });
|
||||||
if (!notification) {
|
if (!notification) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { NStore } from '@nostrify/nostrify';
|
||||||
|
|
||||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
import { nostrDate } from '@/utils.ts';
|
import { nostrDate } from '@/utils.ts';
|
||||||
|
|
@ -6,7 +8,7 @@ import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
import { getTagSet } from '@/utils/tags.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
|
|
||||||
/** Expects a `reportEvent` of kind 1984 and a `profile` of kind 0 of the person being reported */
|
/** Expects a `reportEvent` of kind 1984 and a `profile` of kind 0 of the person being reported */
|
||||||
async function renderReport(event: DittoEvent) {
|
function renderReport(event: DittoEvent) {
|
||||||
// The category is present in both the 'e' and 'p' tag, however, it is possible to report a user without reporting a note, so it's better to get the category from the 'p' tag
|
// The category is present in both the 'e' and 'p' tag, however, it is possible to report a user without reporting a note, so it's better to get the category from the 'p' tag
|
||||||
const category = event.tags.find(([name]) => name === 'p')?.[2];
|
const category = event.tags.find(([name]) => name === 'p')?.[2];
|
||||||
const statusIds = event.tags.filter(([name]) => name === 'e').map((tag) => tag[1]) ?? [];
|
const statusIds = event.tags.filter(([name]) => name === 'e').map((tag) => tag[1]) ?? [];
|
||||||
|
|
@ -23,9 +25,7 @@ async function renderReport(event: DittoEvent) {
|
||||||
created_at: nostrDate(event.created_at).toISOString(),
|
created_at: nostrDate(event.created_at).toISOString(),
|
||||||
status_ids: statusIds,
|
status_ids: statusIds,
|
||||||
rules_ids: null,
|
rules_ids: null,
|
||||||
target_account: event.reported_profile
|
target_account: event.reported_profile ? renderAccount(event.reported_profile) : accountFromPubkey(reportedPubkey),
|
||||||
? await renderAccount(event.reported_profile)
|
|
||||||
: await accountFromPubkey(reportedPubkey),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ interface RenderAdminReportOpts {
|
||||||
/** Admin-level information about a filed report.
|
/** Admin-level information about a filed report.
|
||||||
* Expects an event of kind 1984 fully hydrated.
|
* Expects an event of kind 1984 fully hydrated.
|
||||||
* https://docs.joinmastodon.org/entities/Admin_Report */
|
* https://docs.joinmastodon.org/entities/Admin_Report */
|
||||||
async function renderAdminReport(event: DittoEvent, opts: RenderAdminReportOpts) {
|
async function renderAdminReport(store: NStore, event: DittoEvent, opts: RenderAdminReportOpts) {
|
||||||
const { viewerPubkey } = opts;
|
const { viewerPubkey } = opts;
|
||||||
|
|
||||||
// The category is present in both the 'e' and 'p' tag, however, it is possible to report a user without reporting a note, so it's better to get the category from the 'p' tag
|
// The category is present in both the 'e' and 'p' tag, however, it is possible to report a user without reporting a note, so it's better to get the category from the 'p' tag
|
||||||
|
|
@ -45,7 +45,7 @@ async function renderAdminReport(event: DittoEvent, opts: RenderAdminReportOpts)
|
||||||
const statuses = [];
|
const statuses = [];
|
||||||
if (event.reported_notes) {
|
if (event.reported_notes) {
|
||||||
for (const status of event.reported_notes) {
|
for (const status of event.reported_notes) {
|
||||||
statuses.push(await renderStatus(status, { viewerPubkey }));
|
statuses.push(await renderStatus(store, status, { viewerPubkey }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { NostrEvent } from '@nostrify/nostrify';
|
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';
|
||||||
|
|
@ -6,7 +6,6 @@ import { MastodonAttachment } from '@/entities/MastodonAttachment.ts';
|
||||||
import { MastodonMention } from '@/entities/MastodonMention.ts';
|
import { MastodonMention } from '@/entities/MastodonMention.ts';
|
||||||
import { MastodonStatus } from '@/entities/MastodonStatus.ts';
|
import { MastodonStatus } from '@/entities/MastodonStatus.ts';
|
||||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { Storages } from '@/storages.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';
|
||||||
import { findReplyTag } from '@/utils/tags.ts';
|
import { findReplyTag } from '@/utils/tags.ts';
|
||||||
|
|
@ -20,7 +19,11 @@ interface RenderStatusOpts {
|
||||||
depth?: number;
|
depth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise<MastodonStatus | undefined> {
|
async function renderStatus(
|
||||||
|
store: NStore,
|
||||||
|
event: DittoEvent,
|
||||||
|
opts: RenderStatusOpts,
|
||||||
|
): Promise<MastodonStatus | undefined> {
|
||||||
const { viewerPubkey, depth = 1 } = opts;
|
const { viewerPubkey, depth = 1 } = opts;
|
||||||
|
|
||||||
if (depth > 2 || depth < 0) return;
|
if (depth > 2 || depth < 0) return;
|
||||||
|
|
@ -38,8 +41,6 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise<
|
||||||
|
|
||||||
const replyId = findReplyTag(event.tags)?.[1];
|
const replyId = findReplyTag(event.tags)?.[1];
|
||||||
|
|
||||||
const store = await Storages.db();
|
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -123,7 +124,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise<
|
||||||
tags: [],
|
tags: [],
|
||||||
emojis: renderEmojis(event),
|
emojis: renderEmojis(event),
|
||||||
poll: null,
|
poll: null,
|
||||||
quote: !event.quote ? null : await renderStatus(event.quote, { depth: depth + 1 }),
|
quote: !event.quote ? null : await renderStatus(store, event.quote, { depth: depth + 1 }),
|
||||||
quote_id: event.quote?.id ?? null,
|
quote_id: event.quote?.id ?? null,
|
||||||
uri: Conf.local(`/users/${account.acct}/statuses/${event.id}`),
|
uri: Conf.local(`/users/${account.acct}/statuses/${event.id}`),
|
||||||
url: Conf.local(`/@${account.acct}/${event.id}`),
|
url: Conf.local(`/@${account.acct}/${event.id}`),
|
||||||
|
|
@ -139,14 +140,18 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise<
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderReblog(event: DittoEvent, opts: RenderStatusOpts): Promise<MastodonStatus | undefined> {
|
async function renderReblog(
|
||||||
|
store: NStore,
|
||||||
|
event: DittoEvent,
|
||||||
|
opts: RenderStatusOpts,
|
||||||
|
): Promise<MastodonStatus | undefined> {
|
||||||
const { viewerPubkey } = opts;
|
const { viewerPubkey } = opts;
|
||||||
if (!event.repost) return;
|
if (!event.repost) return;
|
||||||
|
|
||||||
const status = await renderStatus(event, {}); // omit viewerPubkey intentionally
|
const status = await renderStatus(store, event, {}); // omit viewerPubkey intentionally
|
||||||
if (!status) return;
|
if (!status) return;
|
||||||
|
|
||||||
const reblog = await renderStatus(event.repost, { viewerPubkey }) ?? null;
|
const reblog = await renderStatus(store, event.repost, { viewerPubkey }) ?? null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...status,
|
...status,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export class CustomPolicy implements NPolicy {
|
||||||
async init({ path, databaseUrl, pubkey }: PolicyInit): Promise<void> {
|
async init({ path, databaseUrl, pubkey }: PolicyInit): Promise<void> {
|
||||||
const Policy = (await import(path)).default;
|
const Policy = (await import(path)).default;
|
||||||
|
|
||||||
const db = DittoPolyPg.create(databaseUrl, { poolSize: 1 });
|
const db = new DittoPolyPg(databaseUrl, { poolSize: 1 });
|
||||||
|
|
||||||
const store = new DittoPgStore({
|
const store = new DittoPgStore({
|
||||||
db,
|
db,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./middleware": "./middleware/mod.ts",
|
"./middleware": "./middleware/mod.ts",
|
||||||
|
"./pagination": "./pagination/mod.ts",
|
||||||
"./router": "./router/mod.ts",
|
"./router": "./router/mod.ts",
|
||||||
"./test": "./test.ts"
|
"./test": "./test.ts"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
packages/mastoapi/pagination/mod.ts
Normal file
3
packages/mastoapi/pagination/mod.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { buildLinkHeader, buildListLinkHeader } from './link-header.ts';
|
||||||
|
export { paginated, paginatedList } from './paginate.ts';
|
||||||
|
export { paginationSchema } from './schema.ts';
|
||||||
|
|
@ -7,7 +7,7 @@ import { DittoApp } from './DittoApp.ts';
|
||||||
import { DittoRoute } from './DittoRoute.ts';
|
import { DittoRoute } from './DittoRoute.ts';
|
||||||
|
|
||||||
Deno.test('DittoApp', async () => {
|
Deno.test('DittoApp', async () => {
|
||||||
await using db = DittoPolyPg.create('memory://');
|
await using db = new DittoPolyPg('memory://');
|
||||||
const conf = new DittoConf(new Map());
|
const conf = new DittoConf(new Map());
|
||||||
const relay = new MockRelay();
|
const relay = new MockRelay();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
import { DittoConf } from '@ditto/conf';
|
||||||
|
import { DittoPolyPg } from '@ditto/db';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { DittoPgStore } from '../packages/ditto/storages/DittoPgStore.ts';
|
||||||
import {
|
import {
|
||||||
updateTrendingEvents,
|
updateTrendingEvents,
|
||||||
updateTrendingHashtags,
|
updateTrendingHashtags,
|
||||||
|
|
@ -8,6 +11,11 @@ import {
|
||||||
updateTrendingZappedEvents,
|
updateTrendingZappedEvents,
|
||||||
} from '../packages/ditto/trends.ts';
|
} from '../packages/ditto/trends.ts';
|
||||||
|
|
||||||
|
const conf = new DittoConf(Deno.env);
|
||||||
|
const db = new DittoPolyPg(conf.databaseUrl);
|
||||||
|
const relay = new DittoPgStore({ db, pubkey: await conf.signer.getPublicKey() });
|
||||||
|
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']);
|
||||||
const trends = trendSchema.array().parse(Deno.args);
|
const trends = trendSchema.array().parse(Deno.args);
|
||||||
|
|
||||||
|
|
@ -19,23 +27,23 @@ for (const trend of trends) {
|
||||||
switch (trend) {
|
switch (trend) {
|
||||||
case 'pubkeys':
|
case 'pubkeys':
|
||||||
console.log('Updating trending pubkeys...');
|
console.log('Updating trending pubkeys...');
|
||||||
await updateTrendingPubkeys();
|
await updateTrendingPubkeys(ctx);
|
||||||
break;
|
break;
|
||||||
case 'zapped_events':
|
case 'zapped_events':
|
||||||
console.log('Updating trending zapped events...');
|
console.log('Updating trending zapped events...');
|
||||||
await updateTrendingZappedEvents();
|
await updateTrendingZappedEvents(ctx);
|
||||||
break;
|
break;
|
||||||
case 'events':
|
case 'events':
|
||||||
console.log('Updating trending events...');
|
console.log('Updating trending events...');
|
||||||
await updateTrendingEvents();
|
await updateTrendingEvents(ctx);
|
||||||
break;
|
break;
|
||||||
case 'hashtags':
|
case 'hashtags':
|
||||||
console.log('Updating trending hashtags...');
|
console.log('Updating trending hashtags...');
|
||||||
await updateTrendingHashtags();
|
await updateTrendingHashtags(ctx);
|
||||||
break;
|
break;
|
||||||
case 'links':
|
case 'links':
|
||||||
console.log('Updating trending links...');
|
console.log('Updating trending links...');
|
||||||
await updateTrendingLinks();
|
await updateTrendingLinks(ctx);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue