feat: surface moderation data in federation admin + Mastodon API
1. Federation admin page (/admin/federation): new Moderation section showing blocked servers (with hostnames), blocked accounts, and muted accounts/keywords 2. GET /api/v1/domain_blocks: returns actual blocked server hostnames from ap_blocked_servers (was stub returning []) 3. Relationship responses: domain_blocking field now checks if the account's domain matches a blocked server hostname (was always false)
This commit is contained in:
@@ -41,12 +41,16 @@ export function federationMgmtController(mountPath, plugin) {
|
|||||||
|
|
||||||
const redisUrl = plugin.options.redisUrl || "";
|
const redisUrl = plugin.options.redisUrl || "";
|
||||||
|
|
||||||
// Parallel: collection stats + posts + recent activities
|
// Parallel: collection stats + posts + recent activities + moderation data
|
||||||
const [collectionStats, postsResult, recentActivities] =
|
const pluginCollections = plugin._collections || {};
|
||||||
|
const [collectionStats, postsResult, recentActivities, blockedServers, blockedAccounts, mutedAccounts] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
getCollectionStats(collections, { redisUrl }),
|
getCollectionStats(collections, { redisUrl }),
|
||||||
getPaginatedPosts(collections, request.query.page),
|
getPaginatedPosts(collections, request.query.page),
|
||||||
getRecentActivities(collections),
|
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);
|
const csrfToken = getToken(request.session);
|
||||||
@@ -62,6 +66,9 @@ export function federationMgmtController(mountPath, plugin) {
|
|||||||
posts: postsResult.posts,
|
posts: postsResult.posts,
|
||||||
cursor: postsResult.cursor,
|
cursor: postsResult.cursor,
|
||||||
recentActivities,
|
recentActivities,
|
||||||
|
blockedServers: blockedServers || [],
|
||||||
|
blockedAccounts: blockedAccounts || [],
|
||||||
|
mutedAccounts: mutedAccounts || [],
|
||||||
csrfToken,
|
csrfToken,
|
||||||
mountPath,
|
mountPath,
|
||||||
publicationUrl: plugin._publicationUrl,
|
publicationUrl: plugin._publicationUrl,
|
||||||
|
|||||||
@@ -170,11 +170,12 @@ router.get("/api/v1/accounts/relationships", async (req, res, next) => {
|
|||||||
|
|
||||||
const collections = req.app.locals.mastodonCollections;
|
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_followers.find({}).toArray(),
|
||||||
collections.ap_following.find({}).toArray(),
|
collections.ap_following.find({}).toArray(),
|
||||||
collections.ap_blocked.find({}).toArray(),
|
collections.ap_blocked.find({}).toArray(),
|
||||||
collections.ap_muted.find({}).toArray(),
|
collections.ap_muted.find({}).toArray(),
|
||||||
|
collections.ap_blocked_servers?.find({}).toArray() || [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const followerIds = new Set(followers.map((f) => remoteActorId(f.actorUrl)));
|
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 blockedIds = new Set(blocked.map((b) => remoteActorId(b.url)));
|
||||||
const mutedIds = new Set(muted.filter((m) => m.url).map((m) => remoteActorId(m.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) => ({
|
const relationships = ids.map((id) => ({
|
||||||
id,
|
id,
|
||||||
following: followingIds.has(id),
|
following: followingIds.has(id),
|
||||||
@@ -195,7 +211,7 @@ router.get("/api/v1/accounts/relationships", async (req, res, next) => {
|
|||||||
muting_notifications: mutedIds.has(id),
|
muting_notifications: mutedIds.has(id),
|
||||||
requested: false,
|
requested: false,
|
||||||
requested_by: false,
|
requested_by: false,
|
||||||
domain_blocking: false,
|
domain_blocking: domainBlockedIds.has(id),
|
||||||
endorsed: false,
|
endorsed: false,
|
||||||
note: "",
|
note: "",
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -314,8 +314,15 @@ router.get("/api/v1/conversations", (req, res) => {
|
|||||||
|
|
||||||
// ─── Domain blocks ──────────────────────────────────────────────────────────
|
// ─── Domain blocks ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
router.get("/api/v1/domain_blocks", (req, res) => {
|
router.get("/api/v1/domain_blocks", async (req, res) => {
|
||||||
res.json([]);
|
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 ───────────────────────────────────────────────────────────
|
// ─── Endorsements ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
"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.",
|
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"indiekit",
|
"indiekit",
|
||||||
|
|||||||
@@ -116,6 +116,53 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{# --- Moderation Overview --- #}
|
||||||
|
<section class="ap-federation__section">
|
||||||
|
<h2>Moderation</h2>
|
||||||
|
{% if blockedServers.length > 0 %}
|
||||||
|
<h3>Blocked servers ({{ blockedServers.length }})</h3>
|
||||||
|
<div class="ap-federation__stats-grid">
|
||||||
|
{% for server in blockedServers %}
|
||||||
|
<div class="ap-federation__stat-card">
|
||||||
|
<span class="ap-federation__stat-label">🚫 {{ server.hostname }}</span>
|
||||||
|
{% if server.blockedAt %}
|
||||||
|
<span class="ap-federation__stat-count" style="font-size: 0.75rem; opacity: 0.7">{{ server.blockedAt | date("PPp") }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{{ prose({ text: "No servers blocked." }) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if blockedAccounts.length > 0 %}
|
||||||
|
<h3>Blocked accounts ({{ blockedAccounts.length }})</h3>
|
||||||
|
<div class="ap-federation__stats-grid">
|
||||||
|
{% for account in blockedAccounts %}
|
||||||
|
<div class="ap-federation__stat-card">
|
||||||
|
<span class="ap-federation__stat-label">🚫 {{ account.url or account.handle or "Unknown" }}</span>
|
||||||
|
{% if account.blockedAt %}
|
||||||
|
<span class="ap-federation__stat-count" style="font-size: 0.75rem; opacity: 0.7">{{ account.blockedAt | date("PPp") }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{{ prose({ text: "No accounts blocked." }) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if mutedAccounts.length > 0 %}
|
||||||
|
<h3>Muted ({{ mutedAccounts.length }})</h3>
|
||||||
|
<div class="ap-federation__stats-grid">
|
||||||
|
{% for muted in mutedAccounts %}
|
||||||
|
<div class="ap-federation__stat-card">
|
||||||
|
<span class="ap-federation__stat-label">🔇 {{ muted.url or muted.keyword or "Unknown" }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
{# --- JSON Modal --- #}
|
{# --- JSON Modal --- #}
|
||||||
<div class="ap-federation__modal-overlay" x-show="jsonModalOpen" x-cloak
|
<div class="ap-federation__modal-overlay" x-show="jsonModalOpen" x-cloak
|
||||||
@click.self="jsonModalOpen = false" @keydown.escape.window="jsonModalOpen = false">
|
@click.self="jsonModalOpen = false" @keydown.escape.window="jsonModalOpen = false">
|
||||||
|
|||||||
Reference in New Issue
Block a user