mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
- FEP-8fcf: add syncCollection to Undo(Announce) sendActivity - FEP-fe34: centralized lookupWithSecurity() helper with crossOrigin: "ignore" on all 23 lookupObject call sites - Custom emoji: replaceCustomEmoji() renders :shortcode: as inline <img> in content and actor display names - Manual follow approval: profile toggle, ap_pending_follows collection, approve/reject controllers with federation, pending tab on followers page, follow_request notification type - Coverage audit updated to v2.12.x (overall ~70% → ~82%) Confab-Link: http://localhost:8080/sessions/1f1e729b-0087-499e-a991-f36f46211fe4
112 lines
3.4 KiB
JavaScript
112 lines
3.4 KiB
JavaScript
/**
|
|
* Followers list controller — paginated list of accounts following this actor,
|
|
* with pending follow requests tab when manual approval is enabled.
|
|
*/
|
|
import { getToken } from "../csrf.js";
|
|
|
|
const PAGE_SIZE = 20;
|
|
|
|
export function followersController(mountPath) {
|
|
return async (request, response, next) => {
|
|
try {
|
|
const { application } = request.app.locals;
|
|
const collection = application?.collections?.get("ap_followers");
|
|
const pendingCol = application?.collections?.get("ap_pending_follows");
|
|
|
|
const tab = request.query.tab || "followers";
|
|
|
|
if (!collection) {
|
|
return response.render("activitypub-followers", {
|
|
title: response.locals.__("activitypub.followers"),
|
|
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
|
|
followers: [],
|
|
followerCount: 0,
|
|
pendingFollows: [],
|
|
pendingCount: 0,
|
|
tab,
|
|
mountPath,
|
|
csrfToken: getToken(request),
|
|
});
|
|
}
|
|
|
|
const page = Math.max(1, Number.parseInt(request.query.page, 10) || 1);
|
|
|
|
// Count pending follow requests
|
|
const pendingCount = pendingCol
|
|
? await pendingCol.countDocuments()
|
|
: 0;
|
|
|
|
if (tab === "pending") {
|
|
// Show pending follow requests
|
|
const totalPages = Math.ceil(pendingCount / PAGE_SIZE);
|
|
const pendingFollows = pendingCol
|
|
? await pendingCol
|
|
.find()
|
|
.sort({ requestedAt: -1 })
|
|
.skip((page - 1) * PAGE_SIZE)
|
|
.limit(PAGE_SIZE)
|
|
.toArray()
|
|
: [];
|
|
|
|
const cursor = buildCursor(page, totalPages, mountPath + "/admin/followers?tab=pending");
|
|
|
|
return response.render("activitypub-followers", {
|
|
title: response.locals.__("activitypub.followers"),
|
|
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
|
|
followers: [],
|
|
followerCount: await collection.countDocuments(),
|
|
pendingFollows,
|
|
pendingCount,
|
|
tab,
|
|
mountPath,
|
|
cursor,
|
|
csrfToken: getToken(request),
|
|
});
|
|
}
|
|
|
|
// Show accepted followers (default)
|
|
const totalCount = await collection.countDocuments();
|
|
const totalPages = Math.ceil(totalCount / PAGE_SIZE);
|
|
|
|
const followers = await collection
|
|
.find()
|
|
.sort({ followedAt: -1 })
|
|
.skip((page - 1) * PAGE_SIZE)
|
|
.limit(PAGE_SIZE)
|
|
.toArray();
|
|
|
|
const cursor = buildCursor(page, totalPages, mountPath + "/admin/followers");
|
|
|
|
response.render("activitypub-followers", {
|
|
title: `${totalCount} ${response.locals.__("activitypub.followers")}`,
|
|
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
|
|
followers,
|
|
followerCount: totalCount,
|
|
pendingFollows: [],
|
|
pendingCount,
|
|
tab,
|
|
mountPath,
|
|
cursor,
|
|
csrfToken: getToken(request),
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
}
|
|
|
|
function buildCursor(page, totalPages, basePath) {
|
|
if (totalPages <= 1) return null;
|
|
|
|
const separator = basePath.includes("?") ? "&" : "?";
|
|
|
|
return {
|
|
previous: page > 1
|
|
? { href: `${basePath}${separator}page=${page - 1}` }
|
|
: undefined,
|
|
next: page < totalPages
|
|
? { href: `${basePath}${separator}page=${page + 1}` }
|
|
: undefined,
|
|
};
|
|
}
|