mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Refactor accountsController into a separate route
This commit is contained in:
parent
260340c58e
commit
7d877b5a37
7 changed files with 151 additions and 211 deletions
42
src/app.ts
42
src/app.ts
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
|
||||||
35
src/controllers/api/favourites.ts
Normal file
35
src/controllers/api/favourites.ts
Normal 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);
|
||||||
|
});
|
||||||
|
|
@ -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' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,41 +9,45 @@ 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 } }> =
|
||||||
const signer = c.get('signer');
|
async (c, next) => {
|
||||||
|
const signer = c.get('signer');
|
||||||
|
|
||||||
switch (Conf.uploader) {
|
switch (Conf.uploader) {
|
||||||
case 's3':
|
case 's3':
|
||||||
c.set(
|
c.set(
|
||||||
'uploader',
|
'uploader',
|
||||||
new S3Uploader({
|
new S3Uploader({
|
||||||
accessKey: Conf.s3.accessKey,
|
accessKey: Conf.s3.accessKey,
|
||||||
bucket: Conf.s3.bucket,
|
bucket: Conf.s3.bucket,
|
||||||
endPoint: Conf.s3.endPoint!,
|
endPoint: Conf.s3.endPoint!,
|
||||||
pathStyle: Conf.s3.pathStyle,
|
pathStyle: Conf.s3.pathStyle,
|
||||||
port: Conf.s3.port,
|
port: Conf.s3.port,
|
||||||
region: Conf.s3.region!,
|
region: Conf.s3.region!,
|
||||||
secretKey: Conf.s3.secretKey,
|
secretKey: Conf.s3.secretKey,
|
||||||
sessionToken: Conf.s3.sessionToken,
|
sessionToken: Conf.s3.sessionToken,
|
||||||
useSSL: Conf.s3.useSSL,
|
useSSL: Conf.s3.useSSL,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'ipfs':
|
case 'ipfs':
|
||||||
c.set('uploader', new IPFSUploader({ baseUrl: Conf.mediaDomain, apiUrl: Conf.ipfs.apiUrl, fetch: fetchWorker }));
|
c.set(
|
||||||
break;
|
'uploader',
|
||||||
case 'local':
|
new IPFSUploader({ baseUrl: Conf.mediaDomain, apiUrl: Conf.ipfs.apiUrl, fetch: fetchWorker }),
|
||||||
c.set('uploader', new DenoUploader({ baseUrl: Conf.mediaDomain, dir: Conf.uploadsDir }));
|
);
|
||||||
break;
|
break;
|
||||||
case 'nostrbuild':
|
case 'local':
|
||||||
c.set('uploader', new NostrBuildUploader({ endpoint: Conf.nostrbuildEndpoint, signer, fetch: fetchWorker }));
|
c.set('uploader', new DenoUploader({ baseUrl: Conf.mediaDomain, dir: Conf.uploadsDir }));
|
||||||
break;
|
break;
|
||||||
case 'blossom':
|
case 'nostrbuild':
|
||||||
if (signer) {
|
c.set('uploader', new NostrBuildUploader({ endpoint: Conf.nostrbuildEndpoint, signer, fetch: fetchWorker }));
|
||||||
c.set('uploader', new BlossomUploader({ servers: Conf.blossomServers, signer, fetch: fetchWorker }));
|
break;
|
||||||
}
|
case 'blossom':
|
||||||
break;
|
if (signer) {
|
||||||
}
|
c.set('uploader', new BlossomUploader({ servers: Conf.blossomServers, signer, fetch: fetchWorker }));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue