diff --git a/lib/controllers/federation-mgmt.js b/lib/controllers/federation-mgmt.js
index bda6a73..36ce931 100644
--- a/lib/controllers/federation-mgmt.js
+++ b/lib/controllers/federation-mgmt.js
@@ -41,12 +41,16 @@ export function federationMgmtController(mountPath, plugin) {
const redisUrl = plugin.options.redisUrl || "";
- // Parallel: collection stats + posts + recent activities
- const [collectionStats, postsResult, recentActivities] =
+ // Parallel: collection stats + posts + recent activities + moderation data
+ const pluginCollections = plugin._collections || {};
+ const [collectionStats, postsResult, recentActivities, blockedServers, blockedAccounts, mutedAccounts] =
await Promise.all([
getCollectionStats(collections, { redisUrl }),
getPaginatedPosts(collections, request.query.page),
getRecentActivities(collections),
+ pluginCollections.ap_blocked_servers?.find({}).sort({ blockedAt: -1 }).toArray() || [],
+ pluginCollections.ap_blocked?.find({}).sort({ blockedAt: -1 }).toArray() || [],
+ pluginCollections.ap_muted?.find({}).sort({ mutedAt: -1 }).toArray() || [],
]);
const csrfToken = getToken(request.session);
@@ -62,6 +66,9 @@ export function federationMgmtController(mountPath, plugin) {
posts: postsResult.posts,
cursor: postsResult.cursor,
recentActivities,
+ blockedServers: blockedServers || [],
+ blockedAccounts: blockedAccounts || [],
+ mutedAccounts: mutedAccounts || [],
csrfToken,
mountPath,
publicationUrl: plugin._publicationUrl,
diff --git a/lib/mastodon/routes/accounts.js b/lib/mastodon/routes/accounts.js
index 66239cb..c9d3c1d 100644
--- a/lib/mastodon/routes/accounts.js
+++ b/lib/mastodon/routes/accounts.js
@@ -170,11 +170,12 @@ router.get("/api/v1/accounts/relationships", async (req, res, next) => {
const collections = req.app.locals.mastodonCollections;
- const [followers, following, blocked, muted] = await Promise.all([
+ const [followers, following, blocked, muted, blockedServers] = await Promise.all([
collections.ap_followers.find({}).toArray(),
collections.ap_following.find({}).toArray(),
collections.ap_blocked.find({}).toArray(),
collections.ap_muted.find({}).toArray(),
+ collections.ap_blocked_servers?.find({}).toArray() || [],
]);
const followerIds = new Set(followers.map((f) => remoteActorId(f.actorUrl)));
@@ -182,6 +183,21 @@ router.get("/api/v1/accounts/relationships", async (req, res, next) => {
const blockedIds = new Set(blocked.map((b) => remoteActorId(b.url)));
const mutedIds = new Set(muted.filter((m) => m.url).map((m) => remoteActorId(m.url)));
+ // Build domain-blocked actor ID set by checking known actors against blocked server hostnames
+ const blockedDomains = new Set(blockedServers.map((s) => s.hostname).filter(Boolean));
+ const domainBlockedIds = new Set();
+ if (blockedDomains.size > 0) {
+ const allActors = [...followers, ...following];
+ for (const actor of allActors) {
+ try {
+ const domain = new URL(actor.actorUrl).hostname;
+ if (blockedDomains.has(domain)) {
+ domainBlockedIds.add(remoteActorId(actor.actorUrl));
+ }
+ } catch { /* skip invalid URLs */ }
+ }
+ }
+
const relationships = ids.map((id) => ({
id,
following: followingIds.has(id),
@@ -195,7 +211,7 @@ router.get("/api/v1/accounts/relationships", async (req, res, next) => {
muting_notifications: mutedIds.has(id),
requested: false,
requested_by: false,
- domain_blocking: false,
+ domain_blocking: domainBlockedIds.has(id),
endorsed: false,
note: "",
}));
diff --git a/lib/mastodon/routes/stubs.js b/lib/mastodon/routes/stubs.js
index eb9a73e..70c9458 100644
--- a/lib/mastodon/routes/stubs.js
+++ b/lib/mastodon/routes/stubs.js
@@ -314,8 +314,15 @@ router.get("/api/v1/conversations", (req, res) => {
// ─── Domain blocks ──────────────────────────────────────────────────────────
-router.get("/api/v1/domain_blocks", (req, res) => {
- res.json([]);
+router.get("/api/v1/domain_blocks", async (req, res) => {
+ try {
+ const collections = req.app.locals.mastodonCollections;
+ if (!collections?.ap_blocked_servers) return res.json([]);
+ const docs = await collections.ap_blocked_servers.find({}).toArray();
+ res.json(docs.map((d) => d.hostname).filter(Boolean));
+ } catch {
+ res.json([]);
+ }
});
// ─── Endorsements ───────────────────────────────────────────────────────────
diff --git a/package.json b/package.json
index 98784ef..9798d17 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@rmdes/indiekit-endpoint-activitypub",
- "version": "3.7.4",
+ "version": "3.7.5",
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
"keywords": [
"indiekit",
diff --git a/views/activitypub-federation-mgmt.njk b/views/activitypub-federation-mgmt.njk
index dd52522..d76b37e 100644
--- a/views/activitypub-federation-mgmt.njk
+++ b/views/activitypub-federation-mgmt.njk
@@ -116,6 +116,53 @@
{% endif %}
+ {# --- Moderation Overview --- #}
+ Moderation
+ {% if blockedServers.length > 0 %}
+ Blocked servers ({{ blockedServers.length }})
+ Blocked accounts ({{ blockedAccounts.length }})
+ Muted ({{ mutedAccounts.length }})
+