Refactor accountsController into a separate route

This commit is contained in:
Alex Gleason 2024-10-13 17:27:39 -05:00
parent 260340c58e
commit 7d877b5a37
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
7 changed files with 151 additions and 211 deletions

View file

@ -9,26 +9,7 @@ import '@/startup.ts';
import { Time } from '@/utils/time.ts'; import { Time } from '@/utils/time.ts';
import { import { accountsController } from '@/controllers/api/accounts.ts';
accountController,
accountLookupController,
accountSearchController,
accountStatusesController,
blockController,
createAccountController,
familiarFollowersController,
favouritesController,
followController,
followersController,
followingController,
muteController,
relationshipsController,
unblockController,
unfollowController,
unmuteController,
updateCredentialsController,
verifyCredentialsController,
} from '@/controllers/api/accounts.ts';
import { import {
adminAccountsController, adminAccountsController,
adminActionController, adminActionController,
@ -50,6 +31,7 @@ import {
updateZapSplitsController, updateZapSplitsController,
} from '@/controllers/api/ditto.ts'; } from '@/controllers/api/ditto.ts';
import { emptyArrayController, emptyObjectController, notImplementedController } from '@/controllers/api/fallback.ts'; import { emptyArrayController, emptyObjectController, notImplementedController } from '@/controllers/api/fallback.ts';
import { favouritesController } from '@/controllers/api/favourites.ts';
import { import {
instanceDescriptionController, instanceDescriptionController,
instanceV1Controller, instanceV1Controller,
@ -200,23 +182,7 @@ app.post('/oauth/revoke', emptyObjectController);
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.route('/api/v1/accounts', accountsController);
app.get('/api/v1/accounts/verify_credentials', requireSigner, verifyCredentialsController);
app.patch('/api/v1/accounts/update_credentials', requireSigner, updateCredentialsController);
app.get('/api/v1/accounts/search', accountSearchController);
app.get('/api/v1/accounts/lookup', accountLookupController);
app.get('/api/v1/accounts/relationships', requireSigner, relationshipsController);
app.get('/api/v1/accounts/familiar_followers', requireSigner, familiarFollowersController);
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/block', requireSigner, blockController);
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unblock', requireSigner, unblockController);
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/mute', requireSigner, muteController);
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unmute', requireSigner, unmuteController);
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', requireSigner, followController);
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unfollow', requireSigner, unfollowController);
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/followers', followersController);
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/following', followingController);
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/statuses', accountStatusesController);
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}', accountController);
app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/favourited_by', favouritedByController); app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/favourited_by', favouritedByController);
app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/reblogged_by', rebloggedByController); app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/reblogged_by', rebloggedByController);
@ -268,7 +234,7 @@ app.get('/api/v1/suggestions', suggestionsV1Controller);
app.get('/api/v2/suggestions', suggestionsV2Controller); app.get('/api/v2/suggestions', suggestionsV2Controller);
app.get('/api/v1/notifications', requireSigner, notificationsController); app.get('/api/v1/notifications', requireSigner, notificationsController);
app.get('/api/v1/favourites', requireSigner, favouritesController); app.route('/api/v1/favourites', favouritesController);
app.get('/api/v1/bookmarks', requireSigner, bookmarksController); app.get('/api/v1/bookmarks', requireSigner, bookmarksController);
app.get('/api/v1/blocks', requireSigner, blocksController); app.get('/api/v1/blocks', requireSigner, blocksController);
app.get('/api/v1/mutes', requireSigner, mutesController); app.get('/api/v1/mutes', requireSigner, mutesController);

View file

@ -1,14 +1,12 @@
import { Hono } from '@hono/hono';
import { NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { NostrFilter, NSchema as n } from '@nostrify/nostrify';
import { nip19 } from 'nostr-tools';
import { z } from 'zod'; import { z } from 'zod';
import { type AppController } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { getAuthor, getFollowedPubkeys } from '@/queries.ts'; import { getAuthor, getFollowedPubkeys } from '@/queries.ts';
import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { uploadFile } from '@/utils/upload.ts'; import { uploadFile } from '@/utils/upload.ts';
import { nostrNow } from '@/utils.ts';
import { createEvent, paginated, parseBody, updateEvent, updateListEvent } from '@/utils/api.ts'; import { createEvent, paginated, parseBody, updateEvent, updateListEvent } from '@/utils/api.ts';
import { extractIdentifier, lookupAccount, lookupPubkey } from '@/utils/lookup.ts'; import { extractIdentifier, lookupAccount, lookupPubkey } from '@/utils/lookup.ts';
import { renderAccounts, renderEventAccounts, renderStatuses } from '@/views.ts'; import { renderAccounts, renderEventAccounts, renderStatuses } from '@/views.ts';
@ -19,34 +17,17 @@ import { hydrateEvents } from '@/storages/hydrate.ts';
import { bech32ToPubkey } from '@/utils.ts'; import { bech32ToPubkey } from '@/utils.ts';
import { addTag, deleteTag, findReplyTag, getTagSet } from '@/utils/tags.ts'; import { addTag, deleteTag, findReplyTag, getTagSet } from '@/utils/tags.ts';
import { getPubkeysBySearch } from '@/utils/search.ts'; import { getPubkeysBySearch } from '@/utils/search.ts';
import { requireSigner } from '@/middleware/requireSigner.ts';
import { paginationMiddleware } from '@/middleware/paginationMiddleware.ts';
const usernameSchema = z export const accountsController = new Hono();
.string().min(1).max(30)
.regex(/^[a-z0-9_]+$/i)
.refine((username) => !Conf.forbiddenUsernames.includes(username), 'Username is reserved.');
const createAccountSchema = z.object({ accountsController.post('/', (c) => {
username: usernameSchema, return c.json({ error: 'Please create an account on the web first' }, 422);
}); });
const createAccountController: AppController = async (c) => { accountsController.get('/verify_credentials', requireSigner, async (c) => {
const pubkey = await c.get('signer')?.getPublicKey()!; const signer = c.get('signer');
const result = createAccountSchema.safeParse(await c.req.json());
if (!result.success) {
return c.json({ error: 'Bad request', schema: result.error }, 400);
}
return c.json({
access_token: nip19.npubEncode(pubkey),
token_type: 'Bearer',
scope: 'read write follow push',
created_at: nostrNow(),
});
};
const verifyCredentialsController: AppController = async (c) => {
const signer = c.get('signer')!;
const pubkey = await signer.getPublicKey(); const pubkey = await signer.getPublicKey();
const store = await Storages.db(); const store = await Storages.db();
@ -74,9 +55,9 @@ const verifyCredentialsController: AppController = async (c) => {
: await accountFromPubkey(pubkey, { withSource: true, settingsStore }); : await accountFromPubkey(pubkey, { withSource: true, settingsStore });
return c.json(account); return c.json(account);
}; });
const accountController: AppController = async (c) => { accountsController.get('/:pubkey{[0-9a-f]{64}}', async (c) => {
const pubkey = c.req.param('pubkey'); const pubkey = c.req.param('pubkey');
const event = await getAuthor(pubkey); const event = await getAuthor(pubkey);
@ -85,9 +66,9 @@ const accountController: AppController = async (c) => {
} else { } else {
return c.json(await accountFromPubkey(pubkey)); return c.json(await accountFromPubkey(pubkey));
} }
}; });
const accountLookupController: AppController = async (c) => { accountsController.get('/lookup', async (c) => {
const acct = c.req.query('acct'); const acct = c.req.query('acct');
if (!acct) { if (!acct) {
@ -104,7 +85,7 @@ const accountLookupController: AppController = async (c) => {
} catch { } catch {
return c.json({ error: 'Could not find user.' }, 404); return c.json({ error: 'Could not find user.' }, 404);
} }
}; });
const accountSearchQuerySchema = z.object({ const accountSearchQuerySchema = z.object({
q: z.string().transform(decodeURIComponent), q: z.string().transform(decodeURIComponent),
@ -112,7 +93,7 @@ const accountSearchQuerySchema = z.object({
following: z.boolean().default(false), following: z.boolean().default(false),
}); });
const accountSearchController: AppController = async (c) => { accountsController.get('/search', paginationMiddleware, async (c) => {
const { signal } = c.req.raw; const { signal } = c.req.raw;
const { limit } = c.get('pagination'); const { limit } = c.get('pagination');
const kysely = await Storages.kysely(); const kysely = await Storages.kysely();
@ -155,10 +136,10 @@ const accountSearchController: AppController = async (c) => {
); );
return c.json(accounts); return c.json(accounts);
}; });
const relationshipsController: AppController = async (c) => { accountsController.get('/relationships', requireSigner, async (c) => {
const pubkey = await c.get('signer')?.getPublicKey()!; const pubkey = await c.get('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) {
@ -186,7 +167,7 @@ const relationshipsController: AppController = async (c) => {
); );
return c.json(result); return c.json(result);
}; });
const accountStatusesQuerySchema = z.object({ const accountStatusesQuerySchema = z.object({
pinned: booleanParamSchema.optional(), pinned: booleanParamSchema.optional(),
@ -195,7 +176,7 @@ const accountStatusesQuerySchema = z.object({
tagged: z.string().optional(), tagged: z.string().optional(),
}); });
const accountStatusesController: AppController = async (c) => { accountsController.get('/:pubkey{[0-9a-f]{64}}/statuses', paginationMiddleware, async (c) => {
const pubkey = c.req.param('pubkey'); const pubkey = c.req.param('pubkey');
const { since, until } = c.get('pagination'); const { since, until } = c.get('pagination');
const { pinned, limit, exclude_replies, tagged } = accountStatusesQuerySchema.parse(c.req.query()); const { pinned, limit, exclude_replies, tagged } = accountStatusesQuerySchema.parse(c.req.query());
@ -254,8 +235,9 @@ const accountStatusesController: AppController = async (c) => {
return renderStatus(event, { viewerPubkey }); return renderStatus(event, { viewerPubkey });
}), }),
); );
return paginated(c, events, statuses); return paginated(c, events, statuses);
}; });
const updateCredentialsSchema = z.object({ const updateCredentialsSchema = z.object({
display_name: z.string().optional(), display_name: z.string().optional(),
@ -271,8 +253,8 @@ const updateCredentialsSchema = z.object({
website: z.string().url().or(z.literal('')).optional(), website: z.string().url().or(z.literal('')).optional(),
}); });
const updateCredentialsController: AppController = async (c) => { accountsController.patch('/update_credentials', requireSigner, async (c) => {
const signer = c.get('signer')!; const signer = c.get('signer');
const pubkey = await signer.getPublicKey(); const pubkey = await 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);
@ -337,11 +319,11 @@ const updateCredentialsController: AppController = async (c) => {
} }
return c.json(account); return c.json(account);
}; });
/** https://docs.joinmastodon.org/methods/accounts/#follow */ // https://docs.joinmastodon.org/methods/accounts/#follow
const followController: AppController = async (c) => { accountsController.post('/:pubkey{[0-9a-f]{64}}/follow', requireSigner, async (c) => {
const sourcePubkey = await c.get('signer')?.getPublicKey()!; const sourcePubkey = await c.get('signer').getPublicKey();
const targetPubkey = c.req.param('pubkey'); const targetPubkey = c.req.param('pubkey');
await updateListEvent( await updateListEvent(
@ -354,10 +336,10 @@ const followController: AppController = async (c) => {
relationship.following = true; relationship.following = true;
return c.json(relationship); return c.json(relationship);
}; });
/** https://docs.joinmastodon.org/methods/accounts/#unfollow */ // https://docs.joinmastodon.org/methods/accounts/#unfollow
const unfollowController: AppController = async (c) => { accountsController.post('/:pubkey{[0-9a-f]{64}}/unfollow', requireSigner, async (c) => {
const sourcePubkey = await c.get('signer')?.getPublicKey()!; const sourcePubkey = await c.get('signer')?.getPublicKey()!;
const targetPubkey = c.req.param('pubkey'); const targetPubkey = c.req.param('pubkey');
@ -369,33 +351,33 @@ const unfollowController: AppController = async (c) => {
const relationship = await getRelationship(sourcePubkey, targetPubkey); const relationship = await getRelationship(sourcePubkey, targetPubkey);
return c.json(relationship); return c.json(relationship);
}; });
const followersController: AppController = (c) => { accountsController.get('/:pubkey{[0-9a-f]{64}}/followers', paginationMiddleware, (c) => {
const pubkey = c.req.param('pubkey'); const pubkey = c.req.param('pubkey');
const params = c.get('pagination'); const params = c.get('pagination');
return renderEventAccounts(c, [{ kinds: [3], '#p': [pubkey], ...params }]); return renderEventAccounts(c, [{ kinds: [3], '#p': [pubkey], ...params }]);
}; });
const followingController: AppController = async (c) => { accountsController.get('/:pubkey{[0-9a-f]{64}}/following', paginationMiddleware, async (c) => {
const pubkey = c.req.param('pubkey'); const pubkey = c.req.param('pubkey');
const pubkeys = await getFollowedPubkeys(pubkey); const pubkeys = await getFollowedPubkeys(pubkey);
return renderAccounts(c, [...pubkeys]); return renderAccounts(c, [...pubkeys]);
}; });
/** https://docs.joinmastodon.org/methods/accounts/#block */ // https://docs.joinmastodon.org/methods/accounts/#block
const blockController: AppController = (c) => { accountsController.post('/:pubkey{[0-9a-f]{64}}/block', (c) => {
return c.json({ error: 'Blocking is not supported by Nostr' }, 422); return c.json({ error: 'Blocking is not supported by Nostr' }, 422);
}; });
/** https://docs.joinmastodon.org/methods/accounts/#unblock */ // https://docs.joinmastodon.org/methods/accounts/#unblock
const unblockController: AppController = (c) => { accountsController.post('/:pubkey{[0-9a-f]{64}}/unblock', (c) => {
return c.json({ error: 'Blocking is not supported by Nostr' }, 422); return c.json({ error: 'Blocking is not supported by Nostr' }, 422);
}; });
/** https://docs.joinmastodon.org/methods/accounts/#mute */ // https://docs.joinmastodon.org/methods/accounts/#mute
const muteController: AppController = async (c) => { accountsController.post('/:pubkey{[0-9a-f]{64}}/mute', requireSigner, async (c) => {
const sourcePubkey = await c.get('signer')?.getPublicKey()!; const sourcePubkey = await c.get('signer').getPublicKey();
const targetPubkey = c.req.param('pubkey'); const targetPubkey = c.req.param('pubkey');
await updateListEvent( await updateListEvent(
@ -406,11 +388,11 @@ const muteController: AppController = async (c) => {
const relationship = await getRelationship(sourcePubkey, targetPubkey); const relationship = await getRelationship(sourcePubkey, targetPubkey);
return c.json(relationship); return c.json(relationship);
}; });
/** https://docs.joinmastodon.org/methods/accounts/#unmute */ // https://docs.joinmastodon.org/methods/accounts/#unmute
const unmuteController: AppController = async (c) => { accountsController.post('/:pubkey{[0-9a-f]{64}}/unmute', requireSigner, async (c) => {
const sourcePubkey = await c.get('signer')?.getPublicKey()!; const sourcePubkey = await c.get('signer').getPublicKey();
const targetPubkey = c.req.param('pubkey'); const targetPubkey = c.req.param('pubkey');
await updateListEvent( await updateListEvent(
@ -421,38 +403,11 @@ const unmuteController: AppController = async (c) => {
const relationship = await getRelationship(sourcePubkey, targetPubkey); const relationship = await getRelationship(sourcePubkey, targetPubkey);
return c.json(relationship); return c.json(relationship);
}; });
const favouritesController: AppController = async (c) => {
const pubkey = await c.get('signer')?.getPublicKey()!;
const params = c.get('pagination');
const { signal } = c.req.raw;
accountsController.get('/familiar_followers', requireSigner, async (c) => {
const store = await Storages.db(); const store = await Storages.db();
const signer = c.get('signer');
const events7 = await store.query(
[{ kinds: [7], authors: [pubkey], ...params }],
{ signal },
);
const ids = events7
.map((event) => event.tags.find((tag) => tag[0] === 'e')?.[1])
.filter((id): id is string => !!id);
const events1 = await store.query([{ kinds: [1], ids }], { signal })
.then((events) => hydrateEvents({ events, store, signal }));
const viewerPubkey = await c.get('signer')?.getPublicKey();
const statuses = await Promise.all(
events1.map((event) => renderStatus(event, { viewerPubkey })),
);
return paginated(c, events1, statuses);
};
const familiarFollowersController: AppController = async (c) => {
const store = await Storages.db();
const signer = c.get('signer')!;
const pubkey = await signer.getPublicKey(); const pubkey = await signer.getPublicKey();
const ids = z.array(z.string()).parse(c.req.queries('id[]')); const ids = z.array(z.string()).parse(c.req.queries('id[]'));
@ -470,7 +425,7 @@ const familiarFollowersController: AppController = async (c) => {
})); }));
return c.json(results); return c.json(results);
}; });
async function getRelationship(sourcePubkey: string, targetPubkey: string) { async function getRelationship(sourcePubkey: string, targetPubkey: string) {
const db = await Storages.db(); const db = await Storages.db();
@ -488,24 +443,3 @@ async function getRelationship(sourcePubkey: string, targetPubkey: string) {
event10000: sourceEvents.find((event) => event.kind === 10000 && event.pubkey === sourcePubkey), event10000: sourceEvents.find((event) => event.kind === 10000 && event.pubkey === sourcePubkey),
}); });
} }
export {
accountController,
accountLookupController,
accountSearchController,
accountStatusesController,
blockController,
createAccountController,
familiarFollowersController,
favouritesController,
followController,
followersController,
followingController,
muteController,
relationshipsController,
unblockController,
unfollowController,
unmuteController,
updateCredentialsController,
verifyCredentialsController,
};

View file

@ -0,0 +1,35 @@
import { Hono } from '@hono/hono';
import { paginationMiddleware } from '@/middleware/paginationMiddleware.ts';
import { requireSigner } from '@/middleware/requireSigner.ts';
import { Storages } from '@/storages.ts';
import { hydrateEvents } from '@/storages/hydrate.ts';
import { paginated } from '@/utils/api.ts';
import { renderStatus } from '@/views/mastodon/statuses.ts';
export const favouritesController = new Hono();
favouritesController.get('/', paginationMiddleware, requireSigner, async (c) => {
const pubkey = await c.get('signer').getPublicKey();
const params = c.get('pagination');
const { signal } = c.req.raw;
const store = await Storages.db();
const events7 = await store.query(
[{ kinds: [7], authors: [pubkey], ...params }],
{ signal },
);
const ids = events7
.map((event) => event.tags.find((tag) => tag[0] === 'e')?.[1])
.filter((id): id is string => !!id);
const events1 = await store.query([{ kinds: [1], ids }], { signal })
.then((events) => hydrateEvents({ events, store, signal }));
const statuses = await Promise.all(
events1.map((event) => renderStatus(event, { viewerPubkey: pubkey })),
);
return paginated(c, events1, statuses);
});

View file

@ -1,9 +1,9 @@
import { MiddlewareHandler } from '@hono/hono';
import { HTTPException } from '@hono/hono/http-exception'; import { HTTPException } from '@hono/hono/http-exception';
import { NostrSigner } from '@nostrify/nostrify';
import { AppMiddleware } from '@/app.ts';
/** Throw a 401 if a signer isn't set. */ /** Throw a 401 if a signer isn't set. */
export const requireSigner: AppMiddleware = async (c, next) => { export const requireSigner: MiddlewareHandler<{ Variables: { signer: NostrSigner } }> = async (c, next) => {
if (!c.get('signer')) { if (!c.get('signer')) {
throw new HTTPException(401, { message: 'No pubkey provided' }); throw new HTTPException(401, { message: 'No pubkey provided' });
} }

View file

@ -1,6 +1,7 @@
import { MiddlewareHandler } from '@hono/hono';
import { type NostrSigner, NUploader } from '@nostrify/nostrify';
import { BlossomUploader, NostrBuildUploader } from '@nostrify/nostrify/uploaders'; import { BlossomUploader, NostrBuildUploader } from '@nostrify/nostrify/uploaders';
import { AppMiddleware } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { DenoUploader } from '@/uploaders/DenoUploader.ts'; import { DenoUploader } from '@/uploaders/DenoUploader.ts';
import { IPFSUploader } from '@/uploaders/IPFSUploader.ts'; import { IPFSUploader } from '@/uploaders/IPFSUploader.ts';
@ -8,7 +9,8 @@ import { S3Uploader } from '@/uploaders/S3Uploader.ts';
import { fetchWorker } from '@/workers/fetch.ts'; import { fetchWorker } from '@/workers/fetch.ts';
/** Set an uploader for the user. */ /** Set an uploader for the user. */
export const uploaderMiddleware: AppMiddleware = async (c, next) => { export const uploaderMiddleware: MiddlewareHandler<{ Variables: { signer?: NostrSigner; uploader?: NUploader } }> =
async (c, next) => {
const signer = c.get('signer'); const signer = c.get('signer');
switch (Conf.uploader) { switch (Conf.uploader) {
@ -29,7 +31,10 @@ export const uploaderMiddleware: AppMiddleware = async (c, next) => {
); );
break; break;
case 'ipfs': case 'ipfs':
c.set('uploader', new IPFSUploader({ baseUrl: Conf.mediaDomain, apiUrl: Conf.ipfs.apiUrl, fetch: fetchWorker })); c.set(
'uploader',
new IPFSUploader({ baseUrl: Conf.mediaDomain, apiUrl: Conf.ipfs.apiUrl, fetch: fetchWorker }),
);
break; break;
case 'local': case 'local':
c.set('uploader', new DenoUploader({ baseUrl: Conf.mediaDomain, dir: Conf.uploadsDir })); c.set('uploader', new DenoUploader({ baseUrl: Conf.mediaDomain, dir: Conf.uploadsDir }));
@ -45,4 +50,4 @@ export const uploaderMiddleware: AppMiddleware = async (c, next) => {
} }
await next(); await next();
}; };

View file

@ -6,7 +6,6 @@ import { parseFormData } from 'formdata-helper';
import { EventTemplate } from 'nostr-tools'; import { EventTemplate } from 'nostr-tools';
import * as TypeFest from 'type-fest'; import * as TypeFest from 'type-fest';
import { type AppContext } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import * as pipeline from '@/pipeline.ts'; import * as pipeline from '@/pipeline.ts';
import { RelayError } from '@/RelayError.ts'; import { RelayError } from '@/RelayError.ts';
@ -21,7 +20,7 @@ const debug = Debug('ditto:api');
type EventStub = TypeFest.SetOptional<EventTemplate, 'content' | 'created_at' | 'tags'>; type EventStub = TypeFest.SetOptional<EventTemplate, 'content' | 'created_at' | 'tags'>;
/** Publish an event through the pipeline. */ /** Publish an event through the pipeline. */
async function createEvent(t: EventStub, c: AppContext): Promise<NostrEvent> { async function createEvent(t: EventStub, c: Context): Promise<NostrEvent> {
const signer = c.get('signer'); const signer = c.get('signer');
if (!signer) { if (!signer) {
@ -50,7 +49,7 @@ interface UpdateEventFilter extends NostrFilter {
async function updateEvent<E extends EventStub>( async function updateEvent<E extends EventStub>(
filter: UpdateEventFilter, filter: UpdateEventFilter,
fn: (prev: NostrEvent) => E | Promise<E>, fn: (prev: NostrEvent) => E | Promise<E>,
c: AppContext, c: Context,
): Promise<NostrEvent> { ): Promise<NostrEvent> {
const store = await Storages.db(); const store = await Storages.db();
@ -72,7 +71,7 @@ async function updateEvent<E extends EventStub>(
function updateListEvent( function updateListEvent(
filter: UpdateEventFilter, filter: UpdateEventFilter,
fn: (tags: string[][]) => string[][], fn: (tags: string[][]) => string[][],
c: AppContext, c: Context,
): Promise<NostrEvent> { ): Promise<NostrEvent> {
return updateEvent(filter, ({ content, tags }) => ({ return updateEvent(filter, ({ content, tags }) => ({
kind: filter.kinds[0], kind: filter.kinds[0],
@ -82,7 +81,7 @@ function updateListEvent(
} }
/** Publish an admin event through the pipeline. */ /** Publish an admin event through the pipeline. */
async function createAdminEvent(t: EventStub, c: AppContext): Promise<NostrEvent> { async function createAdminEvent(t: EventStub, c: Context): Promise<NostrEvent> {
const signer = new AdminSigner(); const signer = new AdminSigner();
const event = await signer.signEvent({ const event = await signer.signEvent({
@ -99,7 +98,7 @@ async function createAdminEvent(t: EventStub, c: AppContext): Promise<NostrEvent
function updateListAdminEvent( function updateListAdminEvent(
filter: UpdateEventFilter, filter: UpdateEventFilter,
fn: (tags: string[][]) => string[][], fn: (tags: string[][]) => string[][],
c: AppContext, c: Context,
): Promise<NostrEvent> { ): Promise<NostrEvent> {
return updateAdminEvent(filter, (prev) => ({ return updateAdminEvent(filter, (prev) => ({
kind: filter.kinds[0], kind: filter.kinds[0],
@ -112,22 +111,22 @@ function updateListAdminEvent(
async function updateAdminEvent<E extends EventStub>( async function updateAdminEvent<E extends EventStub>(
filter: UpdateEventFilter, filter: UpdateEventFilter,
fn: (prev: NostrEvent | undefined) => E, fn: (prev: NostrEvent | undefined) => E,
c: AppContext, c: Context,
): Promise<NostrEvent> { ): Promise<NostrEvent> {
const store = await Storages.db(); const store = await Storages.db();
const [prev] = await store.query([filter], { limit: 1, signal: c.req.raw.signal }); const [prev] = await store.query([filter], { limit: 1, signal: c.req.raw.signal });
return createAdminEvent(fn(prev), c); return createAdminEvent(fn(prev), c);
} }
function updateUser(pubkey: string, n: Record<string, boolean>, c: AppContext): Promise<NostrEvent> { function updateUser(pubkey: string, n: Record<string, boolean>, c: Context): Promise<NostrEvent> {
return updateNames(30382, pubkey, n, c); return updateNames(30382, pubkey, n, c);
} }
function updateEventInfo(id: string, n: Record<string, boolean>, c: AppContext): Promise<NostrEvent> { function updateEventInfo(id: string, n: Record<string, boolean>, c: Context): Promise<NostrEvent> {
return updateNames(30383, id, n, c); return updateNames(30383, id, n, c);
} }
async function updateNames(k: number, d: string, n: Record<string, boolean>, c: AppContext): Promise<NostrEvent> { async function updateNames(k: number, d: string, n: Record<string, boolean>, c: Context): Promise<NostrEvent> {
const signer = new AdminSigner(); const signer = new AdminSigner();
const admin = await signer.getPublicKey(); const admin = await signer.getPublicKey();
@ -158,7 +157,7 @@ async function updateNames(k: number, d: string, n: Record<string, boolean>, c:
} }
/** Push the event through the pipeline, rethrowing any RelayError. */ /** Push the event through the pipeline, rethrowing any RelayError. */
async function publishEvent(event: NostrEvent, c: AppContext): Promise<NostrEvent> { async function publishEvent(event: NostrEvent, c: Context): Promise<NostrEvent> {
debug('EVENT', event); debug('EVENT', event);
try { try {
await pipeline.handleEvent(event, c.req.raw.signal); await pipeline.handleEvent(event, c.req.raw.signal);
@ -209,7 +208,7 @@ type Entity = { id: string };
type HeaderRecord = Record<string, string | string[]>; type HeaderRecord = Record<string, string | string[]>;
/** Return results with pagination headers. Assumes chronological sorting of events. */ /** Return results with pagination headers. Assumes chronological sorting of events. */
function paginated(c: AppContext, events: NostrEvent[], entities: (Entity | undefined)[], headers: HeaderRecord = {}) { function paginated(c: Context, events: NostrEvent[], entities: (Entity | undefined)[], headers: HeaderRecord = {}) {
const link = buildLinkHeader(c.req.url, events); const link = buildLinkHeader(c.req.url, events);
if (link) { if (link) {
@ -240,7 +239,7 @@ function buildListLinkHeader(url: string, params: { offset: number; limit: numbe
/** paginate a list of tags. */ /** paginate a list of tags. */
function paginatedList( function paginatedList(
c: AppContext, c: Context,
params: { offset: number; limit: number }, params: { offset: number; limit: number },
entities: unknown[], entities: unknown[],
headers: HeaderRecord = {}, headers: HeaderRecord = {},

View file

@ -1,6 +1,7 @@
import { type Context } from '@hono/hono';
import { HTTPException } from '@hono/hono/http-exception'; import { HTTPException } from '@hono/hono/http-exception';
import { NUploader } from '@nostrify/nostrify';
import { AppContext } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { DittoUpload, dittoUploads } from '@/DittoUploads.ts'; import { DittoUpload, dittoUploads } from '@/DittoUploads.ts';
@ -11,7 +12,7 @@ interface FileMeta {
/** Upload a file, track it in the database, and return the resulting media object. */ /** Upload a file, track it in the database, and return the resulting media object. */
export async function uploadFile( export async function uploadFile(
c: AppContext, c: Context<{ Variables: { uploader?: NUploader } }>,
file: File, file: File,
meta: FileMeta, meta: FileMeta,
signal?: AbortSignal, signal?: AbortSignal,