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 {
accountController,
accountLookupController,
accountSearchController,
accountStatusesController,
blockController,
createAccountController,
familiarFollowersController,
favouritesController,
followController,
followersController,
followingController,
muteController,
relationshipsController,
unblockController,
unfollowController,
unmuteController,
updateCredentialsController,
verifyCredentialsController,
} from '@/controllers/api/accounts.ts';
import { accountsController } from '@/controllers/api/accounts.ts';
import {
adminAccountsController,
adminActionController,
@ -50,6 +31,7 @@ import {
updateZapSplitsController,
} from '@/controllers/api/ditto.ts';
import { emptyArrayController, emptyObjectController, notImplementedController } from '@/controllers/api/fallback.ts';
import { favouritesController } from '@/controllers/api/favourites.ts';
import {
instanceDescriptionController,
instanceV1Controller,
@ -200,23 +182,7 @@ app.post('/oauth/revoke', emptyObjectController);
app.post('/oauth/authorize', oauthAuthorizeController);
app.get('/oauth/authorize', oauthController);
app.post('/api/v1/accounts', requireProof({ pow: 20 }), createAccountController);
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.route('/api/v1/accounts', accountsController);
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);
@ -268,7 +234,7 @@ app.get('/api/v1/suggestions', suggestionsV1Controller);
app.get('/api/v2/suggestions', suggestionsV2Controller);
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/blocks', requireSigner, blocksController);
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 { nip19 } from 'nostr-tools';
import { z } from 'zod';
import { type AppController } from '@/app.ts';
import { Conf } from '@/config.ts';
import { getAuthor, getFollowedPubkeys } from '@/queries.ts';
import { booleanParamSchema, fileSchema } from '@/schema.ts';
import { Storages } from '@/storages.ts';
import { uploadFile } from '@/utils/upload.ts';
import { nostrNow } from '@/utils.ts';
import { createEvent, paginated, parseBody, updateEvent, updateListEvent } from '@/utils/api.ts';
import { extractIdentifier, lookupAccount, lookupPubkey } from '@/utils/lookup.ts';
import { renderAccounts, renderEventAccounts, renderStatuses } from '@/views.ts';
@ -19,34 +17,17 @@ import { hydrateEvents } from '@/storages/hydrate.ts';
import { bech32ToPubkey } from '@/utils.ts';
import { addTag, deleteTag, findReplyTag, getTagSet } from '@/utils/tags.ts';
import { getPubkeysBySearch } from '@/utils/search.ts';
import { requireSigner } from '@/middleware/requireSigner.ts';
import { paginationMiddleware } from '@/middleware/paginationMiddleware.ts';
const usernameSchema = z
.string().min(1).max(30)
.regex(/^[a-z0-9_]+$/i)
.refine((username) => !Conf.forbiddenUsernames.includes(username), 'Username is reserved.');
export const accountsController = new Hono();
const createAccountSchema = z.object({
username: usernameSchema,
accountsController.post('/', (c) => {
return c.json({ error: 'Please create an account on the web first' }, 422);
});
const createAccountController: AppController = async (c) => {
const pubkey = await c.get('signer')?.getPublicKey()!;
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')!;
accountsController.get('/verify_credentials', requireSigner, async (c) => {
const signer = c.get('signer');
const pubkey = await signer.getPublicKey();
const store = await Storages.db();
@ -74,9 +55,9 @@ const verifyCredentialsController: AppController = async (c) => {
: await accountFromPubkey(pubkey, { withSource: true, settingsStore });
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 event = await getAuthor(pubkey);
@ -85,9 +66,9 @@ const accountController: AppController = async (c) => {
} else {
return c.json(await accountFromPubkey(pubkey));
}
};
});
const accountLookupController: AppController = async (c) => {
accountsController.get('/lookup', async (c) => {
const acct = c.req.query('acct');
if (!acct) {
@ -104,7 +85,7 @@ const accountLookupController: AppController = async (c) => {
} catch {
return c.json({ error: 'Could not find user.' }, 404);
}
};
});
const accountSearchQuerySchema = z.object({
q: z.string().transform(decodeURIComponent),
@ -112,7 +93,7 @@ const accountSearchQuerySchema = z.object({
following: z.boolean().default(false),
});
const accountSearchController: AppController = async (c) => {
accountsController.get('/search', paginationMiddleware, async (c) => {
const { signal } = c.req.raw;
const { limit } = c.get('pagination');
const kysely = await Storages.kysely();
@ -155,10 +136,10 @@ const accountSearchController: AppController = async (c) => {
);
return c.json(accounts);
};
});
const relationshipsController: AppController = async (c) => {
const pubkey = await c.get('signer')?.getPublicKey()!;
accountsController.get('/relationships', requireSigner, async (c) => {
const pubkey = await c.get('signer').getPublicKey();
const ids = z.array(z.string()).safeParse(c.req.queries('id[]'));
if (!ids.success) {
@ -186,7 +167,7 @@ const relationshipsController: AppController = async (c) => {
);
return c.json(result);
};
});
const accountStatusesQuerySchema = z.object({
pinned: booleanParamSchema.optional(),
@ -195,7 +176,7 @@ const accountStatusesQuerySchema = z.object({
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 { since, until } = c.get('pagination');
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 paginated(c, events, statuses);
};
});
const updateCredentialsSchema = z.object({
display_name: z.string().optional(),
@ -271,8 +253,8 @@ const updateCredentialsSchema = z.object({
website: z.string().url().or(z.literal('')).optional(),
});
const updateCredentialsController: AppController = async (c) => {
const signer = c.get('signer')!;
accountsController.patch('/update_credentials', requireSigner, async (c) => {
const signer = c.get('signer');
const pubkey = await signer.getPublicKey();
const body = await parseBody(c.req.raw);
const result = updateCredentialsSchema.safeParse(body);
@ -337,11 +319,11 @@ const updateCredentialsController: AppController = async (c) => {
}
return c.json(account);
};
});
/** https://docs.joinmastodon.org/methods/accounts/#follow */
const followController: AppController = async (c) => {
const sourcePubkey = await c.get('signer')?.getPublicKey()!;
// https://docs.joinmastodon.org/methods/accounts/#follow
accountsController.post('/:pubkey{[0-9a-f]{64}}/follow', requireSigner, async (c) => {
const sourcePubkey = await c.get('signer').getPublicKey();
const targetPubkey = c.req.param('pubkey');
await updateListEvent(
@ -354,10 +336,10 @@ const followController: AppController = async (c) => {
relationship.following = true;
return c.json(relationship);
};
});
/** https://docs.joinmastodon.org/methods/accounts/#unfollow */
const unfollowController: AppController = async (c) => {
// https://docs.joinmastodon.org/methods/accounts/#unfollow
accountsController.post('/:pubkey{[0-9a-f]{64}}/unfollow', requireSigner, async (c) => {
const sourcePubkey = await c.get('signer')?.getPublicKey()!;
const targetPubkey = c.req.param('pubkey');
@ -369,33 +351,33 @@ const unfollowController: AppController = async (c) => {
const relationship = await getRelationship(sourcePubkey, targetPubkey);
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 params = c.get('pagination');
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 pubkeys = await getFollowedPubkeys(pubkey);
return renderAccounts(c, [...pubkeys]);
};
});
/** https://docs.joinmastodon.org/methods/accounts/#block */
const blockController: AppController = (c) => {
// https://docs.joinmastodon.org/methods/accounts/#block
accountsController.post('/:pubkey{[0-9a-f]{64}}/block', (c) => {
return c.json({ error: 'Blocking is not supported by Nostr' }, 422);
};
});
/** https://docs.joinmastodon.org/methods/accounts/#unblock */
const unblockController: AppController = (c) => {
// https://docs.joinmastodon.org/methods/accounts/#unblock
accountsController.post('/:pubkey{[0-9a-f]{64}}/unblock', (c) => {
return c.json({ error: 'Blocking is not supported by Nostr' }, 422);
};
});
/** https://docs.joinmastodon.org/methods/accounts/#mute */
const muteController: AppController = async (c) => {
const sourcePubkey = await c.get('signer')?.getPublicKey()!;
// https://docs.joinmastodon.org/methods/accounts/#mute
accountsController.post('/:pubkey{[0-9a-f]{64}}/mute', requireSigner, async (c) => {
const sourcePubkey = await c.get('signer').getPublicKey();
const targetPubkey = c.req.param('pubkey');
await updateListEvent(
@ -406,11 +388,11 @@ const muteController: AppController = async (c) => {
const relationship = await getRelationship(sourcePubkey, targetPubkey);
return c.json(relationship);
};
});
/** https://docs.joinmastodon.org/methods/accounts/#unmute */
const unmuteController: AppController = async (c) => {
const sourcePubkey = await c.get('signer')?.getPublicKey()!;
// https://docs.joinmastodon.org/methods/accounts/#unmute
accountsController.post('/:pubkey{[0-9a-f]{64}}/unmute', requireSigner, async (c) => {
const sourcePubkey = await c.get('signer').getPublicKey();
const targetPubkey = c.req.param('pubkey');
await updateListEvent(
@ -421,38 +403,11 @@ const unmuteController: AppController = async (c) => {
const relationship = await getRelationship(sourcePubkey, targetPubkey);
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 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 signer = c.get('signer');
const pubkey = await signer.getPublicKey();
const ids = z.array(z.string()).parse(c.req.queries('id[]'));
@ -470,7 +425,7 @@ const familiarFollowersController: AppController = async (c) => {
}));
return c.json(results);
};
});
async function getRelationship(sourcePubkey: string, targetPubkey: string) {
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),
});
}
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 { AppMiddleware } from '@/app.ts';
import { NostrSigner } from '@nostrify/nostrify';
/** 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')) {
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 { AppMiddleware } from '@/app.ts';
import { Conf } from '@/config.ts';
import { DenoUploader } from '@/uploaders/DenoUploader.ts';
import { IPFSUploader } from '@/uploaders/IPFSUploader.ts';
@ -8,41 +9,45 @@ import { S3Uploader } from '@/uploaders/S3Uploader.ts';
import { fetchWorker } from '@/workers/fetch.ts';
/** Set an uploader for the user. */
export const uploaderMiddleware: AppMiddleware = async (c, next) => {
const signer = c.get('signer');
export const uploaderMiddleware: MiddlewareHandler<{ Variables: { signer?: NostrSigner; uploader?: NUploader } }> =
async (c, next) => {
const signer = c.get('signer');
switch (Conf.uploader) {
case 's3':
c.set(
'uploader',
new S3Uploader({
accessKey: Conf.s3.accessKey,
bucket: Conf.s3.bucket,
endPoint: Conf.s3.endPoint!,
pathStyle: Conf.s3.pathStyle,
port: Conf.s3.port,
region: Conf.s3.region!,
secretKey: Conf.s3.secretKey,
sessionToken: Conf.s3.sessionToken,
useSSL: Conf.s3.useSSL,
}),
);
break;
case 'ipfs':
c.set('uploader', new IPFSUploader({ baseUrl: Conf.mediaDomain, apiUrl: Conf.ipfs.apiUrl, fetch: fetchWorker }));
break;
case 'local':
c.set('uploader', new DenoUploader({ baseUrl: Conf.mediaDomain, dir: Conf.uploadsDir }));
break;
case 'nostrbuild':
c.set('uploader', new NostrBuildUploader({ endpoint: Conf.nostrbuildEndpoint, signer, fetch: fetchWorker }));
break;
case 'blossom':
if (signer) {
c.set('uploader', new BlossomUploader({ servers: Conf.blossomServers, signer, fetch: fetchWorker }));
}
break;
}
switch (Conf.uploader) {
case 's3':
c.set(
'uploader',
new S3Uploader({
accessKey: Conf.s3.accessKey,
bucket: Conf.s3.bucket,
endPoint: Conf.s3.endPoint!,
pathStyle: Conf.s3.pathStyle,
port: Conf.s3.port,
region: Conf.s3.region!,
secretKey: Conf.s3.secretKey,
sessionToken: Conf.s3.sessionToken,
useSSL: Conf.s3.useSSL,
}),
);
break;
case 'ipfs':
c.set(
'uploader',
new IPFSUploader({ baseUrl: Conf.mediaDomain, apiUrl: Conf.ipfs.apiUrl, fetch: fetchWorker }),
);
break;
case 'local':
c.set('uploader', new DenoUploader({ baseUrl: Conf.mediaDomain, dir: Conf.uploadsDir }));
break;
case 'nostrbuild':
c.set('uploader', new NostrBuildUploader({ endpoint: Conf.nostrbuildEndpoint, signer, fetch: fetchWorker }));
break;
case 'blossom':
if (signer) {
c.set('uploader', new BlossomUploader({ servers: Conf.blossomServers, signer, fetch: fetchWorker }));
}
break;
}
await next();
};
await next();
};

View file

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

View file

@ -1,6 +1,7 @@
import { type Context } from '@hono/hono';
import { HTTPException } from '@hono/hono/http-exception';
import { NUploader } from '@nostrify/nostrify';
import { AppContext } from '@/app.ts';
import { Conf } from '@/config.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. */
export async function uploadFile(
c: AppContext,
c: Context<{ Variables: { uploader?: NUploader } }>,
file: File,
meta: FileMeta,
signal?: AbortSignal,