Files
indiekit-endpoint-activitypub/lib/controllers/followers.js
Ricardo 9a61145d97 feat: FEP-8fcf/fe34 compliance, custom emoji, manual follow approval (v2.13.0)
- 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
2026-03-17 08:21:36 +01:00

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