diff --git a/src/app.ts b/src/app.ts index e637cb4..2c73369 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,10 +30,13 @@ import { followersController, followingController, relationshipsController, + unblockController, + unfollowController, updateCredentialsController, verifyCredentialsController, } from './controllers/api/accounts.ts'; import { appCredentialsController, createAppController } from './controllers/api/apps.ts'; +import { blocksController } from './controllers/api/blocks.ts'; import { emptyArrayController, emptyObjectController, notImplementedController } from './controllers/api/fallback.ts'; import { instanceController } from './controllers/api/instance.ts'; import { mediaController } from './controllers/api/media.ts'; @@ -136,8 +139,10 @@ app.patch( app.get('/api/v1/accounts/search', accountSearchController); app.get('/api/v1/accounts/lookup', accountLookupController); app.get('/api/v1/accounts/relationships', relationshipsController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/block', blockController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', followController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/block', requirePubkey, blockController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unblock', requirePubkey, unblockController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', requirePubkey, followController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unfollow', requirePubkey, 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); @@ -168,6 +173,7 @@ app.get('/api/v1/trends', cache({ cacheName: 'web', expires: Time.minutes(15) }) app.get('/api/v1/notifications', requirePubkey, notificationsController); app.get('/api/v1/favourites', requirePubkey, favouritesController); +app.get('/api/v1/blocks', requirePubkey, blocksController); app.post('/api/v1/pleroma/admin/config', requireRole('admin'), updateConfigController); @@ -175,7 +181,6 @@ app.post('/api/v1/pleroma/admin/config', requireRole('admin'), updateConfigContr app.get('/api/v1/bookmarks', emptyArrayController); app.get('/api/v1/custom_emojis', emptyArrayController); app.get('/api/v1/filters', emptyArrayController); -app.get('/api/v1/blocks', emptyArrayController); app.get('/api/v1/mutes', emptyArrayController); app.get('/api/v1/domain_blocks', emptyArrayController); app.get('/api/v1/markers', emptyObjectController); diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 9e1acc8..b25dbe4 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -7,12 +7,12 @@ import { type DittoFilter } from '@/filter.ts'; import { getAuthor, getFollowedPubkeys } from '@/queries.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; -import { addTag } from '@/tags.ts'; +import { addTag, deleteTag } from '@/tags.ts'; import { uploadFile } from '@/upload.ts'; import { lookupAccount, nostrNow } from '@/utils.ts'; import { paginated, paginationSchema, parseBody, updateListEvent } from '@/utils/web.ts'; import { createEvent } from '@/utils/web.ts'; -import { renderEventAccounts } from '@/views.ts'; +import { renderAccounts, renderEventAccounts } from '@/views.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { renderRelationship } from '@/views/mastodon/relationships.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; @@ -213,6 +213,7 @@ const updateCredentialsController: AppController = async (c) => { return c.json(account); }; +/** https://docs.joinmastodon.org/methods/accounts/#follow */ const followController: AppController = async (c) => { const sourcePubkey = c.get('pubkey')!; const targetPubkey = c.req.param('pubkey'); @@ -227,6 +228,21 @@ const followController: AppController = async (c) => { return c.json(relationship); }; +/** https://docs.joinmastodon.org/methods/accounts/#unfollow */ +const unfollowController: AppController = async (c) => { + const sourcePubkey = c.get('pubkey')!; + const targetPubkey = c.req.param('pubkey'); + + await updateListEvent( + { kinds: [3], authors: [sourcePubkey] }, + (tags) => deleteTag(tags, ['p', targetPubkey]), + c, + ); + + const relationship = await renderRelationship(sourcePubkey, targetPubkey); + return c.json(relationship); +}; + const followersController: AppController = (c) => { const pubkey = c.req.param('pubkey'); const params = paginationSchema.parse(c.req.query()); @@ -236,16 +252,10 @@ const followersController: AppController = (c) => { const followingController: AppController = async (c) => { const pubkey = c.req.param('pubkey'); const pubkeys = await getFollowedPubkeys(pubkey); - - // TODO: pagination by offset. - const accounts = await Promise.all(pubkeys.map(async (pubkey) => { - const event = await getAuthor(pubkey); - return event ? await renderAccount(event) : undefined; - })); - - return c.json(accounts.filter(Boolean)); + return renderAccounts(c, pubkeys); }; +/** https://docs.joinmastodon.org/methods/accounts/#block */ const blockController: AppController = async (c) => { const sourcePubkey = c.get('pubkey')!; const targetPubkey = c.req.param('pubkey'); @@ -260,6 +270,21 @@ const blockController: AppController = async (c) => { return c.json(relationship); }; +/** https://docs.joinmastodon.org/methods/accounts/#unblock */ +const unblockController: AppController = async (c) => { + const sourcePubkey = c.get('pubkey')!; + const targetPubkey = c.req.param('pubkey'); + + await updateListEvent( + { kinds: [10000], authors: [sourcePubkey] }, + (tags) => deleteTag(tags, ['p', targetPubkey]), + c, + ); + + const relationship = await renderRelationship(sourcePubkey, targetPubkey); + return c.json(relationship); +}; + const favouritesController: AppController = async (c) => { const pubkey = c.get('pubkey')!; const params = paginationSchema.parse(c.req.query()); @@ -296,6 +321,8 @@ export { followersController, followingController, relationshipsController, + unblockController, + unfollowController, updateCredentialsController, verifyCredentialsController, }; diff --git a/src/controllers/api/blocks.ts b/src/controllers/api/blocks.ts new file mode 100644 index 0000000..2ff4f3e --- /dev/null +++ b/src/controllers/api/blocks.ts @@ -0,0 +1,22 @@ +import { type AppController } from '@/app.ts'; +import { eventsDB } from '@/db/events.ts'; +import { getTagSet } from '@/tags.ts'; +import { renderAccounts } from '@/views.ts'; + +/** https://docs.joinmastodon.org/methods/blocks/#get */ +const blocksController: AppController = async (c) => { + const pubkey = c.get('pubkey')!; + + const [event10000] = await eventsDB.getEvents([ + { kinds: [10000], authors: [pubkey], limit: 1 }, + ]); + + if (event10000) { + const pubkeys = getTagSet(event10000.tags, 'p'); + return renderAccounts(c, [...pubkeys].reverse()); + } else { + return c.json([]); + } +}; + +export { blocksController }; diff --git a/src/utils/web.ts b/src/utils/web.ts index 1e7be07..837c4be 100644 --- a/src/utils/web.ts +++ b/src/utils/web.ts @@ -64,6 +64,7 @@ function updateListEvent( tags: fn(prev?.tags ?? []), }), c); } + /** Publish an admin event through the pipeline. */ async function createAdminEvent(t: EventStub, c: AppContext): Promise> { const event = await signAdminEvent({ diff --git a/src/views.ts b/src/views.ts index 95a998e..5988689 100644 --- a/src/views.ts +++ b/src/views.ts @@ -24,4 +24,15 @@ async function renderEventAccounts(c: AppContext, filters: Filter[]) { return paginated(c, events, accounts); } -export { renderEventAccounts }; +async function renderAccounts(c: AppContext, pubkeys: string[]) { + // TODO: pagination by offset. + // FIXME: this is very inefficient! + const accounts = await Promise.all(pubkeys.map(async (pubkey) => { + const event = await getAuthor(pubkey); + return event ? await renderAccount(event) : undefined; + })); + + return c.json(accounts.filter(Boolean)); +} + +export { renderAccounts, renderEventAccounts };