FoundKey/packages/backend/src/server/activitypub/followers.ts
Hélène f89a374e5f BREAKING: activitypub: validate fetch signatures
Enforces HTTP signatures on object fetches, and rejects fetches from blocked
instances. This should mean proper and full blocking of remote instances.

This is now default behavior, which makes it a breaking change.
To disable it (mostly for development purposes), "meta"."allowUnsignedFetches"
can be set to true. It is not the default for development environments as it
is important to have as close as possible behavior to real environments for
ActivityPub development.

Co-authored-by: nullobsi <me@nullob.si>
Co-authored-by: Norm <normandy@biribiri.dev>
Changelog: Added
2023-06-23 16:30:09 +02:00

88 lines
2.4 KiB
TypeScript

import Router from '@koa/router';
import { FindOptionsWhere, IsNull, LessThan } from 'typeorm';
import config from '@/config/index.js';
import * as url from '@/prelude/url.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
import { Users, Followings } from '@/models/index.js';
import { Following } from '@/models/entities/following.js';
import { setResponseType } from '../activitypub.js';
export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user;
const cursor = ctx.request.query.cursor;
if (cursor != null && typeof cursor !== 'string') {
ctx.status = 400;
return;
}
const page = ctx.request.query.page === 'true';
const user = await Users.findOneBy({
id: userId,
host: IsNull(),
});
if (user == null) {
ctx.status = 404;
return;
}
const ffVisible = await Users.areFollowersVisibleTo(user, null);
if (!ffVisible) {
ctx.status = 403;
ctx.set('Cache-Control', 'public, max-age=30');
return;
}
const limit = 10;
const partOf = `${config.url}/users/${userId}/followers`;
if (page) {
const query = {
followeeId: user.id,
} as FindOptionsWhere<Following>;
// カーソルが指定されている場合
if (cursor) {
query.id = LessThan(cursor);
}
// Get followers
const followings = await Followings.find({
where: query,
take: limit + 1,
order: { id: -1 },
});
// 「次のページ」があるかどうか
const inStock = followings.length === limit + 1;
if (inStock) followings.pop();
const renderedFollowers = await Promise.all(followings.map(following => renderFollowUser(following.followerId)));
const rendered = renderOrderedCollectionPage(
`${partOf}?${url.query({
page: 'true',
cursor,
})}`,
user.followersCount, renderedFollowers, partOf,
undefined,
inStock ? `${partOf}?${url.query({
page: 'true',
cursor: followings[followings.length - 1].id,
})}` : undefined,
);
ctx.body = renderActivity(rendered);
setResponseType(ctx);
} else {
// index page
const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
ctx.body = renderActivity(rendered);
setResponseType(ctx);
}
};