firefish/packages/backend/src/server/activitypub/followers.ts
sup39 b71da18b03
refactor (backend): port fetch-meta to backend-rs
Co-authored-by: naskya <m@naskya.net>
2024-04-14 20:16:22 +09:00

120 lines
3.1 KiB
TypeScript

import { 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, UserProfiles } from "@/models/index.js";
import type { Following } from "@/models/entities/following.js";
import { checkFetch } from "@/remote/activitypub/check-fetch.js";
import { fetchMeta } from "backend-rs";
import { setResponseType } from "../activitypub.js";
import type { FindOptionsWhere } from "typeorm";
import type Router from "@koa/router";
export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req);
if (verify !== 200) {
ctx.status = verify;
return;
}
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;
}
//#region Check ff visibility
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === "private") {
ctx.status = 403;
ctx.set("Cache-Control", "public, max-age=30");
return;
} else if (profile.ffVisibility === "followers") {
ctx.status = 403;
ctx.set("Cache-Control", "public, max-age=30");
return;
}
//#endregion
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);
}
const meta = await fetchMeta(true);
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {
ctx.set("Cache-Control", "public, max-age=180");
}
};