mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Rename @ditto/api to @ditto/mastoapi, start using the new router and middleware in app
This commit is contained in:
parent
22d7a5fa55
commit
67aec57990
67 changed files with 1134 additions and 769 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"workspace": [
|
"workspace": [
|
||||||
"./packages/api",
|
|
||||||
"./packages/conf",
|
"./packages/conf",
|
||||||
"./packages/db",
|
"./packages/db",
|
||||||
"./packages/ditto",
|
"./packages/ditto",
|
||||||
"./packages/lang",
|
"./packages/lang",
|
||||||
|
"./packages/mastoapi",
|
||||||
"./packages/metrics",
|
"./packages/metrics",
|
||||||
"./packages/policies",
|
"./packages/policies",
|
||||||
"./packages/ratelimiter",
|
"./packages/ratelimiter",
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { Hono } from '@hono/hono';
|
|
||||||
import { assertEquals } from '@std/assert';
|
|
||||||
|
|
||||||
import { confMw } from './confMw.ts';
|
|
||||||
|
|
||||||
Deno.test('confMw', async () => {
|
|
||||||
const env = new Map([
|
|
||||||
['DITTO_NSEC', 'nsec19shyxpuzd0cq2p5078fwnws7tyykypud6z205fzhlmlrs2vpz6hs83zwkw'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
const app = new Hono();
|
|
||||||
|
|
||||||
app.get('/', confMw(env), async (c) => c.text(await c.var.conf.signer.getPublicKey()));
|
|
||||||
|
|
||||||
const response = await app.request('/');
|
|
||||||
const body = await response.text();
|
|
||||||
|
|
||||||
assertEquals(body, '1ba0c5ed1bbbf3b7eb0d7843ba16836a0201ea68a76bafcba507358c45911ff6');
|
|
||||||
});
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { DittoConf } from '@ditto/conf';
|
|
||||||
|
|
||||||
import type { MiddlewareHandler } from '@hono/hono';
|
|
||||||
|
|
||||||
/** Set Ditto config. */
|
|
||||||
export function confMw(
|
|
||||||
env: { get(key: string): string | undefined },
|
|
||||||
): MiddlewareHandler<{ Variables: { conf: DittoConf } }> {
|
|
||||||
const conf = new DittoConf(env);
|
|
||||||
|
|
||||||
return async (c, next) => {
|
|
||||||
c.set('conf', conf);
|
|
||||||
await next();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { Hono } from '@hono/hono';
|
|
||||||
import { assertEquals } from '@std/assert';
|
|
||||||
|
|
||||||
import { confMw } from './confMw.ts';
|
|
||||||
import { confRequiredMw } from './confRequiredMw.ts';
|
|
||||||
|
|
||||||
Deno.test('confRequiredMw', async (t) => {
|
|
||||||
const app = new Hono();
|
|
||||||
|
|
||||||
app.get('/without', confRequiredMw, (c) => c.text('ok'));
|
|
||||||
app.get('/with', confMw(new Map()), confRequiredMw, (c) => c.text('ok'));
|
|
||||||
|
|
||||||
await t.step('without conf returns 500', async () => {
|
|
||||||
const response = await app.request('/without');
|
|
||||||
assertEquals(response.status, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
await t.step('with conf returns 200', async () => {
|
|
||||||
const response = await app.request('/with');
|
|
||||||
assertEquals(response.status, 200);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { HTTPException } from '@hono/hono/http-exception';
|
|
||||||
|
|
||||||
import type { DittoConf } from '@ditto/conf';
|
|
||||||
import type { MiddlewareHandler } from '@hono/hono';
|
|
||||||
|
|
||||||
/** Throws an error if conf isn't set. */
|
|
||||||
export const confRequiredMw: MiddlewareHandler<{ Variables: { conf: DittoConf } }> = async (c, next) => {
|
|
||||||
const { conf } = c.var;
|
|
||||||
|
|
||||||
if (!conf) {
|
|
||||||
throw new HTTPException(500, { message: 'Ditto config not set in request.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
await next();
|
|
||||||
};
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export { confMw } from './confMw.ts';
|
|
||||||
export { confRequiredMw } from './confRequiredMw.ts';
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
import { confMw } from '@ditto/api/middleware';
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { type DittoConf } from '@ditto/conf';
|
import { DittoDB } from '@ditto/db';
|
||||||
import { DittoTables } from '@ditto/db';
|
import { paginationMiddleware, userMiddleware } from '@ditto/mastoapi/middleware';
|
||||||
|
import { DittoApp, type DittoEnv } from '@ditto/router';
|
||||||
import { type DittoTranslator } from '@ditto/translators';
|
import { type DittoTranslator } from '@ditto/translators';
|
||||||
import { type Context, Env as HonoEnv, Handler, Hono, 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';
|
||||||
import { cors } from '@hono/hono/cors';
|
import { cors } from '@hono/hono/cors';
|
||||||
import { serveStatic } from '@hono/hono/deno';
|
import { serveStatic } from '@hono/hono/deno';
|
||||||
import { NostrEvent, NostrSigner, NStore, NUploader } from '@nostrify/nostrify';
|
import { NostrEvent, NostrSigner, NRelay, NUploader } from '@nostrify/nostrify';
|
||||||
import { Kysely } from 'kysely';
|
|
||||||
|
|
||||||
import '@/startup.ts';
|
import '@/startup.ts';
|
||||||
|
|
||||||
|
import { Conf } from '@/config.ts';
|
||||||
|
import { Storages } from '@/storages.ts';
|
||||||
import { Time } from '@/utils/time.ts';
|
import { Time } from '@/utils/time.ts';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -140,34 +142,33 @@ import { cacheControlMiddleware } from '@/middleware/cacheControlMiddleware.ts';
|
||||||
import { cspMiddleware } from '@/middleware/cspMiddleware.ts';
|
import { cspMiddleware } from '@/middleware/cspMiddleware.ts';
|
||||||
import { metricsMiddleware } from '@/middleware/metricsMiddleware.ts';
|
import { metricsMiddleware } from '@/middleware/metricsMiddleware.ts';
|
||||||
import { notActivitypubMiddleware } from '@/middleware/notActivitypubMiddleware.ts';
|
import { notActivitypubMiddleware } from '@/middleware/notActivitypubMiddleware.ts';
|
||||||
import { paginationMiddleware } from '@/middleware/paginationMiddleware.ts';
|
|
||||||
import { rateLimitMiddleware } from '@/middleware/rateLimitMiddleware.ts';
|
import { rateLimitMiddleware } from '@/middleware/rateLimitMiddleware.ts';
|
||||||
import { requireSigner } from '@/middleware/requireSigner.ts';
|
|
||||||
import { signerMiddleware } from '@/middleware/signerMiddleware.ts';
|
|
||||||
import { storeMiddleware } from '@/middleware/storeMiddleware.ts';
|
|
||||||
import { uploaderMiddleware } from '@/middleware/uploaderMiddleware.ts';
|
import { uploaderMiddleware } from '@/middleware/uploaderMiddleware.ts';
|
||||||
import { translatorMiddleware } from '@/middleware/translatorMiddleware.ts';
|
import { translatorMiddleware } from '@/middleware/translatorMiddleware.ts';
|
||||||
import { logiMiddleware } from '@/middleware/logiMiddleware.ts';
|
import { logiMiddleware } from '@/middleware/logiMiddleware.ts';
|
||||||
|
|
||||||
export interface AppEnv extends HonoEnv {
|
export interface AppEnv extends DittoEnv {
|
||||||
Variables: {
|
Variables: {
|
||||||
conf: DittoConf;
|
conf: DittoConf;
|
||||||
/** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */
|
|
||||||
signer?: NostrSigner;
|
|
||||||
/** Uploader for the user to upload files. */
|
/** Uploader for the user to upload files. */
|
||||||
uploader?: NUploader;
|
uploader?: NUploader;
|
||||||
/** NIP-98 signed event proving the pubkey is owned by the user. */
|
/** NIP-98 signed event proving the pubkey is owned by the user. */
|
||||||
proof?: NostrEvent;
|
proof?: NostrEvent;
|
||||||
/** Kysely instance for the database. */
|
/** Kysely instance for the database. */
|
||||||
kysely: Kysely<DittoTables>;
|
db: DittoDB;
|
||||||
/** Storage for the user, might filter out unwanted content. */
|
/** Base database store. No content filtering. */
|
||||||
store: NStore;
|
relay: NRelay;
|
||||||
/** Normalized pagination params. */
|
/** Normalized pagination params. */
|
||||||
pagination: { since?: number; until?: number; limit: number };
|
pagination: { since?: number; until?: number; limit: number };
|
||||||
/** Normalized list pagination params. */
|
|
||||||
listPagination: { offset: number; limit: number };
|
|
||||||
/** Translation service. */
|
/** Translation service. */
|
||||||
translator?: DittoTranslator;
|
translator?: DittoTranslator;
|
||||||
|
signal: AbortSignal;
|
||||||
|
user?: {
|
||||||
|
/** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */
|
||||||
|
signer: NostrSigner;
|
||||||
|
/** User's relay. Might filter out unwanted content. */
|
||||||
|
relay: NRelay;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,21 +177,29 @@ 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 Hono<AppEnv>({ strict: false });
|
const app = new DittoApp({
|
||||||
|
conf: Conf,
|
||||||
|
db: await Storages.database(),
|
||||||
|
relay: await Storages.db(),
|
||||||
|
}, {
|
||||||
|
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. */
|
||||||
const staticFiles = serveStatic({ root: new URL('./static/', import.meta.url).pathname });
|
const staticFiles = serveStatic({ root: new URL('./static/', import.meta.url).pathname });
|
||||||
|
|
||||||
app.use(confMw(Deno.env), cacheControlMiddleware({ noStore: true }));
|
app.use(cacheControlMiddleware({ noStore: true }));
|
||||||
|
|
||||||
const ratelimit = every(
|
const ratelimit = every(
|
||||||
rateLimitMiddleware(30, Time.seconds(5), false),
|
rateLimitMiddleware(30, Time.seconds(5), false),
|
||||||
rateLimitMiddleware(300, Time.minutes(5), false),
|
rateLimitMiddleware(300, Time.minutes(5), false),
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use('/api/*', metricsMiddleware, ratelimit, paginationMiddleware, logiMiddleware);
|
const requireSigner = userMiddleware({ privileged: false, required: true });
|
||||||
|
|
||||||
|
app.use('/api/*', metricsMiddleware, ratelimit, paginationMiddleware(), logiMiddleware);
|
||||||
app.use('/.well-known/*', metricsMiddleware, ratelimit, logiMiddleware);
|
app.use('/.well-known/*', metricsMiddleware, ratelimit, logiMiddleware);
|
||||||
app.use('/nodeinfo/*', metricsMiddleware, ratelimit, logiMiddleware);
|
app.use('/nodeinfo/*', metricsMiddleware, ratelimit, logiMiddleware);
|
||||||
app.use('/oauth/*', metricsMiddleware, ratelimit, logiMiddleware);
|
app.use('/oauth/*', metricsMiddleware, ratelimit, logiMiddleware);
|
||||||
|
|
@ -201,10 +210,8 @@ app.get('/relay', metricsMiddleware, ratelimit, relayController);
|
||||||
app.use(
|
app.use(
|
||||||
cspMiddleware(),
|
cspMiddleware(),
|
||||||
cors({ origin: '*', exposeHeaders: ['link'] }),
|
cors({ origin: '*', exposeHeaders: ['link'] }),
|
||||||
signerMiddleware,
|
|
||||||
uploaderMiddleware,
|
uploaderMiddleware,
|
||||||
auth98Middleware(),
|
auth98Middleware(),
|
||||||
storeMiddleware,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
app.get('/metrics', metricsController);
|
app.get('/metrics', metricsController);
|
||||||
|
|
@ -251,7 +258,7 @@ app.post('/oauth/revoke', revokeTokenController);
|
||||||
app.post('/oauth/authorize', oauthAuthorizeController);
|
app.post('/oauth/authorize', oauthAuthorizeController);
|
||||||
app.get('/oauth/authorize', oauthController);
|
app.get('/oauth/authorize', oauthController);
|
||||||
|
|
||||||
app.post('/api/v1/accounts', requireProof({ pow: 20 }), createAccountController);
|
app.post('/api/v1/accounts', requireProof(), createAccountController);
|
||||||
app.get('/api/v1/accounts/verify_credentials', requireSigner, verifyCredentialsController);
|
app.get('/api/v1/accounts/verify_credentials', requireSigner, verifyCredentialsController);
|
||||||
app.patch('/api/v1/accounts/update_credentials', requireSigner, updateCredentialsController);
|
app.patch('/api/v1/accounts/update_credentials', requireSigner, updateCredentialsController);
|
||||||
app.get('/api/v1/accounts/search', accountSearchController);
|
app.get('/api/v1/accounts/search', accountSearchController);
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ const createAccountSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const createAccountController: AppController = async (c) => {
|
const createAccountController: AppController = async (c) => {
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
const { user } = c.var;
|
||||||
|
|
||||||
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const result = createAccountSchema.safeParse(await c.req.json());
|
const result = createAccountSchema.safeParse(await c.req.json());
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|
@ -46,15 +48,15 @@ const createAccountController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyCredentialsController: AppController = async (c) => {
|
const verifyCredentialsController: AppController = async (c) => {
|
||||||
const signer = c.get('signer')!;
|
const { relay, user } = c.var;
|
||||||
const pubkey = await signer.getPublicKey();
|
|
||||||
|
|
||||||
const store = await Storages.db();
|
const signer = user!.signer;
|
||||||
|
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, { signal: AbortSignal.timeout(5000) }),
|
||||||
|
|
||||||
store.query([{
|
relay.query([{
|
||||||
kinds: [30078],
|
kinds: [30078],
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
'#d': ['pub.ditto.pleroma_settings_store'],
|
'#d': ['pub.ditto.pleroma_settings_store'],
|
||||||
|
|
@ -115,12 +117,10 @@ const accountSearchQuerySchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const accountSearchController: AppController = async (c) => {
|
const accountSearchController: AppController = async (c) => {
|
||||||
const { store } = c.var;
|
const { db, relay, user, pagination, signal } = c.var;
|
||||||
const { signal } = c.req.raw;
|
const { limit } = pagination;
|
||||||
const { limit } = c.get('pagination');
|
|
||||||
|
|
||||||
const kysely = await Storages.kysely();
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
|
||||||
|
|
||||||
const result = accountSearchQuerySchema.safeParse(c.req.query());
|
const result = accountSearchQuerySchema.safeParse(c.req.query());
|
||||||
|
|
||||||
|
|
@ -144,8 +144,8 @@ const accountSearchController: AppController = async (c) => {
|
||||||
events.push(event);
|
events.push(event);
|
||||||
} else {
|
} else {
|
||||||
const following = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set<string>();
|
const following = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set<string>();
|
||||||
const authors = [...await getPubkeysBySearch(kysely, { q: query, limit, offset: 0, following })];
|
const authors = [...await getPubkeysBySearch(db.kysely, { q: query, limit, offset: 0, following })];
|
||||||
const profiles = await store.query([{ kinds: [0], authors, limit }], { signal });
|
const profiles = await relay.query([{ kinds: [0], authors, limit }], { signal });
|
||||||
|
|
||||||
for (const pubkey of authors) {
|
for (const pubkey of authors) {
|
||||||
const profile = profiles.find((event) => event.pubkey === pubkey);
|
const profile = profiles.find((event) => event.pubkey === pubkey);
|
||||||
|
|
@ -155,14 +155,16 @@ const accountSearchController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const accounts = await hydrateEvents({ events, store, signal })
|
const accounts = await hydrateEvents({ events, relay, signal })
|
||||||
.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 pubkey = await c.get('signer')?.getPublicKey()!;
|
const { user } = c.var;
|
||||||
|
|
||||||
|
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[]'));
|
||||||
|
|
||||||
if (!ids.success) {
|
if (!ids.success) {
|
||||||
|
|
@ -201,17 +203,17 @@ const accountStatusesQuerySchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const accountStatusesController: AppController = async (c) => {
|
const accountStatusesController: AppController = async (c) => {
|
||||||
|
const { conf, user, signal } = c.var;
|
||||||
|
|
||||||
const pubkey = c.req.param('pubkey');
|
const pubkey = c.req.param('pubkey');
|
||||||
const { conf } = c.var;
|
|
||||||
const { since, until } = c.var.pagination;
|
const { since, until } = c.var.pagination;
|
||||||
const { pinned, limit, exclude_replies, tagged, only_media } = accountStatusesQuerySchema.parse(c.req.query());
|
const { pinned, limit, exclude_replies, tagged, only_media } = accountStatusesQuerySchema.parse(c.req.query());
|
||||||
const { signal } = c.req.raw;
|
|
||||||
|
|
||||||
const store = await Storages.db();
|
const { relay } = c.var;
|
||||||
|
|
||||||
const [[author], [user]] = await Promise.all([
|
const [[author], [userEvent]] = await Promise.all([
|
||||||
store.query([{ kinds: [0], authors: [pubkey], limit: 1 }], { signal }),
|
relay.query([{ kinds: [0], authors: [pubkey], limit: 1 }], { signal }),
|
||||||
store.query([{ kinds: [30382], authors: [await conf.signer.getPublicKey()], '#d': [pubkey], limit: 1 }], {
|
relay.query([{ kinds: [30382], authors: [await conf.signer.getPublicKey()], '#d': [pubkey], limit: 1 }], {
|
||||||
signal,
|
signal,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
@ -220,14 +222,14 @@ const accountStatusesController: AppController = async (c) => {
|
||||||
assertAuthenticated(c, author);
|
assertAuthenticated(c, author);
|
||||||
}
|
}
|
||||||
|
|
||||||
const names = getTagSet(user?.tags ?? [], 'n');
|
const names = getTagSet(userEvent?.tags ?? [], 'n');
|
||||||
|
|
||||||
if (names.has('disabled')) {
|
if (names.has('disabled')) {
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pinned) {
|
if (pinned) {
|
||||||
const [pinEvent] = await store.query([{ kinds: [10001], authors: [pubkey], limit: 1 }], { signal });
|
const [pinEvent] = await relay.query([{ kinds: [10001], authors: [pubkey], limit: 1 }], { signal });
|
||||||
if (pinEvent) {
|
if (pinEvent) {
|
||||||
const pinnedEventIds = getTagSet(pinEvent.tags, 'e');
|
const pinnedEventIds = getTagSet(pinEvent.tags, 'e');
|
||||||
return renderStatuses(c, [...pinnedEventIds].reverse());
|
return renderStatuses(c, [...pinnedEventIds].reverse());
|
||||||
|
|
@ -264,8 +266,8 @@ 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 store.query([filter], opts)
|
const events = await relay.query([filter], opts)
|
||||||
.then((events) => hydrateEvents({ events, store, signal }))
|
.then((events) => hydrateEvents({ events, relay, signal }))
|
||||||
.then((events) => {
|
.then((events) => {
|
||||||
if (exclude_replies) {
|
if (exclude_replies) {
|
||||||
return events.filter((event) => {
|
return events.filter((event) => {
|
||||||
|
|
@ -276,7 +278,7 @@ const accountStatusesController: AppController = async (c) => {
|
||||||
return events;
|
return events;
|
||||||
});
|
});
|
||||||
|
|
||||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
|
|
||||||
const statuses = await Promise.all(
|
const statuses = await Promise.all(
|
||||||
events.map((event) => {
|
events.map((event) => {
|
||||||
|
|
@ -303,12 +305,11 @@ const updateCredentialsSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateCredentialsController: AppController = async (c) => {
|
const updateCredentialsController: AppController = async (c) => {
|
||||||
const signer = c.get('signer')!;
|
const { relay, user, signal } = c.var;
|
||||||
const pubkey = await signer.getPublicKey();
|
|
||||||
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const body = await parseBody(c.req.raw);
|
const body = await parseBody(c.req.raw);
|
||||||
const result = updateCredentialsSchema.safeParse(body);
|
const result = updateCredentialsSchema.safeParse(body);
|
||||||
const store = await Storages.db();
|
|
||||||
const signal = c.req.raw.signal;
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return c.json(result.error, 422);
|
return c.json(result.error, 422);
|
||||||
|
|
@ -318,7 +319,7 @@ const updateCredentialsController: AppController = async (c) => {
|
||||||
let event: NostrEvent | undefined;
|
let event: NostrEvent | undefined;
|
||||||
|
|
||||||
if (keys.length === 1 && keys[0] === 'pleroma_settings_store') {
|
if (keys.length === 1 && keys[0] === 'pleroma_settings_store') {
|
||||||
event = (await store.query([{ kinds: [0], authors: [pubkey] }]))[0];
|
event = (await relay.query([{ kinds: [0], authors: [pubkey] }]))[0];
|
||||||
} else {
|
} else {
|
||||||
event = await updateEvent(
|
event = await updateEvent(
|
||||||
{ kinds: [0], authors: [pubkey], limit: 1 },
|
{ kinds: [0], authors: [pubkey], limit: 1 },
|
||||||
|
|
@ -374,7 +375,7 @@ const updateCredentialsController: AppController = async (c) => {
|
||||||
|
|
||||||
let account: MastodonAccount;
|
let account: MastodonAccount;
|
||||||
if (event) {
|
if (event) {
|
||||||
await hydrateEvents({ events: [event], store, signal });
|
await hydrateEvents({ events: [event], relay, signal });
|
||||||
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 });
|
||||||
|
|
@ -393,7 +394,9 @@ 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 sourcePubkey = await c.get('signer')?.getPublicKey()!;
|
const { user } = c.var;
|
||||||
|
|
||||||
|
const sourcePubkey = await user!.signer.getPublicKey();
|
||||||
const targetPubkey = c.req.param('pubkey');
|
const targetPubkey = c.req.param('pubkey');
|
||||||
|
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -410,7 +413,9 @@ 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 sourcePubkey = await c.get('signer')?.getPublicKey()!;
|
const { user } = c.var;
|
||||||
|
|
||||||
|
const sourcePubkey = await user!.signer.getPublicKey();
|
||||||
const targetPubkey = c.req.param('pubkey');
|
const targetPubkey = c.req.param('pubkey');
|
||||||
|
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -447,7 +452,9 @@ 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 sourcePubkey = await c.get('signer')?.getPublicKey()!;
|
const { user } = c.var;
|
||||||
|
|
||||||
|
const sourcePubkey = await user!.signer.getPublicKey();
|
||||||
const targetPubkey = c.req.param('pubkey');
|
const targetPubkey = c.req.param('pubkey');
|
||||||
|
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -462,7 +469,9 @@ const muteController: AppController = async (c) => {
|
||||||
|
|
||||||
/** 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 sourcePubkey = await c.get('signer')?.getPublicKey()!;
|
const { user } = c.var;
|
||||||
|
|
||||||
|
const sourcePubkey = await user!.signer.getPublicKey();
|
||||||
const targetPubkey = c.req.param('pubkey');
|
const targetPubkey = c.req.param('pubkey');
|
||||||
|
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -476,14 +485,12 @@ const unmuteController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const favouritesController: AppController = async (c) => {
|
const favouritesController: AppController = async (c) => {
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
const { relay, user, pagination, signal } = c.var;
|
||||||
const params = c.get('pagination');
|
|
||||||
const { signal } = c.req.raw;
|
|
||||||
|
|
||||||
const store = await Storages.db();
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
|
|
||||||
const events7 = await store.query(
|
const events7 = await relay.query(
|
||||||
[{ kinds: [7], authors: [pubkey], ...params }],
|
[{ kinds: [7], authors: [pubkey], ...pagination }],
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -491,10 +498,10 @@ const favouritesController: AppController = async (c) => {
|
||||||
.map((event) => event.tags.find((tag) => tag[0] === 'e')?.[1])
|
.map((event) => event.tags.find((tag) => tag[0] === 'e')?.[1])
|
||||||
.filter((id): id is string => !!id);
|
.filter((id): id is string => !!id);
|
||||||
|
|
||||||
const events1 = await store.query([{ kinds: [1, 20], ids }], { signal })
|
const events1 = await relay.query([{ kinds: [1, 20], ids }], { signal })
|
||||||
.then((events) => hydrateEvents({ events, store, signal }));
|
.then((events) => hydrateEvents({ events, relay, signal }));
|
||||||
|
|
||||||
const viewerPubkey = await c.get('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(event, { viewerPubkey })),
|
||||||
|
|
@ -503,16 +510,15 @@ const favouritesController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const familiarFollowersController: AppController = async (c) => {
|
const familiarFollowersController: AppController = async (c) => {
|
||||||
const store = await Storages.db();
|
const { relay, user } = c.var;
|
||||||
const signer = c.get('signer')!;
|
|
||||||
const pubkey = await 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(pubkey);
|
||||||
|
|
||||||
const results = await Promise.all(ids.map(async (id) => {
|
const results = await Promise.all(ids.map(async (id) => {
|
||||||
const followLists = await store.query([{ kinds: [3], authors: [...follows], '#p': [id] }])
|
const followLists = await relay.query([{ kinds: [3], authors: [...follows], '#p': [id] }])
|
||||||
.then((events) => hydrateEvents({ events, store }));
|
.then((events) => hydrateEvents({ events, relay }));
|
||||||
|
|
||||||
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)),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ 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 { Storages } from '@/storages.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, paginated, parseBody, updateEventInfo, updateUser } from '@/utils/api.ts';
|
||||||
import { renderNameRequest } from '@/views/ditto.ts';
|
import { renderNameRequest } from '@/views/ditto.ts';
|
||||||
|
|
@ -29,10 +28,8 @@ const adminAccountQuerySchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const adminAccountsController: AppController = async (c) => {
|
const adminAccountsController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, signal, pagination } = c.var;
|
||||||
const store = await Storages.db();
|
|
||||||
const params = c.get('pagination');
|
|
||||||
const { signal } = c.req.raw;
|
|
||||||
const {
|
const {
|
||||||
local,
|
local,
|
||||||
pending,
|
pending,
|
||||||
|
|
@ -50,8 +47,8 @@ const adminAccountsController: AppController = async (c) => {
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const orig = await store.query(
|
const orig = await relay.query(
|
||||||
[{ kinds: [30383], authors: [adminPubkey], '#k': ['3036'], '#n': ['pending'], ...params }],
|
[{ kinds: [30383], authors: [adminPubkey], '#k': ['3036'], '#n': ['pending'], ...pagination }],
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -61,8 +58,8 @@ const adminAccountsController: AppController = async (c) => {
|
||||||
.filter((id): id is string => !!id),
|
.filter((id): id is string => !!id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const events = await store.query([{ kinds: [3036], ids: [...ids] }])
|
const events = await relay.query([{ kinds: [3036], ids: [...ids] }])
|
||||||
.then((events) => hydrateEvents({ store, events, signal }));
|
.then((events) => hydrateEvents({ relay, events, signal }));
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -88,8 +85,8 @@ const adminAccountsController: AppController = async (c) => {
|
||||||
n.push('moderator');
|
n.push('moderator');
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await store.query(
|
const events = await relay.query(
|
||||||
[{ kinds: [30382], authors: [adminPubkey], '#n': n, ...params }],
|
[{ kinds: [30382], authors: [adminPubkey], '#n': n, ...pagination }],
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -99,8 +96,8 @@ const adminAccountsController: AppController = async (c) => {
|
||||||
.filter((pubkey): pubkey is string => !!pubkey),
|
.filter((pubkey): pubkey is string => !!pubkey),
|
||||||
);
|
);
|
||||||
|
|
||||||
const authors = await store.query([{ kinds: [0], authors: [...pubkeys] }])
|
const authors = await relay.query([{ kinds: [0], authors: [...pubkeys] }])
|
||||||
.then((events) => hydrateEvents({ store, events, signal }));
|
.then((events) => hydrateEvents({ relay, events, signal }));
|
||||||
|
|
||||||
const accounts = await Promise.all(
|
const accounts = await Promise.all(
|
||||||
[...pubkeys].map((pubkey) => {
|
[...pubkeys].map((pubkey) => {
|
||||||
|
|
@ -112,14 +109,14 @@ const adminAccountsController: AppController = async (c) => {
|
||||||
return paginated(c, events, accounts);
|
return paginated(c, events, accounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter: NostrFilter = { kinds: [0], ...params };
|
const filter: NostrFilter = { kinds: [0], ...pagination };
|
||||||
|
|
||||||
if (local) {
|
if (local) {
|
||||||
filter.search = `domain:${conf.url.host}`;
|
filter.search = `domain:${conf.url.host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await store.query([filter], { signal })
|
const events = await relay.query([filter], { signal })
|
||||||
.then((events) => hydrateEvents({ store, events, signal }));
|
.then((events) => hydrateEvents({ relay, events, signal }));
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -130,9 +127,9 @@ const adminAccountActionSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const adminActionController: AppController = async (c) => {
|
const adminActionController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay } = c.var;
|
||||||
|
|
||||||
const body = await parseBody(c.req.raw);
|
const body = await parseBody(c.req.raw);
|
||||||
const store = await Storages.db();
|
|
||||||
const result = adminAccountActionSchema.safeParse(body);
|
const result = adminAccountActionSchema.safeParse(body);
|
||||||
const authorId = c.req.param('id');
|
const authorId = c.req.param('id');
|
||||||
|
|
||||||
|
|
@ -156,13 +153,13 @@ const adminActionController: AppController = async (c) => {
|
||||||
if (data.type === 'suspend') {
|
if (data.type === 'suspend') {
|
||||||
n.disabled = true;
|
n.disabled = true;
|
||||||
n.suspended = true;
|
n.suspended = true;
|
||||||
store.remove([{ authors: [authorId] }]).catch((e: unknown) => {
|
relay.remove!([{ authors: [authorId] }]).catch((e: unknown) => {
|
||||||
logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, error: errorJson(e) });
|
logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, error: errorJson(e) });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (data.type === 'revoke_name') {
|
if (data.type === 'revoke_name') {
|
||||||
n.revoke_name = true;
|
n.revoke_name = true;
|
||||||
store.remove([{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#p': [authorId] }]).catch(
|
relay.remove!([{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#p': [authorId] }]).catch(
|
||||||
(e: unknown) => {
|
(e: unknown) => {
|
||||||
logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, error: errorJson(e) });
|
logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, error: errorJson(e) });
|
||||||
},
|
},
|
||||||
|
|
@ -177,9 +174,9 @@ const adminActionController: AppController = async (c) => {
|
||||||
const adminApproveController: AppController = async (c) => {
|
const adminApproveController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf } = c.var;
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
const store = await Storages.db();
|
const { relay } = c.var;
|
||||||
|
|
||||||
const [event] = await store.query([{ kinds: [3036], ids: [eventId] }]);
|
const [event] = await relay.query([{ kinds: [3036], ids: [eventId] }]);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return c.json({ error: 'Event not found' }, 404);
|
return c.json({ error: 'Event not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +189,7 @@ const adminApproveController: AppController = async (c) => {
|
||||||
return c.json({ error: 'Invalid NIP-05' }, 400);
|
return c.json({ error: 'Invalid NIP-05' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [existing] = await store.query([
|
const [existing] = await relay.query([
|
||||||
{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#d': [r], limit: 1 },
|
{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#d': [r], limit: 1 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -212,7 +209,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], store });
|
await hydrateEvents({ events: [event], relay });
|
||||||
|
|
||||||
const nameRequest = await renderNameRequest(event);
|
const nameRequest = await renderNameRequest(event);
|
||||||
return c.json(nameRequest);
|
return c.json(nameRequest);
|
||||||
|
|
@ -220,15 +217,15 @@ const adminApproveController: AppController = async (c) => {
|
||||||
|
|
||||||
const adminRejectController: AppController = async (c) => {
|
const adminRejectController: AppController = async (c) => {
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
const store = await Storages.db();
|
const { relay } = c.var;
|
||||||
|
|
||||||
const [event] = await store.query([{ kinds: [3036], ids: [eventId] }]);
|
const [event] = await relay.query([{ kinds: [3036], ids: [eventId] }]);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return c.json({ error: 'Event not found' }, 404);
|
return c.json({ error: 'Event not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateEventInfo(eventId, { pending: false, approved: false, rejected: true }, c);
|
await updateEventInfo(eventId, { pending: false, approved: false, rejected: true }, c);
|
||||||
await hydrateEvents({ events: [event], store });
|
await hydrateEvents({ events: [event], relay });
|
||||||
|
|
||||||
const nameRequest = await renderNameRequest(event);
|
const nameRequest = await renderNameRequest(event);
|
||||||
return c.json(nameRequest);
|
return c.json(nameRequest);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { getTagSet } from '@/utils/tags.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
import { renderStatuses } from '@/views.ts';
|
import { renderStatuses } from '@/views.ts';
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/bookmarks/#get */
|
/** https://docs.joinmastodon.org/methods/bookmarks/#get */
|
||||||
const bookmarksController: AppController = async (c) => {
|
const bookmarksController: AppController = async (c) => {
|
||||||
const store = await Storages.db();
|
const { relay, user, signal } = c.var;
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
|
||||||
const { signal } = c.req.raw;
|
|
||||||
|
|
||||||
const [event10003] = await store.query(
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
|
|
||||||
|
const [event10003] = await relay.query(
|
||||||
[{ kinds: [10003], authors: [pubkey], limit: 1 }],
|
[{ kinds: [10003], authors: [pubkey], limit: 1 }],
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -152,9 +152,11 @@ const pointSchema = z.object({
|
||||||
|
|
||||||
/** Verify the captcha solution and sign an event in the database. */
|
/** Verify the captcha solution and sign an event in the database. */
|
||||||
export const captchaVerifyController: AppController = async (c) => {
|
export const captchaVerifyController: AppController = async (c) => {
|
||||||
|
const { user } = c.var;
|
||||||
|
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const result = pointSchema.safeParse(await c.req.json());
|
const result = pointSchema.safeParse(await c.req.json());
|
||||||
const pubkey = await c.get('signer')!.getPublicKey();
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return c.json({ error: 'Invalid input' }, { status: 422 });
|
return c.json({ error: 'Invalid input' }, { status: 422 });
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { confMw } from '@ditto/api/middleware';
|
|
||||||
import { Env as HonoEnv, Hono } from '@hono/hono';
|
import { Env as HonoEnv, Hono } from '@hono/hono';
|
||||||
import { NostrSigner, NSecSigner, NStore } from '@nostrify/nostrify';
|
import { NostrSigner, NSecSigner, NStore } from '@nostrify/nostrify';
|
||||||
import { genEvent } from '@nostrify/nostrify/test';
|
import { genEvent } from '@nostrify/nostrify/test';
|
||||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
|
||||||
import { bytesToString, stringToBytes } from '@scure/base';
|
import { bytesToString, stringToBytes } from '@scure/base';
|
||||||
import { stub } from '@std/testing/mock';
|
import { stub } from '@std/testing/mock';
|
||||||
import { assertEquals, assertExists, assertObjectMatch } from '@std/assert';
|
import { assertEquals, assertExists, assertObjectMatch } from '@std/assert';
|
||||||
|
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||||
|
|
||||||
import { createTestDB } from '@/test.ts';
|
import { createTestDB } from '@/test.ts';
|
||||||
|
|
||||||
|
|
@ -44,7 +43,6 @@ Deno.test('PUT /wallet must be successful', {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(confMw(new Map()));
|
|
||||||
app.route('/', cashuApp);
|
app.route('/', cashuApp);
|
||||||
|
|
||||||
const response = await app.request('/wallet', {
|
const response = await app.request('/wallet', {
|
||||||
|
|
@ -123,7 +121,6 @@ Deno.test('PUT /wallet must NOT be successful: wrong request body/schema', async
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(confMw(new Map()));
|
|
||||||
app.route('/', cashuApp);
|
app.route('/', cashuApp);
|
||||||
|
|
||||||
const response = await app.request('/wallet', {
|
const response = await app.request('/wallet', {
|
||||||
|
|
@ -162,7 +159,6 @@ Deno.test('PUT /wallet must NOT be successful: wallet already exists', {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(confMw(new Map()));
|
|
||||||
app.route('/', cashuApp);
|
app.route('/', cashuApp);
|
||||||
|
|
||||||
await db.store.event(genEvent({ kind: 17375 }, sk));
|
await db.store.event(genEvent({ kind: 17375 }, sk));
|
||||||
|
|
@ -206,7 +202,6 @@ Deno.test('GET /wallet must be successful', {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(confMw(new Map()));
|
|
||||||
app.route('/', cashuApp);
|
app.route('/', cashuApp);
|
||||||
|
|
||||||
// Wallet
|
// Wallet
|
||||||
|
|
@ -312,7 +307,6 @@ Deno.test('GET /mints must be successful', async () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(confMw(new Map()));
|
|
||||||
app.route('/', cashuApp);
|
app.route('/', cashuApp);
|
||||||
|
|
||||||
const response = await app.request('/mints', {
|
const response = await app.request('/mints', {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
import { Proof } from '@cashu/cashu-ts';
|
import { Proof } from '@cashu/cashu-ts';
|
||||||
import { confRequiredMw } from '@ditto/api/middleware';
|
import { userMiddleware } from '@ditto/mastoapi/middleware';
|
||||||
import { Hono } from '@hono/hono';
|
import { DittoMiddleware, DittoRoute } from '@ditto/router';
|
||||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||||
import { bytesToString, stringToBytes } from '@scure/base';
|
import { bytesToString, stringToBytes } from '@scure/base';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { createEvent, parseBody } from '@/utils/api.ts';
|
import { createEvent, parseBody } from '@/utils/api.ts';
|
||||||
import { requireNip44Signer } from '@/middleware/requireSigner.ts';
|
|
||||||
import { requireStore } from '@/middleware/storeMiddleware.ts';
|
|
||||||
import { walletSchema } from '@/schema.ts';
|
import { walletSchema } from '@/schema.ts';
|
||||||
import { swapNutzapsMiddleware } from '@/middleware/swapNutzapsMiddleware.ts';
|
import { swapNutzapsMiddleware } from '@/middleware/swapNutzapsMiddleware.ts';
|
||||||
import { isNostrId } from '@/utils.ts';
|
import { isNostrId } from '@/utils.ts';
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
import { errorJson } from '@/utils/log.ts';
|
import { errorJson } from '@/utils/log.ts';
|
||||||
|
import { SetRequired } from 'type-fest';
|
||||||
|
import { NostrSigner } from '@nostrify/nostrify';
|
||||||
|
|
||||||
type Wallet = z.infer<typeof walletSchema>;
|
type Wallet = z.infer<typeof walletSchema>;
|
||||||
|
|
||||||
const app = new Hono().use('*', confRequiredMw, requireStore);
|
const app = new DittoRoute();
|
||||||
|
|
||||||
// app.delete('/wallet') -> 204
|
// app.delete('/wallet') -> 204
|
||||||
|
|
||||||
|
|
@ -33,6 +33,19 @@ interface Nutzap {
|
||||||
recipient_pubkey: string;
|
recipient_pubkey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requireNip44Signer: DittoMiddleware<{ user: { signer: SetRequired<NostrSigner, 'nip44'> } }> = async (
|
||||||
|
c,
|
||||||
|
next,
|
||||||
|
) => {
|
||||||
|
const { user } = c.var;
|
||||||
|
|
||||||
|
if (!user?.signer.nip44) {
|
||||||
|
return c.json({ error: 'User does not have a NIP-44 signer' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
|
||||||
const createCashuWalletAndNutzapInfoSchema = z.object({
|
const createCashuWalletAndNutzapInfoSchema = z.object({
|
||||||
mints: z.array(z.string().url()).nonempty().transform((val) => {
|
mints: z.array(z.string().url()).nonempty().transform((val) => {
|
||||||
return [...new Set(val)];
|
return [...new Set(val)];
|
||||||
|
|
@ -44,12 +57,11 @@ const createCashuWalletAndNutzapInfoSchema = z.object({
|
||||||
* https://github.com/nostr-protocol/nips/blob/master/60.md
|
* https://github.com/nostr-protocol/nips/blob/master/60.md
|
||||||
* https://github.com/nostr-protocol/nips/blob/master/61.md#nutzap-informational-event
|
* https://github.com/nostr-protocol/nips/blob/master/61.md#nutzap-informational-event
|
||||||
*/
|
*/
|
||||||
app.put('/wallet', requireNip44Signer, async (c) => {
|
app.put('/wallet', userMiddleware({ privileged: false, required: true }), requireNip44Signer, async (c) => {
|
||||||
const { conf, signer } = c.var;
|
const { conf, user, relay, signal } = c.var;
|
||||||
const store = c.get('store');
|
|
||||||
const pubkey = await signer.getPublicKey();
|
const pubkey = await user.signer.getPublicKey();
|
||||||
const body = await parseBody(c.req.raw);
|
const body = await parseBody(c.req.raw);
|
||||||
const { signal } = c.req.raw;
|
|
||||||
const result = createCashuWalletAndNutzapInfoSchema.safeParse(body);
|
const result = createCashuWalletAndNutzapInfoSchema.safeParse(body);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|
@ -58,7 +70,7 @@ app.put('/wallet', requireNip44Signer, async (c) => {
|
||||||
|
|
||||||
const { mints } = result.data;
|
const { mints } = result.data;
|
||||||
|
|
||||||
const [event] = await store.query([{ authors: [pubkey], kinds: [17375] }], { signal });
|
const [event] = await relay.query([{ authors: [pubkey], kinds: [17375] }], { signal });
|
||||||
if (event) {
|
if (event) {
|
||||||
return c.json({ error: 'You already have a wallet 😏' }, 400);
|
return c.json({ error: 'You already have a wallet 😏' }, 400);
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +87,7 @@ app.put('/wallet', requireNip44Signer, async (c) => {
|
||||||
walletContentTags.push(['mint', mint]);
|
walletContentTags.push(['mint', mint]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptedWalletContentTags = await signer.nip44.encrypt(pubkey, JSON.stringify(walletContentTags));
|
const encryptedWalletContentTags = await user.signer.nip44.encrypt(pubkey, JSON.stringify(walletContentTags));
|
||||||
|
|
||||||
// Wallet
|
// Wallet
|
||||||
await createEvent({
|
await createEvent({
|
||||||
|
|
@ -105,18 +117,22 @@ app.put('/wallet', requireNip44Signer, async (c) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Gets a wallet, if it exists. */
|
/** Gets a wallet, if it exists. */
|
||||||
app.get('/wallet', requireNip44Signer, swapNutzapsMiddleware, async (c) => {
|
app.get(
|
||||||
const { conf, signer } = c.var;
|
'/wallet',
|
||||||
const store = c.get('store');
|
userMiddleware({ privileged: false, required: true }),
|
||||||
const pubkey = await signer.getPublicKey();
|
requireNip44Signer,
|
||||||
const { signal } = c.req.raw;
|
swapNutzapsMiddleware,
|
||||||
|
async (c) => {
|
||||||
|
const { conf, relay, user, signal } = c.var;
|
||||||
|
|
||||||
const [event] = await store.query([{ authors: [pubkey], kinds: [17375] }], { signal });
|
const pubkey = await user.signer.getPublicKey();
|
||||||
|
|
||||||
|
const [event] = await relay.query([{ authors: [pubkey], kinds: [17375] }], { signal });
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return c.json({ error: 'Wallet not found' }, 404);
|
return c.json({ error: 'Wallet not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedContent: string[][] = JSON.parse(await signer.nip44.decrypt(pubkey, event.content));
|
const decryptedContent: string[][] = JSON.parse(await user.signer.nip44.decrypt(pubkey, event.content));
|
||||||
|
|
||||||
const privkey = decryptedContent.find(([value]) => value === 'privkey')?.[1];
|
const privkey = decryptedContent.find(([value]) => value === 'privkey')?.[1];
|
||||||
if (!privkey || !isNostrId(privkey)) {
|
if (!privkey || !isNostrId(privkey)) {
|
||||||
|
|
@ -128,11 +144,11 @@ app.get('/wallet', requireNip44Signer, swapNutzapsMiddleware, async (c) => {
|
||||||
let balance = 0;
|
let balance = 0;
|
||||||
const mints: string[] = [];
|
const mints: string[] = [];
|
||||||
|
|
||||||
const tokens = await store.query([{ authors: [pubkey], kinds: [7375] }], { signal });
|
const tokens = await relay.query([{ authors: [pubkey], kinds: [7375] }], { signal });
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
try {
|
try {
|
||||||
const decryptedContent: { mint: string; proofs: Proof[] } = JSON.parse(
|
const decryptedContent: { mint: string; proofs: Proof[] } = JSON.parse(
|
||||||
await signer.nip44.decrypt(pubkey, token.content),
|
await user.signer.nip44.decrypt(pubkey, token.content),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mints.includes(decryptedContent.mint)) {
|
if (!mints.includes(decryptedContent.mint)) {
|
||||||
|
|
@ -156,7 +172,8 @@ app.get('/wallet', requireNip44Signer, swapNutzapsMiddleware, async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return c.json(walletEntity, 200);
|
return c.json(walletEntity, 200);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/** Get mints set by the CASHU_MINTS environment variable. */
|
/** Get mints set by the CASHU_MINTS environment variable. */
|
||||||
app.get('/mints', (c) => {
|
app.get('/mints', (c) => {
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,9 @@ const relaySchema = z.object({
|
||||||
type RelayEntity = z.infer<typeof relaySchema>;
|
type RelayEntity = z.infer<typeof relaySchema>;
|
||||||
|
|
||||||
export const adminRelaysController: AppController = async (c) => {
|
export const adminRelaysController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay } = c.var;
|
||||||
const store = await Storages.db();
|
|
||||||
|
|
||||||
const [event] = await store.query([
|
const [event] = await relay.query([
|
||||||
{ kinds: [10002], authors: [await conf.signer.getPublicKey()], limit: 1 },
|
{ kinds: [10002], authors: [await conf.signer.getPublicKey()], limit: 1 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -43,8 +42,7 @@ export const adminRelaysController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const adminSetRelaysController: AppController = async (c) => {
|
export const adminSetRelaysController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay } = c.var;
|
||||||
const store = await Storages.db();
|
|
||||||
const relays = relaySchema.array().parse(await c.req.json());
|
const relays = relaySchema.array().parse(await c.req.json());
|
||||||
|
|
||||||
const event = await conf.signer.signEvent({
|
const event = await conf.signer.signEvent({
|
||||||
|
|
@ -54,7 +52,7 @@ export const adminSetRelaysController: AppController = async (c) => {
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
});
|
});
|
||||||
|
|
||||||
await store.event(event);
|
await relay.event(event);
|
||||||
|
|
||||||
return c.json(renderRelays(event));
|
return c.json(renderRelays(event));
|
||||||
};
|
};
|
||||||
|
|
@ -79,14 +77,12 @@ const nameRequestSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const nameRequestController: AppController = async (c) => {
|
export const nameRequestController: AppController = async (c) => {
|
||||||
const store = await Storages.db();
|
const { conf, relay, user } = c.var;
|
||||||
const signer = c.get('signer')!;
|
|
||||||
const pubkey = await signer.getPublicKey();
|
|
||||||
const { conf } = c.var;
|
|
||||||
|
|
||||||
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const { name, reason } = nameRequestSchema.parse(await c.req.json());
|
const { name, reason } = nameRequestSchema.parse(await c.req.json());
|
||||||
|
|
||||||
const [existing] = await store.query([{ kinds: [3036], authors: [pubkey], '#r': [name], limit: 1 }]);
|
const [existing] = await relay.query([{ kinds: [3036], authors: [pubkey], '#r': [name], limit: 1 }]);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return c.json({ error: 'Name request already exists' }, 400);
|
return c.json({ error: 'Name request already exists' }, 400);
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +98,7 @@ export const nameRequestController: AppController = async (c) => {
|
||||||
],
|
],
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], store: await Storages.db() });
|
await hydrateEvents({ events: [event], relay });
|
||||||
|
|
||||||
const nameRequest = await renderNameRequest(event);
|
const nameRequest = await renderNameRequest(event);
|
||||||
return c.json(nameRequest);
|
return c.json(nameRequest);
|
||||||
|
|
@ -114,10 +110,8 @@ const nameRequestsSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const nameRequestsController: AppController = async (c) => {
|
export const nameRequestsController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, user, signal } = c.var;
|
||||||
const store = await Storages.db();
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const signer = c.get('signer')!;
|
|
||||||
const pubkey = await signer.getPublicKey();
|
|
||||||
|
|
||||||
const params = c.get('pagination');
|
const params = c.get('pagination');
|
||||||
const { approved, rejected } = nameRequestsSchema.parse(c.req.query());
|
const { approved, rejected } = nameRequestsSchema.parse(c.req.query());
|
||||||
|
|
@ -137,7 +131,7 @@ export const nameRequestsController: AppController = async (c) => {
|
||||||
filter['#n'] = ['rejected'];
|
filter['#n'] = ['rejected'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const orig = await store.query([filter]);
|
const orig = await relay.query([filter]);
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
|
|
||||||
for (const event of orig) {
|
for (const event of orig) {
|
||||||
|
|
@ -151,8 +145,8 @@ export const nameRequestsController: AppController = async (c) => {
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await store.query([{ kinds: [3036], ids: [...ids], authors: [pubkey] }])
|
const events = await relay.query([{ kinds: [3036], ids: [...ids], authors: [pubkey] }])
|
||||||
.then((events) => hydrateEvents({ store, events: events, signal: c.req.raw.signal }));
|
.then((events) => hydrateEvents({ relay, events: events, signal }));
|
||||||
|
|
||||||
const nameRequests = await Promise.all(
|
const nameRequests = await Promise.all(
|
||||||
events.map((event) => renderNameRequest(event)),
|
events.map((event) => renderNameRequest(event)),
|
||||||
|
|
@ -170,10 +164,9 @@ const zapSplitSchema = z.record(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateZapSplitsController: AppController = async (c) => {
|
export const updateZapSplitsController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay } = c.var;
|
||||||
const body = await parseBody(c.req.raw);
|
const body = await parseBody(c.req.raw);
|
||||||
const result = zapSplitSchema.safeParse(body);
|
const result = zapSplitSchema.safeParse(body);
|
||||||
const store = c.get('store');
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return c.json({ error: result.error }, 400);
|
return c.json({ error: result.error }, 400);
|
||||||
|
|
@ -181,7 +174,7 @@ export const updateZapSplitsController: AppController = async (c) => {
|
||||||
|
|
||||||
const adminPubkey = await conf.signer.getPublicKey();
|
const adminPubkey = await conf.signer.getPublicKey();
|
||||||
|
|
||||||
const dittoZapSplit = await getZapSplits(store, adminPubkey);
|
const dittoZapSplit = await getZapSplits(relay, adminPubkey);
|
||||||
if (!dittoZapSplit) {
|
if (!dittoZapSplit) {
|
||||||
return c.json({ error: 'Zap split not activated, restart the server.' }, 404);
|
return c.json({ error: 'Zap split not activated, restart the server.' }, 404);
|
||||||
}
|
}
|
||||||
|
|
@ -208,10 +201,9 @@ export const updateZapSplitsController: AppController = async (c) => {
|
||||||
const deleteZapSplitSchema = z.array(n.id()).min(1);
|
const deleteZapSplitSchema = z.array(n.id()).min(1);
|
||||||
|
|
||||||
export const deleteZapSplitsController: AppController = async (c) => {
|
export const deleteZapSplitsController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay } = c.var;
|
||||||
const body = await parseBody(c.req.raw);
|
const body = await parseBody(c.req.raw);
|
||||||
const result = deleteZapSplitSchema.safeParse(body);
|
const result = deleteZapSplitSchema.safeParse(body);
|
||||||
const store = c.get('store');
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return c.json({ error: result.error }, 400);
|
return c.json({ error: result.error }, 400);
|
||||||
|
|
@ -219,7 +211,7 @@ export const deleteZapSplitsController: AppController = async (c) => {
|
||||||
|
|
||||||
const adminPubkey = await conf.signer.getPublicKey();
|
const adminPubkey = await conf.signer.getPublicKey();
|
||||||
|
|
||||||
const dittoZapSplit = await getZapSplits(store, adminPubkey);
|
const dittoZapSplit = await getZapSplits(relay, adminPubkey);
|
||||||
if (!dittoZapSplit) {
|
if (!dittoZapSplit) {
|
||||||
return c.json({ error: 'Zap split not activated, restart the server.' }, 404);
|
return c.json({ error: 'Zap split not activated, restart the server.' }, 404);
|
||||||
}
|
}
|
||||||
|
|
@ -239,10 +231,9 @@ export const deleteZapSplitsController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getZapSplitsController: AppController = async (c) => {
|
export const getZapSplitsController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay } = c.var;
|
||||||
const store = c.get('store');
|
|
||||||
|
|
||||||
const dittoZapSplit: DittoZapSplits | undefined = await getZapSplits(store, await conf.signer.getPublicKey()) ?? {};
|
const dittoZapSplit: DittoZapSplits | undefined = await getZapSplits(relay, await conf.signer.getPublicKey()) ?? {};
|
||||||
if (!dittoZapSplit) {
|
if (!dittoZapSplit) {
|
||||||
return c.json({ error: 'Zap split not activated, restart the server.' }, 404);
|
return c.json({ error: 'Zap split not activated, restart the server.' }, 404);
|
||||||
}
|
}
|
||||||
|
|
@ -265,11 +256,11 @@ export const getZapSplitsController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const statusZapSplitsController: AppController = async (c) => {
|
export const statusZapSplitsController: AppController = async (c) => {
|
||||||
const store = c.get('store');
|
const { relay, signal } = c.var;
|
||||||
const id = c.req.param('id');
|
|
||||||
const { signal } = c.req.raw;
|
|
||||||
|
|
||||||
const [event] = await store.query([{ kinds: [1, 20], ids: [id], limit: 1 }], { signal });
|
const id = c.req.param('id');
|
||||||
|
|
||||||
|
const [event] = await relay.query([{ kinds: [1, 20], ids: [id], limit: 1 }], { signal });
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return c.json({ error: 'Event not found' }, 404);
|
return c.json({ error: 'Event not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
@ -278,8 +269,8 @@ export const statusZapSplitsController: AppController = async (c) => {
|
||||||
|
|
||||||
const pubkeys = zapsTag.map((name) => name[1]);
|
const pubkeys = zapsTag.map((name) => name[1]);
|
||||||
|
|
||||||
const users = await store.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, store, signal });
|
await hydrateEvents({ events: users, relay, signal });
|
||||||
|
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ interface Marker {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const markersController: AppController = async (c) => {
|
export const markersController: AppController = async (c) => {
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
const { user } = c.var;
|
||||||
|
|
||||||
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const timelines = c.req.queries('timeline[]') ?? [];
|
const timelines = c.req.queries('timeline[]') ?? [];
|
||||||
|
|
||||||
const results = await kv.getMany<Marker[]>(
|
const results = await kv.getMany<Marker[]>(
|
||||||
|
|
@ -37,7 +39,9 @@ const markerDataSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateMarkersController: AppController = async (c) => {
|
export const updateMarkersController: AppController = async (c) => {
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
const { user } = c.var;
|
||||||
|
|
||||||
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const record = z.record(z.enum(['home', 'notifications']), markerDataSchema).parse(await parseBody(c.req.raw));
|
const record = z.record(z.enum(['home', 'notifications']), markerDataSchema).parse(await parseBody(c.req.raw));
|
||||||
const timelines = Object.keys(record) as Timeline[];
|
const timelines = Object.keys(record) as Timeline[];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,10 @@ const mediaUpdateSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mediaController: AppController = async (c) => {
|
const mediaController: AppController = async (c) => {
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
const { user, signal } = c.var;
|
||||||
|
|
||||||
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const result = mediaBodySchema.safeParse(await parseBody(c.req.raw));
|
const result = mediaBodySchema.safeParse(await parseBody(c.req.raw));
|
||||||
const { signal } = c.req.raw;
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return c.json({ error: 'Bad request.', schema: result.error }, 422);
|
return c.json({ error: 'Bad request.', schema: result.error }, 422);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { getTagSet } from '@/utils/tags.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
import { renderAccounts } from '@/views.ts';
|
import { renderAccounts } from '@/views.ts';
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/mutes/#get */
|
/** https://docs.joinmastodon.org/methods/mutes/#get */
|
||||||
const mutesController: AppController = async (c) => {
|
const mutesController: AppController = async (c) => {
|
||||||
const store = await Storages.db();
|
const { relay, user, signal } = c.var;
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
|
||||||
const { signal } = c.req.raw;
|
|
||||||
|
|
||||||
const [event10000] = await store.query(
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
|
|
||||||
|
const [event10000] = await relay.query(
|
||||||
[{ kinds: [10000], authors: [pubkey], limit: 1 }],
|
[{ kinds: [10000], authors: [pubkey], limit: 1 }],
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,9 @@ const notificationsSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationsController: AppController = async (c) => {
|
const notificationsController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, user } = c.var;
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
|
||||||
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const params = c.get('pagination');
|
const params = c.get('pagination');
|
||||||
|
|
||||||
const types = notificationTypes
|
const types = notificationTypes
|
||||||
|
|
@ -75,20 +76,21 @@ const notificationsController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const notificationController: AppController = async (c) => {
|
const notificationController: AppController = async (c) => {
|
||||||
|
const { relay, user } = c.var;
|
||||||
|
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const store = c.get('store');
|
|
||||||
|
|
||||||
// Remove the timestamp from the ID.
|
// Remove the timestamp from the ID.
|
||||||
const eventId = id.replace(/^\d+-/, '');
|
const eventId = id.replace(/^\d+-/, '');
|
||||||
|
|
||||||
const [event] = await store.query([{ ids: [eventId] }]);
|
const [event] = await relay.query([{ ids: [eventId] }]);
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return c.json({ error: 'Event not found' }, { status: 404 });
|
return c.json({ error: 'Event not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], store });
|
await hydrateEvents({ events: [event], relay });
|
||||||
|
|
||||||
const notification = await renderNotification(event, { viewerPubkey: pubkey });
|
const notification = await renderNotification(event, { viewerPubkey: pubkey });
|
||||||
|
|
||||||
|
|
@ -105,16 +107,15 @@ async function renderNotifications(
|
||||||
params: DittoPagination,
|
params: DittoPagination,
|
||||||
c: AppContext,
|
c: AppContext,
|
||||||
) {
|
) {
|
||||||
const { conf } = c.var;
|
const { conf, relay, user, signal } = c.var;
|
||||||
const store = c.get('store');
|
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const { signal } = c.req.raw;
|
|
||||||
const opts = { signal, limit: params.limit, timeout: conf.db.timeouts.timelines };
|
const opts = { signal, limit: params.limit, timeout: conf.db.timeouts.timelines };
|
||||||
|
|
||||||
const events = await store
|
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, store, signal }));
|
.then((events) => hydrateEvents({ events, relay, signal }));
|
||||||
|
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@ import { z } from 'zod';
|
||||||
|
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts';
|
import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { createAdminEvent, updateAdminEvent, updateUser } from '@/utils/api.ts';
|
import { createAdminEvent, updateAdminEvent, updateUser } from '@/utils/api.ts';
|
||||||
import { lookupPubkey } from '@/utils/lookup.ts';
|
import { lookupPubkey } from '@/utils/lookup.ts';
|
||||||
import { getPleromaConfigs } from '@/utils/pleroma.ts';
|
import { getPleromaConfigs } from '@/utils/pleroma.ts';
|
||||||
|
|
||||||
const frontendConfigController: AppController = async (c) => {
|
const frontendConfigController: AppController = async (c) => {
|
||||||
const store = await Storages.db();
|
const { relay, signal } = c.var;
|
||||||
const configDB = await getPleromaConfigs(store, c.req.raw.signal);
|
|
||||||
|
const configDB = await getPleromaConfigs(relay, signal);
|
||||||
const frontendConfig = configDB.get(':pleroma', ':frontend_configurations');
|
const frontendConfig = configDB.get(':pleroma', ':frontend_configurations');
|
||||||
|
|
||||||
if (frontendConfig) {
|
if (frontendConfig) {
|
||||||
|
|
@ -25,17 +25,17 @@ const frontendConfigController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const configController: AppController = async (c) => {
|
const configController: AppController = async (c) => {
|
||||||
const store = await Storages.db();
|
const { relay, signal } = c.var;
|
||||||
const configs = await getPleromaConfigs(store, c.req.raw.signal);
|
|
||||||
|
const configs = await getPleromaConfigs(relay, signal);
|
||||||
return c.json({ configs, need_reboot: false });
|
return c.json({ configs, need_reboot: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Pleroma admin config controller. */
|
/** Pleroma admin config controller. */
|
||||||
const updateConfigController: AppController = async (c) => {
|
const updateConfigController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, signal } = c.var;
|
||||||
|
|
||||||
const store = await Storages.db();
|
const configs = await getPleromaConfigs(relay, signal);
|
||||||
const configs = await getPleromaConfigs(store, c.req.raw.signal);
|
|
||||||
const { configs: newConfigs } = z.object({ configs: z.array(configSchema) }).parse(await c.req.json());
|
const { configs: newConfigs } = z.object({ configs: z.array(configSchema) }).parse(await c.req.json());
|
||||||
|
|
||||||
configs.merge(newConfigs);
|
configs.merge(newConfigs);
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ const pushSubscribeSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const pushSubscribeController: AppController = async (c) => {
|
export const pushSubscribeController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, user } = c.var;
|
||||||
const vapidPublicKey = await conf.vapidPublicKey;
|
const vapidPublicKey = await conf.vapidPublicKey;
|
||||||
|
|
||||||
if (!vapidPublicKey) {
|
if (!vapidPublicKey) {
|
||||||
|
|
@ -52,7 +52,7 @@ export const pushSubscribeController: AppController = async (c) => {
|
||||||
const accessToken = getAccessToken(c.req.raw);
|
const accessToken = getAccessToken(c.req.raw);
|
||||||
|
|
||||||
const kysely = await Storages.kysely();
|
const kysely = await Storages.kysely();
|
||||||
const signer = c.get('signer')!;
|
const signer = user!.signer;
|
||||||
|
|
||||||
const result = pushSubscribeSchema.safeParse(await parseBody(c.req.raw));
|
const result = pushSubscribeSchema.safeParse(await parseBody(c.req.raw));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { AppController } from '@/app.ts';
|
import { AppController } from '@/app.ts';
|
||||||
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 { Storages } from '@/storages.ts';
|
|
||||||
import { createEvent } from '@/utils/api.ts';
|
import { createEvent } from '@/utils/api.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';
|
||||||
|
|
@ -11,16 +10,15 @@ import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
* https://docs.pleroma.social/backend/development/API/pleroma_api/#put-apiv1pleromastatusesidreactionsemoji
|
* https://docs.pleroma.social/backend/development/API/pleroma_api/#put-apiv1pleromastatusesidreactionsemoji
|
||||||
*/
|
*/
|
||||||
const reactionController: AppController = async (c) => {
|
const reactionController: AppController = async (c) => {
|
||||||
|
const { relay, user } = c.var;
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const emoji = c.req.param('emoji');
|
const emoji = c.req.param('emoji');
|
||||||
const signer = c.get('signer')!;
|
|
||||||
|
|
||||||
if (!/^\p{RGI_Emoji}$/v.test(emoji)) {
|
if (!/^\p{RGI_Emoji}$/v.test(emoji)) {
|
||||||
return c.json({ error: 'Invalid emoji' }, 400);
|
return c.json({ error: 'Invalid emoji' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = await Storages.db();
|
const [event] = await relay.query([{ kinds: [1, 20], ids: [id], limit: 1 }]);
|
||||||
const [event] = await store.query([{ kinds: [1, 20], ids: [id], limit: 1 }]);
|
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return c.json({ error: 'Status not found' }, 404);
|
return c.json({ error: 'Status not found' }, 404);
|
||||||
|
|
@ -33,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], store });
|
await hydrateEvents({ events: [event], relay });
|
||||||
|
|
||||||
const status = await renderStatus(event, { viewerPubkey: await signer.getPublicKey() });
|
const status = await renderStatus(event, { viewerPubkey: await user!.signer.getPublicKey() });
|
||||||
|
|
||||||
return c.json(status);
|
return c.json(status);
|
||||||
};
|
};
|
||||||
|
|
@ -45,17 +43,17 @@ const reactionController: AppController = async (c) => {
|
||||||
* https://docs.pleroma.social/backend/development/API/pleroma_api/#delete-apiv1pleromastatusesidreactionsemoji
|
* https://docs.pleroma.social/backend/development/API/pleroma_api/#delete-apiv1pleromastatusesidreactionsemoji
|
||||||
*/
|
*/
|
||||||
const deleteReactionController: AppController = async (c) => {
|
const deleteReactionController: AppController = async (c) => {
|
||||||
|
const { relay, user } = c.var;
|
||||||
|
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const emoji = c.req.param('emoji');
|
const emoji = c.req.param('emoji');
|
||||||
const signer = c.get('signer')!;
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const pubkey = await signer.getPublicKey();
|
|
||||||
const store = await Storages.db();
|
|
||||||
|
|
||||||
if (!/^\p{RGI_Emoji}$/v.test(emoji)) {
|
if (!/^\p{RGI_Emoji}$/v.test(emoji)) {
|
||||||
return c.json({ error: 'Invalid emoji' }, 400);
|
return c.json({ error: 'Invalid emoji' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [event] = await store.query([
|
const [event] = await relay.query([
|
||||||
{ kinds: [1, 20], ids: [id], limit: 1 },
|
{ kinds: [1, 20], ids: [id], limit: 1 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -63,7 +61,7 @@ const deleteReactionController: AppController = async (c) => {
|
||||||
return c.json({ error: 'Status not found' }, 404);
|
return c.json({ error: 'Status not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await store.query([
|
const events = await relay.query([
|
||||||
{ kinds: [7], authors: [pubkey], '#e': [id] },
|
{ kinds: [7], authors: [pubkey], '#e': [id] },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -88,19 +86,20 @@ const deleteReactionController: AppController = async (c) => {
|
||||||
* https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromastatusesidreactions
|
* https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromastatusesidreactions
|
||||||
*/
|
*/
|
||||||
const reactionsController: AppController = async (c) => {
|
const reactionsController: AppController = async (c) => {
|
||||||
|
const { relay, user } = c.var;
|
||||||
|
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const store = await Storages.db();
|
const pubkey = await user?.signer.getPublicKey();
|
||||||
const pubkey = await c.get('signer')?.getPublicKey();
|
|
||||||
const emoji = c.req.param('emoji') as string | undefined;
|
const emoji = c.req.param('emoji') as string | undefined;
|
||||||
|
|
||||||
if (typeof emoji === 'string' && !/^\p{RGI_Emoji}$/v.test(emoji)) {
|
if (typeof emoji === 'string' && !/^\p{RGI_Emoji}$/v.test(emoji)) {
|
||||||
return c.json({ error: 'Invalid emoji' }, 400);
|
return c.json({ error: 'Invalid emoji' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await store.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, store }));
|
.then((events) => hydrateEvents({ events, relay }));
|
||||||
|
|
||||||
/** Events grouped by emoji. */
|
/** Events grouped by emoji. */
|
||||||
const byEmoji = events.reduce((acc, event) => {
|
const byEmoji = events.reduce((acc, event) => {
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ 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 } = c.var;
|
const { conf, relay } = c.var;
|
||||||
const store = c.get('store');
|
|
||||||
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 +49,7 @@ const reportController: AppController = async (c) => {
|
||||||
tags,
|
tags,
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], store });
|
await hydrateEvents({ events: [event], relay });
|
||||||
return c.json(await renderReport(event));
|
return c.json(await renderReport(event));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -61,18 +61,16 @@ const adminReportsSchema = z.object({
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/admin/reports/#get */
|
/** https://docs.joinmastodon.org/methods/admin/reports/#get */
|
||||||
const adminReportsController: AppController = async (c) => {
|
const adminReportsController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, user, pagination } = c.var;
|
||||||
const store = c.get('store');
|
|
||||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
|
||||||
|
|
||||||
const params = c.get('pagination');
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
const { resolved, account_id, target_account_id } = adminReportsSchema.parse(c.req.query());
|
const { resolved, account_id, target_account_id } = adminReportsSchema.parse(c.req.query());
|
||||||
|
|
||||||
const filter: NostrFilter = {
|
const filter: NostrFilter = {
|
||||||
kinds: [30383],
|
kinds: [30383],
|
||||||
authors: [await conf.signer.getPublicKey()],
|
authors: [await conf.signer.getPublicKey()],
|
||||||
'#k': ['1984'],
|
'#k': ['1984'],
|
||||||
...params,
|
...pagination,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof resolved === 'boolean') {
|
if (typeof resolved === 'boolean') {
|
||||||
|
|
@ -85,7 +83,7 @@ const adminReportsController: AppController = async (c) => {
|
||||||
filter['#P'] = [target_account_id];
|
filter['#P'] = [target_account_id];
|
||||||
}
|
}
|
||||||
|
|
||||||
const orig = await store.query([filter]);
|
const orig = await relay.query([filter]);
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
|
|
||||||
for (const event of orig) {
|
for (const event of orig) {
|
||||||
|
|
@ -95,8 +93,8 @@ const adminReportsController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await store.query([{ kinds: [1984], ids: [...ids] }])
|
const events = await relay.query([{ kinds: [1984], ids: [...ids] }])
|
||||||
.then((events) => hydrateEvents({ store, events: events, signal: c.req.raw.signal }));
|
.then((events) => hydrateEvents({ relay, events: events, signal: c.req.raw.signal }));
|
||||||
|
|
||||||
const reports = await Promise.all(
|
const reports = await Promise.all(
|
||||||
events.map((event) => renderAdminReport(event, { viewerPubkey })),
|
events.map((event) => renderAdminReport(event, { viewerPubkey })),
|
||||||
|
|
@ -107,12 +105,12 @@ const adminReportsController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/admin/reports/#get-one */
|
/** https://docs.joinmastodon.org/methods/admin/reports/#get-one */
|
||||||
const adminReportController: AppController = async (c) => {
|
const adminReportController: AppController = async (c) => {
|
||||||
const eventId = c.req.param('id');
|
const { relay, user, signal } = c.var;
|
||||||
const { signal } = c.req.raw;
|
|
||||||
const store = c.get('store');
|
|
||||||
const pubkey = await c.get('signer')?.getPublicKey();
|
|
||||||
|
|
||||||
const [event] = await store.query([{
|
const eventId = c.req.param('id');
|
||||||
|
const pubkey = await user?.signer.getPublicKey();
|
||||||
|
|
||||||
|
const [event] = await relay.query([{
|
||||||
kinds: [1984],
|
kinds: [1984],
|
||||||
ids: [eventId],
|
ids: [eventId],
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
|
@ -122,7 +120,7 @@ const adminReportController: AppController = async (c) => {
|
||||||
return c.json({ error: 'Not found' }, 404);
|
return c.json({ error: 'Not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], store, signal });
|
await hydrateEvents({ events: [event], relay, signal });
|
||||||
|
|
||||||
const report = await renderAdminReport(event, { viewerPubkey: pubkey });
|
const report = await renderAdminReport(event, { viewerPubkey: pubkey });
|
||||||
return c.json(report);
|
return c.json(report);
|
||||||
|
|
@ -130,12 +128,12 @@ const adminReportController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/admin/reports/#resolve */
|
/** https://docs.joinmastodon.org/methods/admin/reports/#resolve */
|
||||||
const adminReportResolveController: AppController = async (c) => {
|
const adminReportResolveController: AppController = async (c) => {
|
||||||
const eventId = c.req.param('id');
|
const { relay, user, signal } = c.var;
|
||||||
const { signal } = c.req.raw;
|
|
||||||
const store = c.get('store');
|
|
||||||
const pubkey = await c.get('signer')?.getPublicKey();
|
|
||||||
|
|
||||||
const [event] = await store.query([{
|
const eventId = c.req.param('id');
|
||||||
|
const pubkey = await user?.signer.getPublicKey();
|
||||||
|
|
||||||
|
const [event] = await relay.query([{
|
||||||
kinds: [1984],
|
kinds: [1984],
|
||||||
ids: [eventId],
|
ids: [eventId],
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
|
@ -146,19 +144,19 @@ 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], store, signal });
|
await hydrateEvents({ events: [event], relay, signal });
|
||||||
|
|
||||||
const report = await renderAdminReport(event, { viewerPubkey: pubkey });
|
const report = await renderAdminReport(event, { viewerPubkey: pubkey });
|
||||||
return c.json(report);
|
return c.json(report);
|
||||||
};
|
};
|
||||||
|
|
||||||
const adminReportReopenController: AppController = async (c) => {
|
const adminReportReopenController: AppController = async (c) => {
|
||||||
const eventId = c.req.param('id');
|
const { relay, user, signal } = c.var;
|
||||||
const { signal } = c.req.raw;
|
|
||||||
const store = c.get('store');
|
|
||||||
const pubkey = await c.get('signer')?.getPublicKey();
|
|
||||||
|
|
||||||
const [event] = await store.query([{
|
const eventId = c.req.param('id');
|
||||||
|
const pubkey = await user?.signer.getPublicKey();
|
||||||
|
|
||||||
|
const [event] = await relay.query([{
|
||||||
kinds: [1984],
|
kinds: [1984],
|
||||||
ids: [eventId],
|
ids: [eventId],
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
|
@ -169,7 +167,7 @@ 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], store, signal });
|
await hydrateEvents({ events: [event], relay, signal });
|
||||||
|
|
||||||
const report = await renderAdminReport(event, { viewerPubkey: pubkey });
|
const report = await renderAdminReport(event, { viewerPubkey: pubkey });
|
||||||
return c.json(report);
|
return c.json(report);
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,16 @@ 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 result = searchQuerySchema.safeParse(c.req.query());
|
const result = searchQuerySchema.safeParse(c.req.query());
|
||||||
const params = c.get('pagination');
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
const { signal } = c.req.raw;
|
|
||||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
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, ...params }, signal);
|
const event = await lookupEvent({ ...result.data, ...pagination }, signal);
|
||||||
const lookup = extractIdentifier(result.data.q);
|
const lookup = extractIdentifier(result.data.q);
|
||||||
|
|
||||||
// Render account from pubkey.
|
// Render account from pubkey.
|
||||||
|
|
@ -54,7 +54,7 @@ const searchController: AppController = async (c) => {
|
||||||
events = [event];
|
events = [event];
|
||||||
}
|
}
|
||||||
|
|
||||||
events.push(...(await searchEvents({ ...result.data, ...params, viewerPubkey }, signal)));
|
events.push(...(await searchEvents({ ...result.data, ...pagination, viewerPubkey }, signal)));
|
||||||
|
|
||||||
const [accounts, statuses] = await Promise.all([
|
const [accounts, statuses] = await Promise.all([
|
||||||
Promise.all(
|
Promise.all(
|
||||||
|
|
@ -78,7 +78,7 @@ const searchController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result.data.type === 'accounts') {
|
if (result.data.type === 'accounts') {
|
||||||
return paginatedList(c, { ...result.data, ...params }, body);
|
return paginatedList(c, { ...result.data, ...pagination }, body);
|
||||||
} else {
|
} else {
|
||||||
return paginated(c, events, body);
|
return paginated(c, events, body);
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +94,7 @@ async function searchEvents(
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = await Storages.db();
|
const relay = await Storages.db();
|
||||||
|
|
||||||
const filter: NostrFilter = {
|
const filter: NostrFilter = {
|
||||||
kinds: typeToKinds(type),
|
kinds: typeToKinds(type),
|
||||||
|
|
@ -121,9 +121,9 @@ async function searchEvents(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query the events.
|
// Query the events.
|
||||||
let events = await store
|
let events = await relay
|
||||||
.query([filter], { signal })
|
.query([filter], { signal })
|
||||||
.then((events) => hydrateEvents({ events, store, signal }));
|
.then((events) => hydrateEvents({ events, relay, signal }));
|
||||||
|
|
||||||
// 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) {
|
||||||
|
|
@ -150,10 +150,10 @@ 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(query: SearchQuery, signal: AbortSignal): Promise<NostrEvent | undefined> {
|
||||||
const filters = await getLookupFilters(query, signal);
|
const filters = await getLookupFilters(query, signal);
|
||||||
const store = await Storages.db();
|
const relay = await Storages.db();
|
||||||
|
|
||||||
return store.query(filters, { limit: 1, signal })
|
return relay.query(filters, { limit: 1, signal })
|
||||||
.then((events) => hydrateEvents({ events, store, signal }))
|
.then((events) => hydrateEvents({ events, relay, signal }))
|
||||||
.then(([event]) => event);
|
.then(([event]) => event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ import { type AppController } from '@/app.ts';
|
||||||
import { DittoUpload, dittoUploads } from '@/DittoUploads.ts';
|
import { DittoUpload, dittoUploads } from '@/DittoUploads.ts';
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts';
|
import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts';
|
||||||
|
import { paginationSchema } from '@/schemas/pagination.ts';
|
||||||
import { addTag, deleteTag } from '@/utils/tags.ts';
|
import { addTag, deleteTag } from '@/utils/tags.ts';
|
||||||
import { asyncReplaceAll } from '@/utils/text.ts';
|
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 { Storages } from '@/storages.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, paginated, paginatedList, parseBody, updateListEvent } from '@/utils/api.ts';
|
||||||
import { getInvoice, getLnurl } from '@/utils/lnurl.ts';
|
import { getInvoice, getLnurl } from '@/utils/lnurl.ts';
|
||||||
|
|
@ -46,9 +46,9 @@ const createStatusSchema = z.object({
|
||||||
);
|
);
|
||||||
|
|
||||||
const statusController: AppController = async (c) => {
|
const statusController: AppController = async (c) => {
|
||||||
const id = c.req.param('id');
|
const { user, signal } = c.var;
|
||||||
const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(1500)]);
|
|
||||||
|
|
||||||
|
const id = c.req.param('id');
|
||||||
const event = await getEvent(id, { signal });
|
const event = await getEvent(id, { signal });
|
||||||
|
|
||||||
if (event?.author) {
|
if (event?.author) {
|
||||||
|
|
@ -56,7 +56,7 @@ const statusController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
const status = await renderStatus(event, { viewerPubkey });
|
const status = await renderStatus(event, { viewerPubkey });
|
||||||
return c.json(status);
|
return c.json(status);
|
||||||
}
|
}
|
||||||
|
|
@ -65,10 +65,10 @@ const statusController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createStatusController: AppController = async (c) => {
|
const createStatusController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, user, signal } = 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);
|
||||||
const store = c.get('store');
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return c.json({ error: 'Bad request', schema: result.error }, 400);
|
return c.json({ error: 'Bad request', schema: result.error }, 400);
|
||||||
|
|
@ -87,14 +87,14 @@ const createStatusController: AppController = async (c) => {
|
||||||
const tags: string[][] = [];
|
const tags: string[][] = [];
|
||||||
|
|
||||||
if (data.in_reply_to_id) {
|
if (data.in_reply_to_id) {
|
||||||
const [ancestor] = await store.query([{ ids: [data.in_reply_to_id] }]);
|
const [ancestor] = await relay.query([{ ids: [data.in_reply_to_id] }]);
|
||||||
|
|
||||||
if (!ancestor) {
|
if (!ancestor) {
|
||||||
return c.json({ error: 'Original post not found.' }, 404);
|
return c.json({ error: 'Original post not found.' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootId = ancestor.tags.find((tag) => tag[0] === 'e' && tag[3] === 'root')?.[1] ?? ancestor.id;
|
const rootId = ancestor.tags.find((tag) => tag[0] === 'e' && tag[3] === 'root')?.[1] ?? ancestor.id;
|
||||||
const root = rootId === ancestor.id ? ancestor : await store.query([{ ids: [rootId] }]).then(([event]) => event);
|
const root = rootId === ancestor.id ? ancestor : await relay.query([{ ids: [rootId] }]).then(([event]) => event);
|
||||||
|
|
||||||
if (root) {
|
if (root) {
|
||||||
tags.push(['e', root.id, conf.relay, 'root', root.pubkey]);
|
tags.push(['e', root.id, conf.relay, 'root', root.pubkey]);
|
||||||
|
|
@ -108,7 +108,7 @@ const createStatusController: AppController = async (c) => {
|
||||||
let quoted: DittoEvent | undefined;
|
let quoted: DittoEvent | undefined;
|
||||||
|
|
||||||
if (data.quote_id) {
|
if (data.quote_id) {
|
||||||
[quoted] = await store.query([{ ids: [data.quote_id] }]);
|
[quoted] = await relay.query([{ ids: [data.quote_id] }]);
|
||||||
|
|
||||||
if (!quoted) {
|
if (!quoted) {
|
||||||
return c.json({ error: 'Quoted post not found.' }, 404);
|
return c.json({ error: 'Quoted post not found.' }, 404);
|
||||||
|
|
@ -190,13 +190,13 @@ const createStatusController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const author = pubkey ? await getAuthor(pubkey) : undefined;
|
const author = pubkey ? await getAuthor(pubkey) : 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);
|
||||||
const lnurl = getLnurl(meta);
|
const lnurl = getLnurl(meta);
|
||||||
const dittoZapSplit = await getZapSplits(store, await conf.signer.getPublicKey());
|
const dittoZapSplit = await getZapSplits(relay, await conf.signer.getPublicKey());
|
||||||
if (lnurl && dittoZapSplit) {
|
if (lnurl && dittoZapSplit) {
|
||||||
const totalSplit = Object.values(dittoZapSplit).reduce((total, { weight }) => total + weight, 0);
|
const totalSplit = Object.values(dittoZapSplit).reduce((total, { weight }) => total + weight, 0);
|
||||||
for (const zapPubkey in dittoZapSplit) {
|
for (const zapPubkey in dittoZapSplit) {
|
||||||
|
|
@ -256,8 +256,8 @@ const createStatusController: AppController = async (c) => {
|
||||||
if (data.quote_id) {
|
if (data.quote_id) {
|
||||||
await hydrateEvents({
|
await hydrateEvents({
|
||||||
events: [event],
|
events: [event],
|
||||||
store: await Storages.db(),
|
relay,
|
||||||
signal: c.req.raw.signal,
|
signal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,11 +265,11 @@ const createStatusController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteStatusController: AppController = async (c) => {
|
const deleteStatusController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, user, signal } = c.var;
|
||||||
const id = c.req.param('id');
|
|
||||||
const pubkey = await c.get('signer')?.getPublicKey();
|
|
||||||
|
|
||||||
const event = await getEvent(id, { signal: c.req.raw.signal });
|
const id = c.req.param('id');
|
||||||
|
const pubkey = await user?.signer.getPublicKey();
|
||||||
|
const event = await getEvent(id, { signal });
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
if (event.pubkey === pubkey) {
|
if (event.pubkey === pubkey) {
|
||||||
|
|
@ -289,10 +289,11 @@ const deleteStatusController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const contextController: AppController = async (c) => {
|
const contextController: AppController = async (c) => {
|
||||||
|
const { relay, user } = c.var;
|
||||||
|
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const store = c.get('store');
|
const [event] = await relay.query([{ kinds: [1, 20], ids: [id] }]);
|
||||||
const [event] = await store.query([{ kinds: [1, 20], ids: [id] }]);
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
|
||||||
|
|
||||||
async function renderStatuses(events: NostrEvent[]) {
|
async function renderStatuses(events: NostrEvent[]) {
|
||||||
const statuses = await Promise.all(
|
const statuses = await Promise.all(
|
||||||
|
|
@ -303,14 +304,14 @@ const contextController: AppController = async (c) => {
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
const [ancestorEvents, descendantEvents] = await Promise.all([
|
const [ancestorEvents, descendantEvents] = await Promise.all([
|
||||||
getAncestors(store, event),
|
getAncestors(relay, event),
|
||||||
getDescendants(store, event),
|
getDescendants(relay, event),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({
|
||||||
events: [...ancestorEvents, ...descendantEvents],
|
events: [...ancestorEvents, ...descendantEvents],
|
||||||
signal: c.req.raw.signal,
|
signal: c.req.raw.signal,
|
||||||
store,
|
relay,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [ancestors, descendants] = await Promise.all([
|
const [ancestors, descendants] = await Promise.all([
|
||||||
|
|
@ -325,10 +326,10 @@ const contextController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const favouriteController: AppController = async (c) => {
|
const favouriteController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, user } = c.var;
|
||||||
|
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const store = await Storages.db();
|
const [target] = await relay.query([{ ids: [id], kinds: [1, 20] }]);
|
||||||
const [target] = await store.query([{ ids: [id], kinds: [1, 20] }]);
|
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
await createEvent({
|
await createEvent({
|
||||||
|
|
@ -340,9 +341,9 @@ const favouriteController: AppController = async (c) => {
|
||||||
],
|
],
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
await hydrateEvents({ events: [target], store });
|
await hydrateEvents({ events: [target], relay });
|
||||||
|
|
||||||
const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() });
|
const status = await renderStatus(target, { viewerPubkey: await user?.signer.getPublicKey() });
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
status.favourited = true;
|
status.favourited = true;
|
||||||
|
|
@ -366,13 +367,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 } = c.var;
|
const { conf, relay, user, signal } = c.var;
|
||||||
const eventId = c.req.param('id');
|
|
||||||
const { signal } = c.req.raw;
|
|
||||||
|
|
||||||
const event = await getEvent(eventId, {
|
const eventId = c.req.param('id');
|
||||||
kind: 1,
|
const event = await getEvent(eventId);
|
||||||
});
|
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return c.json({ error: 'Event not found.' }, 404);
|
return c.json({ error: 'Event not found.' }, 404);
|
||||||
|
|
@ -388,28 +386,28 @@ const reblogStatusController: AppController = async (c) => {
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({
|
||||||
events: [reblogEvent],
|
events: [reblogEvent],
|
||||||
store: await Storages.db(),
|
relay,
|
||||||
signal: signal,
|
signal: signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
const status = await renderReblog(reblogEvent, { viewerPubkey: await c.get('signer')?.getPublicKey() });
|
const status = await renderReblog(reblogEvent, { viewerPubkey: await user?.signer.getPublicKey() });
|
||||||
|
|
||||||
return c.json(status);
|
return c.json(status);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/statuses/#unreblog */
|
/** https://docs.joinmastodon.org/methods/statuses/#unreblog */
|
||||||
const unreblogStatusController: AppController = async (c) => {
|
const unreblogStatusController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, user } = c.var;
|
||||||
const eventId = c.req.param('id');
|
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
|
||||||
const store = await Storages.db();
|
|
||||||
|
|
||||||
const [event] = await store.query([{ ids: [eventId], kinds: [1, 20] }]);
|
const eventId = c.req.param('id');
|
||||||
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
|
|
||||||
|
const [event] = await relay.query([{ ids: [eventId], kinds: [1, 20] }]);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return c.json({ error: 'Record not found' }, 404);
|
return c.json({ error: 'Record not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [repostEvent] = await store.query(
|
const [repostEvent] = await relay.query(
|
||||||
[{ kinds: [6], authors: [pubkey], '#e': [event.id], limit: 1 }],
|
[{ kinds: [6], authors: [pubkey], '#e': [event.id], limit: 1 }],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -432,20 +430,20 @@ const rebloggedByController: AppController = (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const quotesController: AppController = async (c) => {
|
const quotesController: AppController = async (c) => {
|
||||||
const id = c.req.param('id');
|
const { relay, user, pagination } = c.var;
|
||||||
const params = c.get('pagination');
|
|
||||||
const store = await Storages.db();
|
|
||||||
|
|
||||||
const [event] = await store.query([{ ids: [id], kinds: [1, 20] }]);
|
const id = c.req.param('id');
|
||||||
|
|
||||||
|
const [event] = await relay.query([{ ids: [id], kinds: [1, 20] }]);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return c.json({ error: 'Event not found.' }, 404);
|
return c.json({ error: 'Event not found.' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const quotes = await store
|
const quotes = await relay
|
||||||
.query([{ kinds: [1, 20], '#q': [event.id], ...params }])
|
.query([{ kinds: [1, 20], '#q': [event.id], ...pagination }])
|
||||||
.then((events) => hydrateEvents({ events, store }));
|
.then((events) => hydrateEvents({ events, relay }));
|
||||||
|
|
||||||
const viewerPubkey = await c.get('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(event, { viewerPubkey })),
|
||||||
|
|
@ -460,14 +458,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 } = c.var;
|
const { conf, user } = c.var;
|
||||||
const pubkey = await c.get('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);
|
||||||
kind: 1,
|
|
||||||
relations: ['author', 'event_stats', 'author_stats'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -488,14 +483,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 } = c.var;
|
const { conf, user } = c.var;
|
||||||
const pubkey = await c.get('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);
|
||||||
kind: 1,
|
|
||||||
relations: ['author', 'event_stats', 'author_stats'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -516,14 +509,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 } = c.var;
|
const { conf, user } = c.var;
|
||||||
const pubkey = await c.get('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);
|
||||||
kind: 1,
|
|
||||||
relations: ['author', 'event_stats', 'author_stats'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -544,14 +535,13 @@ 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 } = c.var;
|
const { conf, user, signal } = c.var;
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
|
||||||
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
const { signal } = c.req.raw;
|
|
||||||
|
|
||||||
const event = await getEvent(eventId, {
|
const event = await getEvent(eventId, {
|
||||||
kind: 1,
|
kind: 1,
|
||||||
relations: ['author', 'event_stats', 'author_stats'],
|
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -580,11 +570,10 @@ const zapSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const zapController: AppController = async (c) => {
|
const zapController: 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 = zapSchema.safeParse(body);
|
const result = zapSchema.safeParse(body);
|
||||||
const { signal } = c.req.raw;
|
|
||||||
const store = c.get('store');
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return c.json({ error: 'Bad request', schema: result.error }, 400);
|
return c.json({ error: 'Bad request', schema: result.error }, 400);
|
||||||
|
|
@ -611,7 +600,7 @@ const zapController: AppController = async (c) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
[target] = await store.query([{ authors: [account_id], kinds: [0], limit: 1 }]);
|
[target] = await relay.query([{ authors: [account_id], kinds: [0], limit: 1 }]);
|
||||||
const meta = n.json().pipe(n.metadata()).catch({}).parse(target?.content);
|
const meta = n.json().pipe(n.metadata()).catch({}).parse(target?.content);
|
||||||
lnurl = getLnurl(meta);
|
lnurl = getLnurl(meta);
|
||||||
if (target && lnurl) {
|
if (target && lnurl) {
|
||||||
|
|
@ -638,19 +627,19 @@ const zapController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const zappedByController: AppController = async (c) => {
|
const zappedByController: AppController = async (c) => {
|
||||||
const id = c.req.param('id');
|
const { db, relay } = c.var;
|
||||||
const params = c.get('listPagination');
|
|
||||||
const store = await Storages.db();
|
|
||||||
const kysely = await Storages.kysely();
|
|
||||||
|
|
||||||
const zaps = await kysely.selectFrom('event_zaps')
|
const id = c.req.param('id');
|
||||||
|
const { offset, limit } = paginationSchema.parse(c.req.query());
|
||||||
|
|
||||||
|
const zaps = await db.kysely.selectFrom('event_zaps')
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where('target_event_id', '=', id)
|
.where('target_event_id', '=', id)
|
||||||
.orderBy('amount_millisats', 'desc')
|
.orderBy('amount_millisats', 'desc')
|
||||||
.limit(params.limit)
|
.limit(limit)
|
||||||
.offset(params.offset).execute();
|
.offset(offset).execute();
|
||||||
|
|
||||||
const authors = await store.query([{ kinds: [0], authors: zaps.map((zap) => zap.sender_pubkey) }]);
|
const authors = await relay.query([{ kinds: [0], authors: zaps.map((zap) => zap.sender_pubkey) }]);
|
||||||
|
|
||||||
const results = (await Promise.all(
|
const results = (await Promise.all(
|
||||||
zaps.map(async (zap) => {
|
zaps.map(async (zap) => {
|
||||||
|
|
@ -668,7 +657,7 @@ const zappedByController: AppController = async (c) => {
|
||||||
}),
|
}),
|
||||||
)).filter(Boolean);
|
)).filter(Boolean);
|
||||||
|
|
||||||
return paginatedList(c, params, results);
|
return paginatedList(c, { limit, offset }, results);
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ const limiter = new TTLCache<string, number>();
|
||||||
const connections = new Set<WebSocket>();
|
const connections = new Set<WebSocket>();
|
||||||
|
|
||||||
const streamingController: AppController = async (c) => {
|
const streamingController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay } = c.var;
|
||||||
const upgrade = c.req.header('upgrade');
|
const upgrade = c.req.header('upgrade');
|
||||||
const token = c.req.header('sec-websocket-protocol');
|
const token = c.req.header('sec-websocket-protocol');
|
||||||
const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream'));
|
const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream'));
|
||||||
|
|
@ -93,7 +93,6 @@ const streamingController: AppController = async (c) => {
|
||||||
|
|
||||||
const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { protocol: token, idleTimeout: 30 });
|
const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { protocol: token, idleTimeout: 30 });
|
||||||
|
|
||||||
const store = await Storages.db();
|
|
||||||
const policy = pubkey ? new MuteListPolicy(pubkey, await Storages.admin()) : undefined;
|
const policy = pubkey ? new MuteListPolicy(pubkey, await Storages.admin()) : undefined;
|
||||||
|
|
||||||
function send(e: StreamingEvent) {
|
function send(e: StreamingEvent) {
|
||||||
|
|
@ -108,7 +107,7 @@ const streamingController: AppController = async (c) => {
|
||||||
render: (event: NostrEvent) => Promise<StreamingEvent | undefined>,
|
render: (event: NostrEvent) => Promise<StreamingEvent | undefined>,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
for await (const msg of store.req([filter], { signal: controller.signal })) {
|
for await (const msg of relay.req([filter], { signal: controller.signal })) {
|
||||||
if (msg[0] === 'EVENT') {
|
if (msg[0] === 'EVENT') {
|
||||||
const event = msg[2];
|
const event = msg[2];
|
||||||
|
|
||||||
|
|
@ -119,7 +118,7 @@ const streamingController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], store, signal: AbortSignal.timeout(1000) });
|
await hydrateEvents({ events: [event], relay, signal: AbortSignal.timeout(1000) });
|
||||||
|
|
||||||
const result = await render(event);
|
const result = await render(event);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,33 +2,32 @@ 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 { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { paginated, paginatedList } from '@/utils/api.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';
|
||||||
|
|
||||||
export const suggestionsV1Controller: AppController = async (c) => {
|
export const suggestionsV1Controller: AppController = async (c) => {
|
||||||
const signal = c.req.raw.signal;
|
const { signal } = c.var;
|
||||||
const params = c.get('listPagination');
|
const { offset, limit } = paginationSchema.parse(c.req.query());
|
||||||
const suggestions = await renderV2Suggestions(c, params, signal);
|
const suggestions = await renderV2Suggestions(c, { offset, limit }, signal);
|
||||||
const accounts = suggestions.map(({ account }) => account);
|
const accounts = suggestions.map(({ account }) => account);
|
||||||
return paginatedList(c, params, accounts);
|
return paginatedList(c, { offset, limit }, accounts);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const suggestionsV2Controller: AppController = async (c) => {
|
export const suggestionsV2Controller: AppController = async (c) => {
|
||||||
const signal = c.req.raw.signal;
|
const { signal } = c.var;
|
||||||
const params = c.get('listPagination');
|
const { offset, limit } = paginationSchema.parse(c.req.query());
|
||||||
const suggestions = await renderV2Suggestions(c, params, signal);
|
const suggestions = await renderV2Suggestions(c, { offset, limit }, signal);
|
||||||
return paginatedList(c, params, suggestions);
|
return paginatedList(c, { offset, limit }, suggestions);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function renderV2Suggestions(c: AppContext, params: { offset: number; limit: number }, signal?: AbortSignal) {
|
async function renderV2Suggestions(c: AppContext, params: { offset: number; limit: number }, signal?: AbortSignal) {
|
||||||
const { conf } = c.var;
|
const { conf, relay, user } = c.var;
|
||||||
const { offset, limit } = params;
|
const { offset, limit } = params;
|
||||||
|
|
||||||
const store = c.get('store');
|
const pubkey = await user?.signer.getPublicKey();
|
||||||
const signer = c.get('signer');
|
|
||||||
const pubkey = await signer?.getPublicKey();
|
|
||||||
|
|
||||||
const filters: NostrFilter[] = [
|
const filters: NostrFilter[] = [
|
||||||
{ kinds: [30382], authors: [await conf.signer.getPublicKey()], '#n': ['suggested'], limit },
|
{ kinds: [30382], authors: [await conf.signer.getPublicKey()], '#n': ['suggested'], limit },
|
||||||
|
|
@ -40,7 +39,7 @@ async function renderV2Suggestions(c: AppContext, params: { offset: number; limi
|
||||||
filters.push({ kinds: [10000], authors: [pubkey], limit: 1 });
|
filters.push({ kinds: [10000], authors: [pubkey], limit: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await store.query(filters, { signal });
|
const events = await relay.query(filters, { signal });
|
||||||
const adminPubkey = await conf.signer.getPublicKey();
|
const adminPubkey = await conf.signer.getPublicKey();
|
||||||
|
|
||||||
const [userEvents, followsEvent, mutesEvent, trendingEvent] = [
|
const [userEvents, followsEvent, mutesEvent, trendingEvent] = [
|
||||||
|
|
@ -79,11 +78,11 @@ async function renderV2Suggestions(c: AppContext, params: { offset: number; limi
|
||||||
|
|
||||||
const authors = [...pubkeys].slice(offset, offset + limit);
|
const authors = [...pubkeys].slice(offset, offset + limit);
|
||||||
|
|
||||||
const profiles = await store.query(
|
const profiles = await relay.query(
|
||||||
[{ kinds: [0], authors, limit: authors.length }],
|
[{ kinds: [0], authors, limit: authors.length }],
|
||||||
{ signal },
|
{ signal },
|
||||||
)
|
)
|
||||||
.then((events) => hydrateEvents({ events, store, signal }));
|
.then((events) => hydrateEvents({ events, relay, signal }));
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -96,13 +95,10 @@ async function renderV2Suggestions(c: AppContext, params: { offset: number; limi
|
||||||
}
|
}
|
||||||
|
|
||||||
export const localSuggestionsController: AppController = async (c) => {
|
export const localSuggestionsController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, pagination, signal } = c.var;
|
||||||
const signal = c.req.raw.signal;
|
|
||||||
const params = c.get('pagination');
|
|
||||||
const store = c.get('store');
|
|
||||||
|
|
||||||
const grants = await store.query(
|
const grants = await relay.query(
|
||||||
[{ kinds: [30360], authors: [await conf.signer.getPublicKey()], ...params }],
|
[{ kinds: [30360], authors: [await conf.signer.getPublicKey()], ...pagination }],
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -115,11 +111,11 @@ export const localSuggestionsController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const profiles = await store.query(
|
const profiles = await relay.query(
|
||||||
[{ kinds: [0], authors: [...pubkeys], search: `domain:${conf.url.host}`, ...params }],
|
[{ kinds: [0], authors: [...pubkeys], search: `domain:${conf.url.host}`, ...pagination }],
|
||||||
{ signal },
|
{ signal },
|
||||||
)
|
)
|
||||||
.then((events) => hydrateEvents({ store, events, signal }));
|
.then((events) => hydrateEvents({ relay, events, signal }));
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ const homeQuerySchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const homeTimelineController: AppController = async (c) => {
|
const homeTimelineController: AppController = async (c) => {
|
||||||
const params = c.get('pagination');
|
const { user, pagination } = c.var;
|
||||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
const pubkey = await user?.signer.getPublicKey()!;
|
||||||
const result = homeQuerySchema.safeParse(c.req.query());
|
const result = homeQuerySchema.safeParse(c.req.query());
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|
@ -26,7 +26,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(pubkey)];
|
||||||
const filter: NostrFilter = { authors, kinds: [1, 6, 20], ...params };
|
const filter: NostrFilter = { authors, kinds: [1, 6, 20], ...pagination };
|
||||||
|
|
||||||
const search: string[] = [];
|
const search: string[] = [];
|
||||||
|
|
||||||
|
|
@ -90,35 +90,32 @@ const hashtagTimelineController: AppController = (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const suggestedTimelineController: AppController = async (c) => {
|
const suggestedTimelineController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, pagination } = c.var;
|
||||||
const store = c.get('store');
|
|
||||||
const params = c.get('pagination');
|
|
||||||
|
|
||||||
const [follows] = await store.query(
|
const [follows] = await relay.query(
|
||||||
[{ kinds: [3], authors: [await conf.signer.getPublicKey()], limit: 1 }],
|
[{ kinds: [3], authors: [await conf.signer.getPublicKey()], limit: 1 }],
|
||||||
);
|
);
|
||||||
|
|
||||||
const authors = [...getTagSet(follows?.tags ?? [], 'p')];
|
const authors = [...getTagSet(follows?.tags ?? [], 'p')];
|
||||||
|
|
||||||
return renderStatuses(c, [{ authors, kinds: [1, 20], ...params }]);
|
return renderStatuses(c, [{ authors, kinds: [1, 20], ...pagination }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Render statuses for timelines. */
|
/** Render statuses for timelines. */
|
||||||
async function renderStatuses(c: AppContext, filters: NostrFilter[]) {
|
async function renderStatuses(c: AppContext, filters: NostrFilter[]) {
|
||||||
const { conf } = c.var;
|
const { conf, relay, user, signal } = c.var;
|
||||||
const { signal } = c.req.raw;
|
|
||||||
const store = c.get('store');
|
|
||||||
const opts = { signal, timeout: conf.db.timeouts.timelines };
|
const opts = { signal, timeout: conf.db.timeouts.timelines };
|
||||||
|
|
||||||
const events = await store
|
const events = await relay
|
||||||
.query(filters, opts)
|
.query(filters, opts)
|
||||||
.then((events) => hydrateEvents({ events, store, signal }));
|
.then((events) => hydrateEvents({ events, relay, signal }));
|
||||||
|
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
|
|
||||||
const statuses = (await Promise.all(events.map((event) => {
|
const statuses = (await Promise.all(events.map((event) => {
|
||||||
if (event.kind === 6) {
|
if (event.kind === 6) {
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ const translateSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const translateController: AppController = async (c) => {
|
const translateController: AppController = async (c) => {
|
||||||
|
const { user, signal } = c.var;
|
||||||
|
|
||||||
const result = translateSchema.safeParse(await parseBody(c.req.raw));
|
const result = translateSchema.safeParse(await parseBody(c.req.raw));
|
||||||
const { signal } = c.req.raw;
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return c.json({ error: 'Bad request.', schema: result.error }, 422);
|
return c.json({ error: 'Bad request.', schema: result.error }, 422);
|
||||||
|
|
@ -38,7 +39,7 @@ const translateController: AppController = async (c) => {
|
||||||
return c.json({ error: 'Record not found' }, 400);
|
return c.json({ error: 'Record not found' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
const viewerPubkey = await user?.signer.getPublicKey();
|
||||||
|
|
||||||
if (lang.toLowerCase() === event?.language?.toLowerCase()) {
|
if (lang.toLowerCase() === event?.language?.toLowerCase()) {
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ const trendingTagsController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getTrendingHashtags(conf: DittoConf) {
|
async function getTrendingHashtags(conf: DittoConf) {
|
||||||
const store = await Storages.db();
|
const relay = await Storages.db();
|
||||||
const trends = await getTrendingTags(store, 't', await conf.signer.getPublicKey());
|
const trends = await getTrendingTags(relay, 't', await conf.signer.getPublicKey());
|
||||||
|
|
||||||
return trends.map((trend) => {
|
return trends.map((trend) => {
|
||||||
const hashtag = trend.value;
|
const hashtag = trend.value;
|
||||||
|
|
@ -105,8 +105,8 @@ const trendingLinksController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getTrendingLinks(conf: DittoConf) {
|
async function getTrendingLinks(conf: DittoConf) {
|
||||||
const store = await Storages.db();
|
const relay = await Storages.db();
|
||||||
const trends = await getTrendingTags(store, '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) => {
|
||||||
const link = trend.value;
|
const link = trend.value;
|
||||||
|
|
@ -140,11 +140,10 @@ async function getTrendingLinks(conf: DittoConf) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const trendingStatusesController: AppController = async (c) => {
|
const trendingStatusesController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay } = c.var;
|
||||||
const store = await Storages.db();
|
|
||||||
const { limit, offset, until } = paginationSchema.parse(c.req.query());
|
const { limit, offset, until } = paginationSchema.parse(c.req.query());
|
||||||
|
|
||||||
const [label] = await store.query([{
|
const [label] = await relay.query([{
|
||||||
kinds: [1985],
|
kinds: [1985],
|
||||||
'#L': ['pub.ditto.trends'],
|
'#L': ['pub.ditto.trends'],
|
||||||
'#l': ['#e'],
|
'#l': ['#e'],
|
||||||
|
|
@ -162,8 +161,8 @@ const trendingStatusesController: AppController = async (c) => {
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await store.query([{ kinds: [1, 20], ids }])
|
const results = await relay.query([{ kinds: [1, 20], ids }])
|
||||||
.then((events) => hydrateEvents({ events, store }));
|
.then((events) => hydrateEvents({ events, relay }));
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
|
|
||||||
import { AppMiddleware } from '@/app.ts';
|
import { AppMiddleware } from '@/app.ts';
|
||||||
import { Storages } from '@/storages.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';
|
||||||
|
|
@ -10,11 +9,14 @@ 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 {
|
||||||
|
|
@ -23,7 +25,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(params ?? {});
|
const entities = await getEntities(relay, 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) {
|
||||||
|
|
@ -37,11 +39,9 @@ export const frontendController: AppMiddleware = async (c) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getEntities(params: { acct?: string; statusId?: string }): Promise<MetadataEntities> {
|
async function getEntities(relay: NStore, params: { acct?: string; statusId?: string }): Promise<MetadataEntities> {
|
||||||
const store = await Storages.db();
|
|
||||||
|
|
||||||
const entities: MetadataEntities = {
|
const entities: MetadataEntities = {
|
||||||
instance: await getInstanceMetadata(store),
|
instance: await getInstanceMetadata(relay),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (params.statusId) {
|
if (params.statusId) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { AppController } from '@/app.ts';
|
import { AppController } from '@/app.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { WebManifestCombined } from '@/types/webmanifest.ts';
|
import { WebManifestCombined } from '@/types/webmanifest.ts';
|
||||||
import { getInstanceMetadata } from '@/utils/instance.ts';
|
import { getInstanceMetadata } from '@/utils/instance.ts';
|
||||||
|
|
||||||
export const manifestController: AppController = async (c) => {
|
export const manifestController: AppController = async (c) => {
|
||||||
const meta = await getInstanceMetadata(await Storages.db(), c.req.raw.signal);
|
const { relay, signal } = c.var;
|
||||||
|
|
||||||
|
const meta = await getInstanceMetadata(relay, signal);
|
||||||
|
|
||||||
const manifest: WebManifestCombined = {
|
const manifest: WebManifestCombined = {
|
||||||
description: meta.about,
|
description: meta.about,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
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 relayInfoController: AppController = async (c) => {
|
const relayInfoController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, signal } = c.var;
|
||||||
const store = await Storages.db();
|
|
||||||
const meta = await getInstanceMetadata(store, c.req.raw.signal);
|
const meta = await getInstanceMetadata(relay, signal);
|
||||||
|
|
||||||
c.res.headers.set('access-control-allow-origin', '*');
|
c.res.headers.set('access-control-allow-origin', '*');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ 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 * as pipeline from '@/pipeline.ts';
|
||||||
import { RelayError } from '@/RelayError.ts';
|
import { RelayError } from '@/RelayError.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { type DittoPgStore } from '@/storages/DittoPgStore.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 { Time } from '@/utils/time.ts';
|
import { Time } from '@/utils/time.ts';
|
||||||
|
|
@ -42,7 +42,7 @@ const limiters = {
|
||||||
const connections = new Set<WebSocket>();
|
const connections = new Set<WebSocket>();
|
||||||
|
|
||||||
/** Set up the Websocket connection. */
|
/** Set up the Websocket connection. */
|
||||||
function connectStream(socket: WebSocket, ip: string | undefined, conf: DittoConf) {
|
function connectStream(conf: DittoConf, relay: DittoPgStore, socket: WebSocket, ip: string | undefined) {
|
||||||
const controllers = new Map<string, AbortController>();
|
const controllers = new Map<string, AbortController>();
|
||||||
|
|
||||||
if (ip) {
|
if (ip) {
|
||||||
|
|
@ -133,10 +133,8 @@ function connectStream(socket: WebSocket, ip: string | undefined, conf: DittoCon
|
||||||
controllers.get(subId)?.abort();
|
controllers.get(subId)?.abort();
|
||||||
controllers.set(subId, controller);
|
controllers.set(subId, controller);
|
||||||
|
|
||||||
const store = await Storages.db();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const [verb, , ...rest] of store.req(filters, { limit: 100, timeout: conf.db.timeouts.relay })) {
|
for await (const [verb, , ...rest] of relay.req(filters, { limit: 100, timeout: conf.db.timeouts.relay })) {
|
||||||
send([verb, subId, ...rest] as NostrRelayMsg);
|
send([verb, subId, ...rest] as NostrRelayMsg);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -185,8 +183,7 @@ function connectStream(socket: WebSocket, ip: string | undefined, conf: DittoCon
|
||||||
/** Handle COUNT. Return the number of events matching the filters. */
|
/** Handle COUNT. Return the number of events matching the filters. */
|
||||||
async function handleCount([_, subId, ...filters]: NostrClientCOUNT): Promise<void> {
|
async function handleCount([_, subId, ...filters]: NostrClientCOUNT): Promise<void> {
|
||||||
if (rateLimited(limiters.req)) return;
|
if (rateLimited(limiters.req)) return;
|
||||||
const store = await Storages.db();
|
const { count } = await relay.count(filters, { timeout: conf.db.timeouts.relay });
|
||||||
const { count } = await store.count(filters, { timeout: conf.db.timeouts.relay });
|
|
||||||
send(['COUNT', subId, { count, approximate: false }]);
|
send(['COUNT', subId, { count, approximate: false }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,7 +196,7 @@ function connectStream(socket: WebSocket, ip: string | undefined, conf: DittoCon
|
||||||
}
|
}
|
||||||
|
|
||||||
const relayController: AppController = (c, next) => {
|
const relayController: AppController = (c, next) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay } = c.var;
|
||||||
const upgrade = c.req.header('upgrade');
|
const upgrade = c.req.header('upgrade');
|
||||||
|
|
||||||
// NIP-11: https://github.com/nostr-protocol/nips/blob/master/11.md
|
// NIP-11: https://github.com/nostr-protocol/nips/blob/master/11.md
|
||||||
|
|
@ -218,7 +215,7 @@ const relayController: AppController = (c, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { idleTimeout: 30 });
|
const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { idleTimeout: 30 });
|
||||||
connectStream(socket, ip, conf);
|
connectStream(conf, relay as DittoPgStore, socket, ip);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,17 +12,17 @@ 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');
|
||||||
return c.json(emptyResult);
|
return c.json(emptyResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = c.get('store');
|
|
||||||
|
|
||||||
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(store, name) : undefined;
|
const pointer = name ? await localNip05Lookup(relay, name) : undefined;
|
||||||
|
|
||||||
if (!name || !pointer) {
|
if (!name || !pointer) {
|
||||||
// Not found, cache for 5 minutes.
|
// Not found, cache for 5 minutes.
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,13 @@ function auth98Middleware(opts: ParseAuthRequestOpts = {}): AppMiddleware {
|
||||||
const result = await parseAuthRequest(req, opts);
|
const result = await parseAuthRequest(req, opts);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
c.set('signer', new ReadOnlySigner(result.data.pubkey));
|
const user = {
|
||||||
c.set('proof', result.data);
|
relay: c.var.relay,
|
||||||
|
signer: new ReadOnlySigner(result.data.pubkey),
|
||||||
|
...c.var.user,
|
||||||
|
};
|
||||||
|
|
||||||
|
c.set('user', user);
|
||||||
}
|
}
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
|
|
@ -71,7 +76,7 @@ function withProof(
|
||||||
opts?: ParseAuthRequestOpts,
|
opts?: ParseAuthRequestOpts,
|
||||||
): AppMiddleware {
|
): AppMiddleware {
|
||||||
return async (c, next) => {
|
return async (c, next) => {
|
||||||
const signer = c.get('signer');
|
const signer = c.var.user?.signer;
|
||||||
const pubkey = await signer?.getPublicKey();
|
const pubkey = await signer?.getPublicKey();
|
||||||
const proof = c.get('proof') || await obtainProof(c, opts);
|
const proof = c.get('proof') || await obtainProof(c, opts);
|
||||||
|
|
||||||
|
|
@ -84,7 +89,13 @@ function withProof(
|
||||||
c.set('proof', proof);
|
c.set('proof', proof);
|
||||||
|
|
||||||
if (!signer) {
|
if (!signer) {
|
||||||
c.set('signer', new ReadOnlySigner(proof.pubkey));
|
const user = {
|
||||||
|
relay: c.var.relay,
|
||||||
|
signer: new ReadOnlySigner(proof.pubkey),
|
||||||
|
...c.var.user,
|
||||||
|
};
|
||||||
|
|
||||||
|
c.set('user', user);
|
||||||
}
|
}
|
||||||
|
|
||||||
await handler(c, proof, next);
|
await handler(c, proof, next);
|
||||||
|
|
@ -96,7 +107,7 @@ function withProof(
|
||||||
|
|
||||||
/** Get the proof over Nostr Connect. */
|
/** Get the proof over Nostr Connect. */
|
||||||
async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) {
|
async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) {
|
||||||
const signer = c.get('signer');
|
const signer = c.var.user?.signer;
|
||||||
if (!signer) {
|
if (!signer) {
|
||||||
throw new HTTPException(401, {
|
throw new HTTPException(401, {
|
||||||
res: c.json({ error: 'No way to sign Nostr event' }, 401),
|
res: c.json({ error: 'No way to sign Nostr event' }, 401),
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import { AppMiddleware } from '@/app.ts';
|
|
||||||
import { paginationSchema } from '@/schemas/pagination.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
|
|
||||||
/** Fixes compatibility with Mastodon apps by that don't use `Link` headers. */
|
|
||||||
export const paginationMiddleware: AppMiddleware = async (c, next) => {
|
|
||||||
const pagination = paginationSchema.parse(c.req.query());
|
|
||||||
|
|
||||||
const {
|
|
||||||
max_id: maxId,
|
|
||||||
min_id: minId,
|
|
||||||
since,
|
|
||||||
until,
|
|
||||||
} = pagination;
|
|
||||||
|
|
||||||
if ((maxId && !until) || (minId && !since)) {
|
|
||||||
const ids: string[] = [];
|
|
||||||
|
|
||||||
if (maxId) ids.push(maxId);
|
|
||||||
if (minId) ids.push(minId);
|
|
||||||
|
|
||||||
if (ids.length) {
|
|
||||||
const store = await Storages.db();
|
|
||||||
|
|
||||||
const events = await store.query(
|
|
||||||
[{ ids, limit: ids.length }],
|
|
||||||
{ signal: c.req.raw.signal },
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const event of events) {
|
|
||||||
if (!until && maxId === event.id) pagination.until = event.created_at;
|
|
||||||
if (!since && minId === event.id) pagination.since = event.created_at;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.set('pagination', {
|
|
||||||
since: pagination.since,
|
|
||||||
until: pagination.until,
|
|
||||||
limit: pagination.limit,
|
|
||||||
});
|
|
||||||
|
|
||||||
c.set('listPagination', {
|
|
||||||
limit: pagination.limit,
|
|
||||||
offset: pagination.offset,
|
|
||||||
});
|
|
||||||
|
|
||||||
await next();
|
|
||||||
};
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { MiddlewareHandler } from '@hono/hono';
|
|
||||||
import { HTTPException } from '@hono/hono/http-exception';
|
|
||||||
import { NostrSigner } from '@nostrify/nostrify';
|
|
||||||
import { SetRequired } from 'type-fest';
|
|
||||||
|
|
||||||
/** Throw a 401 if a signer isn't set. */
|
|
||||||
export const requireSigner: MiddlewareHandler<{ Variables: { signer: NostrSigner } }> = async (c, next) => {
|
|
||||||
if (!c.get('signer')) {
|
|
||||||
throw new HTTPException(401, { message: 'No pubkey provided' });
|
|
||||||
}
|
|
||||||
|
|
||||||
await next();
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Throw a 401 if a NIP-44 signer isn't set. */
|
|
||||||
export const requireNip44Signer: MiddlewareHandler<{ Variables: { signer: SetRequired<NostrSigner, 'nip44'> } }> =
|
|
||||||
async (c, next) => {
|
|
||||||
const signer = c.get('signer');
|
|
||||||
|
|
||||||
if (!signer) {
|
|
||||||
throw new HTTPException(401, { message: 'No pubkey provided' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!signer.nip44) {
|
|
||||||
throw new HTTPException(401, { message: 'No NIP-44 signer provided' });
|
|
||||||
}
|
|
||||||
|
|
||||||
await next();
|
|
||||||
};
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
import { type DittoConf } from '@ditto/conf';
|
|
||||||
import { MiddlewareHandler } from '@hono/hono';
|
|
||||||
import { HTTPException } from '@hono/hono/http-exception';
|
|
||||||
import { NostrSigner, NSecSigner } from '@nostrify/nostrify';
|
|
||||||
import { nip19 } from 'nostr-tools';
|
|
||||||
|
|
||||||
import { ConnectSigner } from '@/signers/ConnectSigner.ts';
|
|
||||||
import { ReadOnlySigner } from '@/signers/ReadOnlySigner.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
import { aesDecrypt } from '@/utils/aes.ts';
|
|
||||||
import { getTokenHash } from '@/utils/auth.ts';
|
|
||||||
|
|
||||||
/** We only accept "Bearer" type. */
|
|
||||||
const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`);
|
|
||||||
|
|
||||||
/** Make a `signer` object available to all controllers, or unset if the user isn't logged in. */
|
|
||||||
export const signerMiddleware: MiddlewareHandler<{ Variables: { signer: NostrSigner; conf: DittoConf } }> = async (
|
|
||||||
c,
|
|
||||||
next,
|
|
||||||
) => {
|
|
||||||
const { conf } = c.var;
|
|
||||||
const header = c.req.header('authorization');
|
|
||||||
const match = header?.match(BEARER_REGEX);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const [_, bech32] = match;
|
|
||||||
|
|
||||||
if (bech32.startsWith('token1')) {
|
|
||||||
try {
|
|
||||||
const kysely = await Storages.kysely();
|
|
||||||
const tokenHash = await getTokenHash(bech32 as `token1${string}`);
|
|
||||||
|
|
||||||
const { pubkey: userPubkey, bunker_pubkey: bunkerPubkey, nip46_sk_enc, nip46_relays } = await kysely
|
|
||||||
.selectFrom('auth_tokens')
|
|
||||||
.select(['pubkey', 'bunker_pubkey', 'nip46_sk_enc', 'nip46_relays'])
|
|
||||||
.where('token_hash', '=', tokenHash)
|
|
||||||
.executeTakeFirstOrThrow();
|
|
||||||
|
|
||||||
const nep46Seckey = await aesDecrypt(conf.seckey, nip46_sk_enc);
|
|
||||||
|
|
||||||
c.set(
|
|
||||||
'signer',
|
|
||||||
new ConnectSigner({
|
|
||||||
bunkerPubkey,
|
|
||||||
userPubkey,
|
|
||||||
signer: new NSecSigner(nep46Seckey),
|
|
||||||
relays: nip46_relays,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
throw new HTTPException(401);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const decoded = nip19.decode(bech32!);
|
|
||||||
|
|
||||||
switch (decoded.type) {
|
|
||||||
case 'npub':
|
|
||||||
c.set('signer', new ReadOnlySigner(decoded.data));
|
|
||||||
break;
|
|
||||||
case 'nprofile':
|
|
||||||
c.set('signer', new ReadOnlySigner(decoded.data.pubkey));
|
|
||||||
break;
|
|
||||||
case 'nsec':
|
|
||||||
c.set('signer', new NSecSigner(decoded.data));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
throw new HTTPException(401);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await next();
|
|
||||||
};
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { MiddlewareHandler } from '@hono/hono';
|
|
||||||
import { NostrSigner, NStore } from '@nostrify/nostrify';
|
|
||||||
|
|
||||||
import { UserStore } from '@/storages/UserStore.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
|
||||||
|
|
||||||
export const requireStore: MiddlewareHandler<{ Variables: { store: NStore } }> = async (c, next) => {
|
|
||||||
if (!c.get('store')) {
|
|
||||||
throw new Error('Store is required');
|
|
||||||
}
|
|
||||||
await next();
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Store middleware. */
|
|
||||||
export const storeMiddleware: MiddlewareHandler<{ Variables: { signer?: NostrSigner; store: NStore } }> = async (
|
|
||||||
c,
|
|
||||||
next,
|
|
||||||
) => {
|
|
||||||
const pubkey = await c.get('signer')?.getPublicKey();
|
|
||||||
|
|
||||||
if (pubkey) {
|
|
||||||
const store = new UserStore(pubkey, await Storages.admin());
|
|
||||||
c.set('store', store);
|
|
||||||
} else {
|
|
||||||
c.set('store', await Storages.admin());
|
|
||||||
}
|
|
||||||
await next();
|
|
||||||
};
|
|
||||||
|
|
@ -6,7 +6,8 @@ import { AppMiddleware } from '@/app.ts';
|
||||||
|
|
||||||
/** Set an uploader for the user. */
|
/** Set an uploader for the user. */
|
||||||
export const uploaderMiddleware: AppMiddleware = async (c, next) => {
|
export const uploaderMiddleware: AppMiddleware = async (c, next) => {
|
||||||
const { signer, conf } = c.var;
|
const { user, conf } = c.var;
|
||||||
|
const signer = user?.signer;
|
||||||
|
|
||||||
switch (conf.uploader) {
|
switch (conf.uploader) {
|
||||||
case 's3':
|
case 's3':
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ function isProtectedEvent(event: NostrEvent): boolean {
|
||||||
|
|
||||||
/** Hydrate the event with the user, if applicable. */
|
/** Hydrate the event with the user, if applicable. */
|
||||||
async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise<void> {
|
async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise<void> {
|
||||||
await hydrateEvents({ events: [event], store: await Storages.db(), signal });
|
await hydrateEvents({ events: [event], relay: await Storages.db(), signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Maybe store the event, if eligible. */
|
/** Maybe store the event, if eligible. */
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,13 @@ interface GetEventOpts {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a Nostr event by its ID.
|
* Get a Nostr event by its ID.
|
||||||
* @deprecated Use `store.query` directly.
|
* @deprecated Use `relay.query` directly.
|
||||||
*/
|
*/
|
||||||
const getEvent = async (
|
const getEvent = async (
|
||||||
id: string,
|
id: string,
|
||||||
opts: GetEventOpts = {},
|
opts: GetEventOpts = {},
|
||||||
): Promise<DittoEvent | undefined> => {
|
): Promise<DittoEvent | undefined> => {
|
||||||
const store = await Storages.db();
|
const relay = await Storages.db();
|
||||||
const { kind, signal = AbortSignal.timeout(1000) } = opts;
|
const { kind, signal = AbortSignal.timeout(1000) } = opts;
|
||||||
|
|
||||||
const filter: NostrFilter = { ids: [id], limit: 1 };
|
const filter: NostrFilter = { ids: [id], limit: 1 };
|
||||||
|
|
@ -33,23 +33,23 @@ const getEvent = async (
|
||||||
filter.kinds = [kind];
|
filter.kinds = [kind];
|
||||||
}
|
}
|
||||||
|
|
||||||
return await store.query([filter], { limit: 1, signal })
|
return await relay.query([filter], { limit: 1, signal })
|
||||||
.then((events) => hydrateEvents({ events, store, signal }))
|
.then((events) => hydrateEvents({ events, relay, signal }))
|
||||||
.then(([event]) => event);
|
.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 `store.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 store = await Storages.db();
|
const relay = await Storages.db();
|
||||||
const { signal = AbortSignal.timeout(1000) } = opts;
|
const { signal = AbortSignal.timeout(1000) } = opts;
|
||||||
|
|
||||||
const events = await store.query([{ authors: [pubkey], kinds: [0], limit: 1 }], { limit: 1, signal });
|
const events = await relay.query([{ authors: [pubkey], kinds: [0], limit: 1 }], { limit: 1, signal });
|
||||||
const event = events[0] ?? fallbackAuthor(pubkey);
|
const event = events[0] ?? fallbackAuthor(pubkey);
|
||||||
|
|
||||||
await hydrateEvents({ events: [event], store, signal });
|
await hydrateEvents({ events: [event], relay, signal });
|
||||||
|
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ Deno.test('hydrateEvents(): author --- WITHOUT stats', async () => {
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({
|
||||||
events: [event1],
|
events: [event1],
|
||||||
store: relay,
|
relay,
|
||||||
kysely: db.kysely,
|
kysely: db.kysely,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ Deno.test('hydrateEvents(): repost --- WITHOUT stats', async () => {
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({
|
||||||
events: [event6],
|
events: [event6],
|
||||||
store: relay,
|
relay,
|
||||||
kysely: db.kysely,
|
kysely: db.kysely,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ Deno.test('hydrateEvents(): quote repost --- WITHOUT stats', async () => {
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({
|
||||||
events: [event1quoteRepost],
|
events: [event1quoteRepost],
|
||||||
store: relay,
|
relay,
|
||||||
kysely: db.kysely,
|
kysely: db.kysely,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -102,7 +102,7 @@ Deno.test('hydrateEvents(): repost of quote repost --- WITHOUT stats', async ()
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({
|
||||||
events: [event6],
|
events: [event6],
|
||||||
store: relay,
|
relay,
|
||||||
kysely: db.kysely,
|
kysely: db.kysely,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -131,7 +131,7 @@ Deno.test('hydrateEvents(): report pubkey and post // kind 1984 --- WITHOUT stat
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({
|
||||||
events: [reportEvent],
|
events: [reportEvent],
|
||||||
store: relay,
|
relay,
|
||||||
kysely: db.kysely,
|
kysely: db.kysely,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -161,7 +161,7 @@ Deno.test('hydrateEvents(): zap sender, zap amount, zapped post // kind 9735 ---
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({
|
||||||
events: [zapReceipt],
|
events: [zapReceipt],
|
||||||
store: relay,
|
relay,
|
||||||
kysely: db.kysely,
|
kysely: db.kysely,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@ import { Storages } from '@/storages.ts';
|
||||||
|
|
||||||
interface HydrateOpts {
|
interface HydrateOpts {
|
||||||
events: DittoEvent[];
|
events: DittoEvent[];
|
||||||
store: NStore;
|
relay: NStore;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
kysely?: Kysely<DittoTables>;
|
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, store, signal, kysely = await Storages.kysely() } = opts;
|
const { events, relay, signal, kysely = await Storages.kysely() } = opts;
|
||||||
|
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
return events;
|
return events;
|
||||||
|
|
@ -30,23 +30,23 @@ async function hydrateEvents(opts: HydrateOpts): Promise<DittoEvent[]> {
|
||||||
|
|
||||||
const cache = [...events];
|
const cache = [...events];
|
||||||
|
|
||||||
for (const event of await gatherRelatedEvents({ events: cache, store, signal })) {
|
for (const event of await gatherRelatedEvents({ events: cache, relay, signal })) {
|
||||||
cache.push(event);
|
cache.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of await gatherQuotes({ events: cache, store, signal })) {
|
for (const event of await gatherQuotes({ events: cache, relay, signal })) {
|
||||||
cache.push(event);
|
cache.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of await gatherProfiles({ events: cache, store, signal })) {
|
for (const event of await gatherProfiles({ events: cache, relay, signal })) {
|
||||||
cache.push(event);
|
cache.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of await gatherUsers({ events: cache, store, signal })) {
|
for (const event of await gatherUsers({ events: cache, relay, signal })) {
|
||||||
cache.push(event);
|
cache.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of await gatherInfo({ events: cache, store, signal })) {
|
for (const event of await gatherInfo({ events: cache, relay, signal })) {
|
||||||
cache.push(event);
|
cache.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,7 +199,7 @@ export function assembleEvents(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Collect event targets (eg reposts, quote posts, reacted posts, etc.) */
|
/** Collect event targets (eg reposts, quote posts, reacted posts, etc.) */
|
||||||
function gatherRelatedEvents({ events, store, signal }: HydrateOpts): Promise<DittoEvent[]> {
|
function gatherRelatedEvents({ 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) {
|
||||||
|
|
@ -234,14 +234,14 @@ function gatherRelatedEvents({ events, store, signal }: HydrateOpts): Promise<Di
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return store.query(
|
return relay.query(
|
||||||
[{ ids: [...ids], limit: ids.size }],
|
[{ ids: [...ids], limit: ids.size }],
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Collect quotes from the events. */
|
/** Collect quotes from the events. */
|
||||||
function gatherQuotes({ events, store, signal }: HydrateOpts): Promise<DittoEvent[]> {
|
function gatherQuotes({ 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) {
|
||||||
|
|
@ -253,14 +253,14 @@ function gatherQuotes({ events, store, signal }: HydrateOpts): Promise<DittoEven
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return store.query(
|
return relay.query(
|
||||||
[{ ids: [...ids], limit: ids.size }],
|
[{ ids: [...ids], limit: ids.size }],
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Collect profiles from the events. */
|
/** Collect profiles from the events. */
|
||||||
async function gatherProfiles({ events, store, signal }: HydrateOpts): Promise<DittoEvent[]> {
|
async function gatherProfiles({ events, relay, signal }: HydrateOpts): Promise<DittoEvent[]> {
|
||||||
const pubkeys = new Set<string>();
|
const pubkeys = new Set<string>();
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
|
|
@ -300,7 +300,7 @@ async function gatherProfiles({ events, store, signal }: HydrateOpts): Promise<D
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const authors = await store.query(
|
const authors = await relay.query(
|
||||||
[{ kinds: [0], authors: [...pubkeys], limit: pubkeys.size }],
|
[{ kinds: [0], authors: [...pubkeys], limit: pubkeys.size }],
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
|
|
@ -317,21 +317,21 @@ async function gatherProfiles({ events, store, signal }: HydrateOpts): Promise<D
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Collect users from the events. */
|
/** Collect users from the events. */
|
||||||
async function gatherUsers({ events, store, signal }: HydrateOpts): Promise<DittoEvent[]> {
|
async function gatherUsers({ 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) {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return store.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, store, signal }: HydrateOpts): Promise<DittoEvent[]> {
|
async function gatherInfo({ 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) {
|
||||||
|
|
@ -344,7 +344,7 @@ async function gatherInfo({ events, store, signal }: HydrateOpts): Promise<Ditto
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return store.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 },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -267,7 +267,7 @@ function localRequest(c: Context): Request {
|
||||||
/** 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 (
|
||||||
!c.get('signer') && author.tags.some(([name, value, ns]) =>
|
!c.var.user && author.tags.some(([name, value, ns]) =>
|
||||||
name === 'l' &&
|
name === 'l' &&
|
||||||
value === '!no-unauthenticated' &&
|
value === '!no-unauthenticated' &&
|
||||||
ns === 'com.atproto.label.defs#selfLabel'
|
ns === 'com.atproto.label.defs#selfLabel'
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
import { NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
||||||
|
|
||||||
import { AppContext } from '@/app.ts';
|
import { AppContext } from '@/app.ts';
|
||||||
import { Storages } from '@/storages.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 { paginated, paginatedList } from '@/utils/api.ts';
|
||||||
|
|
@ -20,13 +20,12 @@ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], opts?:
|
||||||
}
|
}
|
||||||
|
|
||||||
const { signal = AbortSignal.timeout(1000), filterFn } = opts ?? {};
|
const { signal = AbortSignal.timeout(1000), filterFn } = opts ?? {};
|
||||||
|
const { relay } = c.var;
|
||||||
|
|
||||||
const store = await Storages.db();
|
const events = await relay.query(filters, { signal })
|
||||||
|
|
||||||
const events = await store.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, store, signal }))
|
.then((events) => hydrateEvents({ 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(
|
||||||
|
|
@ -43,14 +42,13 @@ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], opts?:
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderAccounts(c: AppContext, pubkeys: string[]) {
|
async function renderAccounts(c: AppContext, pubkeys: string[]) {
|
||||||
const { offset, limit } = c.get('listPagination');
|
const { offset, limit } = paginationSchema.parse(c.req.query());
|
||||||
const authors = pubkeys.reverse().slice(offset, offset + limit);
|
const authors = pubkeys.reverse().slice(offset, offset + limit);
|
||||||
|
|
||||||
const store = await Storages.db();
|
const { relay, signal } = c.var;
|
||||||
const signal = c.req.raw.signal;
|
|
||||||
|
|
||||||
const events = await store.query([{ kinds: [0], authors }], { signal })
|
const events = await relay.query([{ kinds: [0], authors }], { signal })
|
||||||
.then((events) => hydrateEvents({ events, store, signal }));
|
.then((events) => hydrateEvents({ events, relay, signal }));
|
||||||
|
|
||||||
const accounts = await Promise.all(
|
const accounts = await Promise.all(
|
||||||
authors.map((pubkey) => {
|
authors.map((pubkey) => {
|
||||||
|
|
@ -72,11 +70,11 @@ async function renderStatuses(c: AppContext, ids: string[], signal = AbortSignal
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = await Storages.db();
|
const { user, relay, pagination } = c.var;
|
||||||
const { limit } = c.get('pagination');
|
const { limit } = pagination;
|
||||||
|
|
||||||
const events = await store.query([{ kinds: [1, 20], ids, limit }], { signal })
|
const events = await relay.query([{ kinds: [1, 20], ids, limit }], { signal })
|
||||||
.then((events) => hydrateEvents({ events, store, signal }));
|
.then((events) => hydrateEvents({ events, relay, signal }));
|
||||||
|
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
|
|
@ -84,7 +82,7 @@ async function renderStatuses(c: AppContext, ids: string[], signal = AbortSignal
|
||||||
|
|
||||||
const sortedEvents = [...events].sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
|
const sortedEvents = [...events].sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
|
||||||
|
|
||||||
const viewerPubkey = await c.get('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(event, { viewerPubkey })),
|
||||||
|
|
|
||||||
18
packages/mastoapi/auth/aes.bench.ts
Normal file
18
packages/mastoapi/auth/aes.bench.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { generateSecretKey } from 'nostr-tools';
|
||||||
|
|
||||||
|
import { aesDecrypt, aesEncrypt } from './aes.ts';
|
||||||
|
|
||||||
|
Deno.bench('aesEncrypt', async (b) => {
|
||||||
|
const sk = generateSecretKey();
|
||||||
|
const decrypted = generateSecretKey();
|
||||||
|
b.start();
|
||||||
|
await aesEncrypt(sk, decrypted);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.bench('aesDecrypt', async (b) => {
|
||||||
|
const sk = generateSecretKey();
|
||||||
|
const decrypted = generateSecretKey();
|
||||||
|
const encrypted = await aesEncrypt(sk, decrypted);
|
||||||
|
b.start();
|
||||||
|
await aesDecrypt(sk, encrypted);
|
||||||
|
});
|
||||||
15
packages/mastoapi/auth/aes.test.ts
Normal file
15
packages/mastoapi/auth/aes.test.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
import { encodeHex } from '@std/encoding/hex';
|
||||||
|
import { generateSecretKey } from 'nostr-tools';
|
||||||
|
|
||||||
|
import { aesDecrypt, aesEncrypt } from './aes.ts';
|
||||||
|
|
||||||
|
Deno.test('aesDecrypt & aesEncrypt', async () => {
|
||||||
|
const sk = generateSecretKey();
|
||||||
|
const data = generateSecretKey();
|
||||||
|
|
||||||
|
const encrypted = await aesEncrypt(sk, data);
|
||||||
|
const decrypted = await aesDecrypt(sk, encrypted);
|
||||||
|
|
||||||
|
assertEquals(encodeHex(decrypted), encodeHex(data));
|
||||||
|
});
|
||||||
17
packages/mastoapi/auth/aes.ts
Normal file
17
packages/mastoapi/auth/aes.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
/** Encrypt data with AES-GCM and a secret key. */
|
||||||
|
export async function aesEncrypt(sk: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array> {
|
||||||
|
const secretKey = await crypto.subtle.importKey('raw', sk, { name: 'AES-GCM' }, false, ['encrypt']);
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
const buffer = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, secretKey, plaintext);
|
||||||
|
|
||||||
|
return new Uint8Array([...iv, ...new Uint8Array(buffer)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decrypt data with AES-GCM and a secret key. */
|
||||||
|
export async function aesDecrypt(sk: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> {
|
||||||
|
const secretKey = await crypto.subtle.importKey('raw', sk, { name: 'AES-GCM' }, false, ['decrypt']);
|
||||||
|
const iv = ciphertext.slice(0, 12);
|
||||||
|
const buffer = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, secretKey, ciphertext.slice(12));
|
||||||
|
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
11
packages/mastoapi/auth/token.bench.ts
Normal file
11
packages/mastoapi/auth/token.bench.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { generateToken, getTokenHash } from './token.ts';
|
||||||
|
|
||||||
|
Deno.bench('generateToken', async () => {
|
||||||
|
await generateToken();
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.bench('getTokenHash', async (b) => {
|
||||||
|
const { token } = await generateToken();
|
||||||
|
b.start();
|
||||||
|
await getTokenHash(token);
|
||||||
|
});
|
||||||
18
packages/mastoapi/auth/token.test.ts
Normal file
18
packages/mastoapi/auth/token.test.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
import { decodeHex, encodeHex } from '@std/encoding/hex';
|
||||||
|
|
||||||
|
import { generateToken, getTokenHash } from './token.ts';
|
||||||
|
|
||||||
|
Deno.test('generateToken', async () => {
|
||||||
|
const sk = decodeHex('a0968751df8fd42f362213f08751911672f2a037113b392403bbb7dd31b71c95');
|
||||||
|
|
||||||
|
const { token, hash } = await generateToken(sk);
|
||||||
|
|
||||||
|
assertEquals(token, 'token15ztgw5wl3l2z7d3zz0cgw5v3zee09gphzyanjfqrhwma6vdhrj2sauwknd');
|
||||||
|
assertEquals(encodeHex(hash), 'ab4c4ead4d1c72a38fffd45b999937b7e3f25f867b19aaf252df858e77b66a8a');
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('getTokenHash', async () => {
|
||||||
|
const hash = await getTokenHash('token15ztgw5wl3l2z7d3zz0cgw5v3zee09gphzyanjfqrhwma6vdhrj2sauwknd');
|
||||||
|
assertEquals(encodeHex(hash), 'ab4c4ead4d1c72a38fffd45b999937b7e3f25f867b19aaf252df858e77b66a8a');
|
||||||
|
});
|
||||||
30
packages/mastoapi/auth/token.ts
Normal file
30
packages/mastoapi/auth/token.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { bech32 } from '@scure/base';
|
||||||
|
import { generateSecretKey } from 'nostr-tools';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an auth token for the API.
|
||||||
|
*
|
||||||
|
* Returns a bech32 encoded API token and the SHA-256 hash of the bytes.
|
||||||
|
* The token should be presented to the user, but only the hash should be stored in the database.
|
||||||
|
*/
|
||||||
|
export async function generateToken(sk = generateSecretKey()): Promise<{ token: `token1${string}`; hash: Uint8Array }> {
|
||||||
|
const words = bech32.toWords(sk);
|
||||||
|
const token = bech32.encode('token', words);
|
||||||
|
|
||||||
|
const buffer = await crypto.subtle.digest('SHA-256', sk);
|
||||||
|
const hash = new Uint8Array(buffer);
|
||||||
|
|
||||||
|
return { token, hash };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the SHA-256 hash of an API token.
|
||||||
|
* First decodes from bech32 then hashes the bytes.
|
||||||
|
* Used to identify the user in the database by the hash of their token.
|
||||||
|
*/
|
||||||
|
export async function getTokenHash(token: `token1${string}`): Promise<Uint8Array> {
|
||||||
|
const { bytes: sk } = bech32.decodeToBytes(token);
|
||||||
|
const buffer = await crypto.subtle.digest('SHA-256', sk);
|
||||||
|
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "@ditto/api",
|
"name": "@ditto/mastoapi",
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./middleware": "./middleware/mod.ts"
|
"./middleware": "./middleware/mod.ts"
|
||||||
2
packages/mastoapi/middleware/mod.ts
Normal file
2
packages/mastoapi/middleware/mod.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { paginationMiddleware } from './paginationMiddleware.ts';
|
||||||
|
export { userMiddleware } from './userMiddleware.ts';
|
||||||
81
packages/mastoapi/middleware/paginationMiddleware.ts
Normal file
81
packages/mastoapi/middleware/paginationMiddleware.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { paginated, paginatedList } from '../pagination/paginate.ts';
|
||||||
|
import { paginationSchema } from '../pagination/schema.ts';
|
||||||
|
|
||||||
|
import type { DittoMiddleware } from '@ditto/router';
|
||||||
|
import type { NostrEvent } from '@nostrify/nostrify';
|
||||||
|
|
||||||
|
interface Pagination {
|
||||||
|
since?: number;
|
||||||
|
until?: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListPagination {
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderRecord = Record<string, string | string[]>;
|
||||||
|
type PaginateFn = (events: NostrEvent[], body: object | unknown[], headers?: HeaderRecord) => Response;
|
||||||
|
type ListPaginateFn = (params: ListPagination, body: object | unknown[], headers?: HeaderRecord) => Response;
|
||||||
|
|
||||||
|
/** Fixes compatibility with Mastodon apps by that don't use `Link` headers. */
|
||||||
|
// @ts-ignore Types are right.
|
||||||
|
export function paginationMiddleware(): DittoMiddleware<{ pagination: Pagination; paginate: PaginateFn }>;
|
||||||
|
export function paginationMiddleware(
|
||||||
|
type: 'list',
|
||||||
|
): DittoMiddleware<{ pagination: ListPagination; paginate: ListPaginateFn }>;
|
||||||
|
export function paginationMiddleware(
|
||||||
|
type?: string,
|
||||||
|
): DittoMiddleware<{ pagination?: Pagination | ListPagination; paginate: PaginateFn | ListPaginateFn }> {
|
||||||
|
return async (c, next) => {
|
||||||
|
const { relay } = c.var;
|
||||||
|
|
||||||
|
const pagination = paginationSchema.parse(c.req.query());
|
||||||
|
|
||||||
|
const {
|
||||||
|
max_id: maxId,
|
||||||
|
min_id: minId,
|
||||||
|
since,
|
||||||
|
until,
|
||||||
|
} = pagination;
|
||||||
|
|
||||||
|
if ((maxId && !until) || (minId && !since)) {
|
||||||
|
const ids: string[] = [];
|
||||||
|
|
||||||
|
if (maxId) ids.push(maxId);
|
||||||
|
if (minId) ids.push(minId);
|
||||||
|
|
||||||
|
if (ids.length) {
|
||||||
|
const events = await relay.query(
|
||||||
|
[{ ids, limit: ids.length }],
|
||||||
|
{ signal: c.req.raw.signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
if (!until && maxId === event.id) pagination.until = event.created_at;
|
||||||
|
if (!since && minId === event.id) pagination.since = event.created_at;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'list') {
|
||||||
|
c.set('pagination', {
|
||||||
|
limit: pagination.limit,
|
||||||
|
offset: pagination.offset,
|
||||||
|
});
|
||||||
|
const fn: ListPaginateFn = (params, body, headers) => paginatedList(c, params, body, headers);
|
||||||
|
c.set('paginate', fn);
|
||||||
|
} else {
|
||||||
|
c.set('pagination', {
|
||||||
|
since: pagination.since,
|
||||||
|
until: pagination.until,
|
||||||
|
limit: pagination.limit,
|
||||||
|
});
|
||||||
|
const fn: PaginateFn = (events, body, headers) => paginated(c, events, body, headers);
|
||||||
|
c.set('paginate', fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
}
|
||||||
128
packages/mastoapi/middleware/userMiddleware.ts
Normal file
128
packages/mastoapi/middleware/userMiddleware.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { HTTPException } from '@hono/hono/http-exception';
|
||||||
|
import { type NostrSigner, type NRelay, NSecSigner } from '@nostrify/nostrify';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
|
import { aesDecrypt } from '../auth/aes.ts';
|
||||||
|
import { getTokenHash } from '../auth/token.ts';
|
||||||
|
import { ConnectSigner } from '../signers/ConnectSigner.ts';
|
||||||
|
import { ReadOnlySigner } from '../signers/ReadOnlySigner.ts';
|
||||||
|
|
||||||
|
import type { DittoConf } from '@ditto/conf';
|
||||||
|
import type { DittoDB } from '@ditto/db';
|
||||||
|
import type { DittoMiddleware } from '@ditto/router';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
signer: NostrSigner;
|
||||||
|
relay: NRelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** We only accept "Bearer" type. */
|
||||||
|
const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`);
|
||||||
|
|
||||||
|
export function userMiddleware(opts: { privileged: true; required: false }): never;
|
||||||
|
// @ts-ignore The types are right.
|
||||||
|
export function userMiddleware(opts: { privileged: false; required: true }): DittoMiddleware<{ user: User }>;
|
||||||
|
export function userMiddleware(opts: { privileged: true; required?: boolean }): DittoMiddleware<{ user: User }>;
|
||||||
|
export function userMiddleware(opts: { privileged: false; required?: boolean }): DittoMiddleware<{ user?: User }>;
|
||||||
|
export function userMiddleware(opts: { privileged: boolean; required?: boolean }): DittoMiddleware<{ user?: User }> {
|
||||||
|
const { privileged, required = privileged } = opts;
|
||||||
|
|
||||||
|
if (privileged && !required) {
|
||||||
|
throw new Error('Privileged middleware requires authorization.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return async (c, next) => {
|
||||||
|
const header = c.req.header('authorization');
|
||||||
|
|
||||||
|
if (!header && required) {
|
||||||
|
throw new HTTPException(403, { message: 'Authorization required.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header) {
|
||||||
|
const user: User = {
|
||||||
|
signer: await getSigner(header, c.var),
|
||||||
|
relay: c.var.relay, // TODO: set user's relay
|
||||||
|
};
|
||||||
|
|
||||||
|
c.set('user', user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (privileged) {
|
||||||
|
// TODO: add back nip98 auth
|
||||||
|
throw new HTTPException(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSignerOpts {
|
||||||
|
db: DittoDB;
|
||||||
|
conf: DittoConf;
|
||||||
|
relay: NRelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSigner(header: string, opts: GetSignerOpts): NostrSigner | Promise<NostrSigner> {
|
||||||
|
const match = header.match(BEARER_REGEX);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new HTTPException(400, { message: 'Invalid Authorization header.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [_, bech32] = match;
|
||||||
|
|
||||||
|
if (isToken(bech32)) {
|
||||||
|
return getSignerFromToken(bech32, opts);
|
||||||
|
} else {
|
||||||
|
return getSignerFromNip19(bech32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isToken(value: string): value is `token1${string}` {
|
||||||
|
return value.startsWith('token1');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSignerFromToken(token: `token1${string}`, opts: GetSignerOpts): Promise<NostrSigner> {
|
||||||
|
const { conf, db, relay } = opts;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenHash = await getTokenHash(token);
|
||||||
|
|
||||||
|
const row = await db.kysely
|
||||||
|
.selectFrom('auth_tokens')
|
||||||
|
.select(['pubkey', 'bunker_pubkey', 'nip46_sk_enc', 'nip46_relays'])
|
||||||
|
.where('token_hash', '=', tokenHash)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
const nep46Seckey = await aesDecrypt(conf.seckey, row.nip46_sk_enc);
|
||||||
|
|
||||||
|
return new ConnectSigner({
|
||||||
|
bunkerPubkey: row.bunker_pubkey,
|
||||||
|
userPubkey: row.pubkey,
|
||||||
|
signer: new NSecSigner(nep46Seckey),
|
||||||
|
relays: row.nip46_relays,
|
||||||
|
relay,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
throw new HTTPException(401, { message: 'Token is wrong or expired.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSignerFromNip19(bech32: string): NostrSigner {
|
||||||
|
try {
|
||||||
|
const decoded = nip19.decode(bech32);
|
||||||
|
|
||||||
|
switch (decoded.type) {
|
||||||
|
case 'npub':
|
||||||
|
return new ReadOnlySigner(decoded.data);
|
||||||
|
case 'nprofile':
|
||||||
|
return new ReadOnlySigner(decoded.data.pubkey);
|
||||||
|
case 'nsec':
|
||||||
|
return new NSecSigner(decoded.data);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fallthrough
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new HTTPException(401, { message: 'Invalid NIP-19 identifier in Authorization header.' });
|
||||||
|
}
|
||||||
34
packages/mastoapi/pagination/link-header.test.ts
Normal file
34
packages/mastoapi/pagination/link-header.test.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { genEvent } from '@nostrify/nostrify/test';
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
|
import { buildLinkHeader, buildListLinkHeader } from './link-header.ts';
|
||||||
|
|
||||||
|
Deno.test('buildLinkHeader', () => {
|
||||||
|
const url = 'https://ditto.test/api/v1/events';
|
||||||
|
|
||||||
|
const events = [
|
||||||
|
genEvent({ created_at: 1 }),
|
||||||
|
genEvent({ created_at: 2 }),
|
||||||
|
genEvent({ created_at: 3 }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const link = buildLinkHeader(url, events);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
link?.toString(),
|
||||||
|
'<https://ditto.test/api/v1/events?until=3>; rel="next", <https://ditto.test/api/v1/events?since=1>; rel="prev"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('buildListLinkHeader', () => {
|
||||||
|
const url = 'https://ditto.test/api/v1/tags';
|
||||||
|
|
||||||
|
const params = { offset: 0, limit: 3 };
|
||||||
|
|
||||||
|
const link = buildListLinkHeader(url, params);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
link?.toString(),
|
||||||
|
'<https://ditto.test/api/v1/tags?offset=3&limit=3>; rel="next", <https://ditto.test/api/v1/tags?offset=0&limit=3>; rel="prev"',
|
||||||
|
);
|
||||||
|
});
|
||||||
39
packages/mastoapi/pagination/link-header.ts
Normal file
39
packages/mastoapi/pagination/link-header.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import type { NostrEvent } from '@nostrify/nostrify';
|
||||||
|
|
||||||
|
/** Build HTTP Link header for Mastodon API pagination. */
|
||||||
|
export function buildLinkHeader(url: string, events: NostrEvent[]): string | undefined {
|
||||||
|
if (events.length <= 1) return;
|
||||||
|
|
||||||
|
const firstEvent = events[0];
|
||||||
|
const lastEvent = events[events.length - 1];
|
||||||
|
|
||||||
|
const { pathname, search } = new URL(url);
|
||||||
|
|
||||||
|
const next = new URL(pathname + search, url);
|
||||||
|
const prev = new URL(pathname + search, url);
|
||||||
|
|
||||||
|
next.searchParams.set('until', String(lastEvent.created_at));
|
||||||
|
prev.searchParams.set('since', String(firstEvent.created_at));
|
||||||
|
|
||||||
|
return `<${next}>; rel="next", <${prev}>; rel="prev"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build HTTP Link header for paginating Nostr lists. */
|
||||||
|
export function buildListLinkHeader(
|
||||||
|
url: string,
|
||||||
|
params: { offset: number; limit: number },
|
||||||
|
): string | undefined {
|
||||||
|
const { pathname, search } = new URL(url);
|
||||||
|
const { offset, limit } = params;
|
||||||
|
|
||||||
|
const next = new URL(pathname + search, url);
|
||||||
|
const prev = new URL(pathname + search, url);
|
||||||
|
|
||||||
|
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"`;
|
||||||
|
}
|
||||||
0
packages/mastoapi/pagination/paginate.test.ts
Normal file
0
packages/mastoapi/pagination/paginate.test.ts
Normal file
43
packages/mastoapi/pagination/paginate.ts
Normal file
43
packages/mastoapi/pagination/paginate.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { buildLinkHeader, buildListLinkHeader } from './link-header.ts';
|
||||||
|
|
||||||
|
import type { Context } from '@hono/hono';
|
||||||
|
import type { NostrEvent } from '@nostrify/nostrify';
|
||||||
|
|
||||||
|
type HeaderRecord = Record<string, string | string[]>;
|
||||||
|
|
||||||
|
/** Return results with pagination headers. Assumes chronological sorting of events. */
|
||||||
|
export function paginated(
|
||||||
|
c: Context,
|
||||||
|
events: NostrEvent[],
|
||||||
|
body: object | unknown[],
|
||||||
|
headers: HeaderRecord = {},
|
||||||
|
): Response {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** paginate a list of tags. */
|
||||||
|
export function paginatedList(
|
||||||
|
c: Context,
|
||||||
|
params: { offset: number; limit: number },
|
||||||
|
body: object | unknown[],
|
||||||
|
headers: HeaderRecord = {},
|
||||||
|
): Response {
|
||||||
|
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);
|
||||||
|
}
|
||||||
23
packages/mastoapi/pagination/schema.test.ts
Normal file
23
packages/mastoapi/pagination/schema.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
|
import { paginationSchema } from './schema.ts';
|
||||||
|
|
||||||
|
Deno.test('paginationSchema', () => {
|
||||||
|
const pagination = paginationSchema.parse({
|
||||||
|
limit: '10',
|
||||||
|
offset: '20',
|
||||||
|
max_id: '1',
|
||||||
|
min_id: '2',
|
||||||
|
since: '3',
|
||||||
|
until: '4',
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(pagination, {
|
||||||
|
limit: 10,
|
||||||
|
offset: 20,
|
||||||
|
max_id: '1',
|
||||||
|
min_id: '2',
|
||||||
|
since: 3,
|
||||||
|
until: 4,
|
||||||
|
});
|
||||||
|
});
|
||||||
14
packages/mastoapi/pagination/schema.ts
Normal file
14
packages/mastoapi/pagination/schema.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/** Schema to parse pagination query params. */
|
||||||
|
export const paginationSchema = z.object({
|
||||||
|
max_id: z.string().transform((val) => {
|
||||||
|
if (!val.includes('-')) return val;
|
||||||
|
return val.split('-')[1];
|
||||||
|
}).optional().catch(undefined),
|
||||||
|
min_id: z.string().optional().catch(undefined),
|
||||||
|
since: z.coerce.number().nonnegative().optional().catch(undefined),
|
||||||
|
until: z.coerce.number().nonnegative().optional().catch(undefined),
|
||||||
|
limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)),
|
||||||
|
offset: z.coerce.number().nonnegative().catch(0),
|
||||||
|
});
|
||||||
124
packages/mastoapi/signers/ConnectSigner.ts
Normal file
124
packages/mastoapi/signers/ConnectSigner.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
// deno-lint-ignore-file require-await
|
||||||
|
import { HTTPException } from '@hono/hono/http-exception';
|
||||||
|
import { NConnectSigner, type NostrEvent, type NostrSigner, type NRelay } from '@nostrify/nostrify';
|
||||||
|
|
||||||
|
interface ConnectSignerOpts {
|
||||||
|
relay: NRelay;
|
||||||
|
bunkerPubkey: string;
|
||||||
|
userPubkey: string;
|
||||||
|
signer: NostrSigner;
|
||||||
|
relays?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NIP-46 signer.
|
||||||
|
*
|
||||||
|
* Simple extension of nostrify's `NConnectSigner`, with our options to keep it DRY.
|
||||||
|
*/
|
||||||
|
export class ConnectSigner implements NostrSigner {
|
||||||
|
private signer: Promise<NConnectSigner>;
|
||||||
|
|
||||||
|
constructor(private opts: ConnectSignerOpts) {
|
||||||
|
this.signer = this.init(opts.signer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(signer: NostrSigner): Promise<NConnectSigner> {
|
||||||
|
return new NConnectSigner({
|
||||||
|
encryption: 'nip44',
|
||||||
|
pubkey: this.opts.bunkerPubkey,
|
||||||
|
relay: this.opts.relay,
|
||||||
|
signer,
|
||||||
|
timeout: 60_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async signEvent(event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>): Promise<NostrEvent> {
|
||||||
|
const signer = await this.signer;
|
||||||
|
try {
|
||||||
|
return await signer.signEvent(event);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === 'AbortError') {
|
||||||
|
throw new HTTPException(408, { message: 'The event was not signed quickly enough' });
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly nip04 = {
|
||||||
|
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
|
||||||
|
const signer = await this.signer;
|
||||||
|
try {
|
||||||
|
return await signer.nip04.encrypt(pubkey, plaintext);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === 'AbortError') {
|
||||||
|
throw new HTTPException(408, {
|
||||||
|
message: 'Text was not encrypted quickly enough',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
|
||||||
|
const signer = await this.signer;
|
||||||
|
try {
|
||||||
|
return await signer.nip04.decrypt(pubkey, ciphertext);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === 'AbortError') {
|
||||||
|
throw new HTTPException(408, {
|
||||||
|
message: 'Text was not decrypted quickly enough',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
readonly nip44 = {
|
||||||
|
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
|
||||||
|
const signer = await this.signer;
|
||||||
|
try {
|
||||||
|
return await signer.nip44.encrypt(pubkey, plaintext);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === 'AbortError') {
|
||||||
|
throw new HTTPException(408, {
|
||||||
|
message: 'Text was not encrypted quickly enough',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
|
||||||
|
const signer = await this.signer;
|
||||||
|
try {
|
||||||
|
return await signer.nip44.decrypt(pubkey, ciphertext);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === 'AbortError') {
|
||||||
|
throw new HTTPException(408, {
|
||||||
|
message: 'Text was not decrypted quickly enough',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prevent unnecessary NIP-46 round-trips.
|
||||||
|
async getPublicKey(): Promise<string> {
|
||||||
|
return this.opts.userPubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the user's relays if they passed in an `nprofile` auth token. */
|
||||||
|
async getRelays(): Promise<Record<string, { read: boolean; write: boolean }>> {
|
||||||
|
return this.opts.relays?.reduce<Record<string, { read: boolean; write: boolean }>>((acc, relay) => {
|
||||||
|
acc[relay] = { read: true, write: true };
|
||||||
|
return acc;
|
||||||
|
}, {}) ?? {};
|
||||||
|
}
|
||||||
|
}
|
||||||
18
packages/mastoapi/signers/ReadOnlySigner.ts
Normal file
18
packages/mastoapi/signers/ReadOnlySigner.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
// deno-lint-ignore-file require-await
|
||||||
|
import { HTTPException } from '@hono/hono/http-exception';
|
||||||
|
|
||||||
|
import type { NostrEvent, NostrSigner } from '@nostrify/nostrify';
|
||||||
|
|
||||||
|
export class ReadOnlySigner implements NostrSigner {
|
||||||
|
constructor(private pubkey: string) {}
|
||||||
|
|
||||||
|
async signEvent(): Promise<NostrEvent> {
|
||||||
|
throw new HTTPException(401, {
|
||||||
|
message: 'Log in with Nostr Connect to sign events',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPublicKey(): Promise<string> {
|
||||||
|
return this.pubkey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { DittoConf } from '@ditto/conf';
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { DittoDB } from '@ditto/db';
|
import { DittoPolyPg } from '@ditto/db';
|
||||||
import { Hono } from '@hono/hono';
|
import { Hono } from '@hono/hono';
|
||||||
import { MockRelay } from '@nostrify/nostrify/test';
|
import { MockRelay } from '@nostrify/nostrify/test';
|
||||||
|
|
||||||
|
|
@ -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 = DittoDB.create('memory://');
|
await using db = DittoPolyPg.create('memory://');
|
||||||
const conf = new DittoConf(new Map());
|
const conf = new DittoConf(new Map());
|
||||||
const relay = new MockRelay();
|
const relay = new MockRelay();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { DittoConf } from '@ditto/conf';
|
import type { DittoConf } from '@ditto/conf';
|
||||||
import type { DittoDatabase } from '@ditto/db';
|
import type { DittoDB } from '@ditto/db';
|
||||||
import type { Env } from '@hono/hono';
|
import type { Env } from '@hono/hono';
|
||||||
import type { NRelay } from '@nostrify/nostrify';
|
import type { NRelay } from '@nostrify/nostrify';
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ export interface DittoEnv extends Env {
|
||||||
* Database object.
|
* Database object.
|
||||||
* @deprecated Store data as Nostr events instead.
|
* @deprecated Store data as Nostr events instead.
|
||||||
*/
|
*/
|
||||||
db: DittoDatabase;
|
db: DittoDB;
|
||||||
/** Abort signal for the request. */
|
/** Abort signal for the request. */
|
||||||
signal: AbortSignal;
|
signal: AbortSignal;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@ export { DittoApp } from './DittoApp.ts';
|
||||||
export { DittoRoute } from './DittoRoute.ts';
|
export { DittoRoute } from './DittoRoute.ts';
|
||||||
|
|
||||||
export type { DittoEnv } from './DittoEnv.ts';
|
export type { DittoEnv } from './DittoEnv.ts';
|
||||||
|
export type { DittoMiddleware } from './DittoMiddleware.ts';
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue