Merge branch 'blocks' into 'main'

Support /api/v1/block, and /api/v1/accounts/:id/unblock

See merge request soapbox-pub/ditto!91
This commit is contained in:
Alex Gleason 2024-01-01 19:14:26 +00:00
commit 89bdc21caa
5 changed files with 80 additions and 14 deletions

View File

@ -30,10 +30,13 @@ import {
followersController, followersController,
followingController, followingController,
relationshipsController, relationshipsController,
unblockController,
unfollowController,
updateCredentialsController, updateCredentialsController,
verifyCredentialsController, verifyCredentialsController,
} from './controllers/api/accounts.ts'; } from './controllers/api/accounts.ts';
import { appCredentialsController, createAppController } from './controllers/api/apps.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 { emptyArrayController, emptyObjectController, notImplementedController } from './controllers/api/fallback.ts';
import { instanceController } from './controllers/api/instance.ts'; import { instanceController } from './controllers/api/instance.ts';
import { mediaController } from './controllers/api/media.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/search', accountSearchController);
app.get('/api/v1/accounts/lookup', accountLookupController); app.get('/api/v1/accounts/lookup', accountLookupController);
app.get('/api/v1/accounts/relationships', relationshipsController); 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}}/block', requirePubkey, blockController);
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', followController); 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}}/followers', followersController);
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/following', followingController); 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}}/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/notifications', requirePubkey, notificationsController);
app.get('/api/v1/favourites', requirePubkey, favouritesController); app.get('/api/v1/favourites', requirePubkey, favouritesController);
app.get('/api/v1/blocks', requirePubkey, blocksController);
app.post('/api/v1/pleroma/admin/config', requireRole('admin'), updateConfigController); 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/bookmarks', emptyArrayController);
app.get('/api/v1/custom_emojis', emptyArrayController); app.get('/api/v1/custom_emojis', emptyArrayController);
app.get('/api/v1/filters', emptyArrayController); app.get('/api/v1/filters', emptyArrayController);
app.get('/api/v1/blocks', emptyArrayController);
app.get('/api/v1/mutes', emptyArrayController); app.get('/api/v1/mutes', emptyArrayController);
app.get('/api/v1/domain_blocks', emptyArrayController); app.get('/api/v1/domain_blocks', emptyArrayController);
app.get('/api/v1/markers', emptyObjectController); app.get('/api/v1/markers', emptyObjectController);

View File

@ -7,12 +7,12 @@ import { type DittoFilter } from '@/filter.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 { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { addTag } from '@/tags.ts'; import { addTag, deleteTag } from '@/tags.ts';
import { uploadFile } from '@/upload.ts'; import { uploadFile } from '@/upload.ts';
import { lookupAccount, nostrNow } from '@/utils.ts'; import { lookupAccount, nostrNow } from '@/utils.ts';
import { paginated, paginationSchema, parseBody, updateListEvent } from '@/utils/web.ts'; import { paginated, paginationSchema, parseBody, updateListEvent } from '@/utils/web.ts';
import { createEvent } 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 { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
import { renderRelationship } from '@/views/mastodon/relationships.ts'; import { renderRelationship } from '@/views/mastodon/relationships.ts';
import { renderStatus } from '@/views/mastodon/statuses.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts';
@ -213,6 +213,7 @@ const updateCredentialsController: AppController = async (c) => {
return c.json(account); return c.json(account);
}; };
/** https://docs.joinmastodon.org/methods/accounts/#follow */
const followController: AppController = async (c) => { const followController: AppController = async (c) => {
const sourcePubkey = c.get('pubkey')!; const sourcePubkey = c.get('pubkey')!;
const targetPubkey = c.req.param('pubkey'); const targetPubkey = c.req.param('pubkey');
@ -227,6 +228,21 @@ const followController: AppController = async (c) => {
return c.json(relationship); 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 followersController: AppController = (c) => {
const pubkey = c.req.param('pubkey'); const pubkey = c.req.param('pubkey');
const params = paginationSchema.parse(c.req.query()); const params = paginationSchema.parse(c.req.query());
@ -236,16 +252,10 @@ const followersController: AppController = (c) => {
const followingController: AppController = async (c) => { const followingController: AppController = 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);
// 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));
}; };
/** https://docs.joinmastodon.org/methods/accounts/#block */
const blockController: AppController = async (c) => { const blockController: AppController = async (c) => {
const sourcePubkey = c.get('pubkey')!; const sourcePubkey = c.get('pubkey')!;
const targetPubkey = c.req.param('pubkey'); const targetPubkey = c.req.param('pubkey');
@ -260,6 +270,21 @@ const blockController: AppController = async (c) => {
return c.json(relationship); 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 favouritesController: AppController = async (c) => {
const pubkey = c.get('pubkey')!; const pubkey = c.get('pubkey')!;
const params = paginationSchema.parse(c.req.query()); const params = paginationSchema.parse(c.req.query());
@ -296,6 +321,8 @@ export {
followersController, followersController,
followingController, followingController,
relationshipsController, relationshipsController,
unblockController,
unfollowController,
updateCredentialsController, updateCredentialsController,
verifyCredentialsController, verifyCredentialsController,
}; };

View File

@ -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 };

View File

@ -64,6 +64,7 @@ function updateListEvent<K extends number>(
tags: fn(prev?.tags ?? []), tags: fn(prev?.tags ?? []),
}), c); }), c);
} }
/** Publish an admin event through the pipeline. */ /** Publish an admin event through the pipeline. */
async function createAdminEvent<K extends number>(t: EventStub<K>, c: AppContext): Promise<Event<K>> { async function createAdminEvent<K extends number>(t: EventStub<K>, c: AppContext): Promise<Event<K>> {
const event = await signAdminEvent({ const event = await signAdminEvent({

View File

@ -24,4 +24,15 @@ async function renderEventAccounts(c: AppContext, filters: Filter[]) {
return paginated(c, events, accounts); 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 };