@rmdes/indiekit-endpoint-activitypub 3.7.4 → 3.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,12 +41,16 @@ export function federationMgmtController(mountPath, plugin) {
41
41
 
42
42
  const redisUrl = plugin.options.redisUrl || "";
43
43
 
44
- // Parallel: collection stats + posts + recent activities
45
- const [collectionStats, postsResult, recentActivities] =
44
+ // Parallel: collection stats + posts + recent activities + moderation data
45
+ const pluginCollections = plugin._collections || {};
46
+ const [collectionStats, postsResult, recentActivities, blockedServers, blockedAccounts, mutedAccounts] =
46
47
  await Promise.all([
47
48
  getCollectionStats(collections, { redisUrl }),
48
49
  getPaginatedPosts(collections, request.query.page),
49
50
  getRecentActivities(collections),
51
+ pluginCollections.ap_blocked_servers?.find({}).sort({ blockedAt: -1 }).toArray() || [],
52
+ pluginCollections.ap_blocked?.find({}).sort({ blockedAt: -1 }).toArray() || [],
53
+ pluginCollections.ap_muted?.find({}).sort({ mutedAt: -1 }).toArray() || [],
50
54
  ]);
51
55
 
52
56
  const csrfToken = getToken(request.session);
@@ -62,6 +66,9 @@ export function federationMgmtController(mountPath, plugin) {
62
66
  posts: postsResult.posts,
63
67
  cursor: postsResult.cursor,
64
68
  recentActivities,
69
+ blockedServers: blockedServers || [],
70
+ blockedAccounts: blockedAccounts || [],
71
+ mutedAccounts: mutedAccounts || [],
65
72
  csrfToken,
66
73
  mountPath,
67
74
  publicationUrl: plugin._publicationUrl,
@@ -170,11 +170,12 @@ router.get("/api/v1/accounts/relationships", async (req, res, next) => {
170
170
 
171
171
  const collections = req.app.locals.mastodonCollections;
172
172
 
173
- const [followers, following, blocked, muted] = await Promise.all([
173
+ const [followers, following, blocked, muted, blockedServers] = await Promise.all([
174
174
  collections.ap_followers.find({}).toArray(),
175
175
  collections.ap_following.find({}).toArray(),
176
176
  collections.ap_blocked.find({}).toArray(),
177
177
  collections.ap_muted.find({}).toArray(),
178
+ collections.ap_blocked_servers?.find({}).toArray() || [],
178
179
  ]);
179
180
 
180
181
  const followerIds = new Set(followers.map((f) => remoteActorId(f.actorUrl)));
@@ -182,6 +183,21 @@ router.get("/api/v1/accounts/relationships", async (req, res, next) => {
182
183
  const blockedIds = new Set(blocked.map((b) => remoteActorId(b.url)));
183
184
  const mutedIds = new Set(muted.filter((m) => m.url).map((m) => remoteActorId(m.url)));
184
185
 
186
+ // Build domain-blocked actor ID set by checking known actors against blocked server hostnames
187
+ const blockedDomains = new Set(blockedServers.map((s) => s.hostname).filter(Boolean));
188
+ const domainBlockedIds = new Set();
189
+ if (blockedDomains.size > 0) {
190
+ const allActors = [...followers, ...following];
191
+ for (const actor of allActors) {
192
+ try {
193
+ const domain = new URL(actor.actorUrl).hostname;
194
+ if (blockedDomains.has(domain)) {
195
+ domainBlockedIds.add(remoteActorId(actor.actorUrl));
196
+ }
197
+ } catch { /* skip invalid URLs */ }
198
+ }
199
+ }
200
+
185
201
  const relationships = ids.map((id) => ({
186
202
  id,
187
203
  following: followingIds.has(id),
@@ -195,7 +211,7 @@ router.get("/api/v1/accounts/relationships", async (req, res, next) => {
195
211
  muting_notifications: mutedIds.has(id),
196
212
  requested: false,
197
213
  requested_by: false,
198
- domain_blocking: false,
214
+ domain_blocking: domainBlockedIds.has(id),
199
215
  endorsed: false,
200
216
  note: "",
201
217
  }));
@@ -314,8 +314,15 @@ router.get("/api/v1/conversations", (req, res) => {
314
314
 
315
315
  // ─── Domain blocks ──────────────────────────────────────────────────────────
316
316
 
317
- router.get("/api/v1/domain_blocks", (req, res) => {
318
- res.json([]);
317
+ router.get("/api/v1/domain_blocks", async (req, res) => {
318
+ try {
319
+ const collections = req.app.locals.mastodonCollections;
320
+ if (!collections?.ap_blocked_servers) return res.json([]);
321
+ const docs = await collections.ap_blocked_servers.find({}).toArray();
322
+ res.json(docs.map((d) => d.hostname).filter(Boolean));
323
+ } catch {
324
+ res.json([]);
325
+ }
319
326
  });
320
327
 
321
328
  // ─── Endorsements ───────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "3.7.4",
3
+ "version": "3.7.5",
4
4
  "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -116,6 +116,53 @@
116
116
  {% endif %}
117
117
  </section>
118
118
 
119
+ {# --- Moderation Overview --- #}
120
+ <section class="ap-federation__section">
121
+ <h2>Moderation</h2>
122
+ {% if blockedServers.length > 0 %}
123
+ <h3>Blocked servers ({{ blockedServers.length }})</h3>
124
+ <div class="ap-federation__stats-grid">
125
+ {% for server in blockedServers %}
126
+ <div class="ap-federation__stat-card">
127
+ <span class="ap-federation__stat-label">🚫 {{ server.hostname }}</span>
128
+ {% if server.blockedAt %}
129
+ <span class="ap-federation__stat-count" style="font-size: 0.75rem; opacity: 0.7">{{ server.blockedAt | date("PPp") }}</span>
130
+ {% endif %}
131
+ </div>
132
+ {% endfor %}
133
+ </div>
134
+ {% else %}
135
+ {{ prose({ text: "No servers blocked." }) }}
136
+ {% endif %}
137
+
138
+ {% if blockedAccounts.length > 0 %}
139
+ <h3>Blocked accounts ({{ blockedAccounts.length }})</h3>
140
+ <div class="ap-federation__stats-grid">
141
+ {% for account in blockedAccounts %}
142
+ <div class="ap-federation__stat-card">
143
+ <span class="ap-federation__stat-label">🚫 {{ account.url or account.handle or "Unknown" }}</span>
144
+ {% if account.blockedAt %}
145
+ <span class="ap-federation__stat-count" style="font-size: 0.75rem; opacity: 0.7">{{ account.blockedAt | date("PPp") }}</span>
146
+ {% endif %}
147
+ </div>
148
+ {% endfor %}
149
+ </div>
150
+ {% else %}
151
+ {{ prose({ text: "No accounts blocked." }) }}
152
+ {% endif %}
153
+
154
+ {% if mutedAccounts.length > 0 %}
155
+ <h3>Muted ({{ mutedAccounts.length }})</h3>
156
+ <div class="ap-federation__stats-grid">
157
+ {% for muted in mutedAccounts %}
158
+ <div class="ap-federation__stat-card">
159
+ <span class="ap-federation__stat-label">🔇 {{ muted.url or muted.keyword or "Unknown" }}</span>
160
+ </div>
161
+ {% endfor %}
162
+ </div>
163
+ {% endif %}
164
+ </section>
165
+
119
166
  {# --- JSON Modal --- #}
120
167
  <div class="ap-federation__modal-overlay" x-show="jsonModalOpen" x-cloak
121
168
  @click.self="jsonModalOpen = false" @keydown.escape.window="jsonModalOpen = false">