@rmdes/indiekit-endpoint-activitypub 3.8.5 → 3.8.6

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.
Files changed (2) hide show
  1. package/index.js +137 -32
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -387,38 +387,6 @@ export default class ActivityPubEndpoint {
387
387
  const router = express.Router(); // eslint-disable-line new-cap
388
388
  const self = this;
389
389
 
390
- // Intercept Micropub delete actions to broadcast Delete to fediverse.
391
- // Wraps res.json to detect successful delete responses, then fires
392
- // broadcastDelete asynchronously so remote servers remove the post.
393
- router.use((req, res, next) => {
394
- if (req.method !== "POST") return next();
395
- if (!req.path.endsWith("/micropub")) return next();
396
-
397
- const action = req.query?.action || req.body?.action;
398
- if (action !== "delete") return next();
399
-
400
- const postUrl = req.query?.url || req.body?.url;
401
- if (!postUrl) return next();
402
-
403
- const originalJson = res.json.bind(res);
404
- res.json = function (body) {
405
- // Fire broadcastDelete after successful delete (status 200)
406
- if (res.statusCode === 200 && body?.success === "delete") {
407
- console.info(
408
- `[ActivityPub] Micropub delete detected for ${postUrl}, broadcasting Delete to followers`,
409
- );
410
- self.broadcastDelete(postUrl).catch((error) => {
411
- console.warn(
412
- `[ActivityPub] broadcastDelete after Micropub delete failed: ${error.message}`,
413
- );
414
- });
415
- }
416
- return originalJson(body);
417
- };
418
-
419
- return next();
420
- });
421
-
422
390
  // Let Fedify handle NodeInfo data (/nodeinfo/2.1)
423
391
  // Only pass GET/HEAD requests — POST/PUT/DELETE must not go through
424
392
  // Fedify here, because fromExpressRequest() consumes the body stream,
@@ -715,6 +683,9 @@ export default class ActivityPubEndpoint {
715
683
  return undefined;
716
684
  }
717
685
  },
686
+
687
+ delete: async (url) => this.delete(url),
688
+ update: async (properties) => this.update(properties),
718
689
  };
719
690
  }
720
691
 
@@ -1181,6 +1152,138 @@ export default class ActivityPubEndpoint {
1181
1152
  }
1182
1153
  }
1183
1154
 
1155
+ /**
1156
+ * Called by post-content.js when a Micropub delete succeeds.
1157
+ * Broadcasts an ActivityPub Delete activity to all followers.
1158
+ * @param {string} url - Full URL of the deleted post
1159
+ */
1160
+ async delete(url) {
1161
+ await this.broadcastDelete(url).catch((err) =>
1162
+ console.warn(`[ActivityPub] broadcastDelete failed for ${url}: ${err.message}`)
1163
+ );
1164
+ }
1165
+
1166
+ /**
1167
+ * Called by post-content.js when a Micropub update succeeds.
1168
+ * Broadcasts an ActivityPub Update activity for the post to all followers.
1169
+ * @param {object} properties - JF2 post properties (must include url)
1170
+ */
1171
+ async update(properties) {
1172
+ await this.broadcastPostUpdate(properties).catch((err) =>
1173
+ console.warn(`[ActivityPub] broadcastPostUpdate failed for ${properties?.url}: ${err.message}`)
1174
+ );
1175
+ }
1176
+
1177
+ /**
1178
+ * Send an Update activity to all followers for a modified post.
1179
+ * Mirrors broadcastDelete() pattern: batch delivery with shared inbox dedup.
1180
+ * @param {object} properties - JF2 post properties
1181
+ */
1182
+ async broadcastPostUpdate(properties) {
1183
+ if (!this._federation) return;
1184
+
1185
+ try {
1186
+ const { Update } = await import("@fedify/fedify/vocab");
1187
+ const actorUrl = this._getActorUrl();
1188
+ const handle = this.options.actor.handle;
1189
+ const ctx = this._federation.createContext(
1190
+ new URL(this._publicationUrl),
1191
+ { handle, publicationUrl: this._publicationUrl },
1192
+ );
1193
+
1194
+ // Build the Note/Article object by calling jf2ToAS2Activity() and
1195
+ // extracting the wrapped object from the returned Create activity.
1196
+ // For post edits, Fediverse servers expect an Update activity wrapping
1197
+ // the updated object — NOT a second Create activity.
1198
+ const createActivity = jf2ToAS2Activity(
1199
+ properties,
1200
+ actorUrl,
1201
+ this._publicationUrl,
1202
+ { visibility: this.options.defaultVisibility },
1203
+ );
1204
+
1205
+ if (!createActivity) {
1206
+ console.warn(`[ActivityPub] broadcastPostUpdate: could not convert post to AS2 for ${properties?.url}`);
1207
+ return;
1208
+ }
1209
+
1210
+ // Extract the Note/Article object from the Create wrapper, then build
1211
+ // an Update activity around it — matching broadcastActorUpdate() pattern.
1212
+ const noteObject = await createActivity.getObject();
1213
+ const activity = new Update({
1214
+ actor: ctx.getActorUri(handle),
1215
+ object: noteObject,
1216
+ });
1217
+
1218
+ const followers = await this._collections.ap_followers
1219
+ .find({})
1220
+ .project({ actorUrl: 1, inbox: 1, sharedInbox: 1 })
1221
+ .toArray();
1222
+
1223
+ const inboxMap = new Map();
1224
+ for (const f of followers) {
1225
+ const key = f.sharedInbox || f.inbox;
1226
+ if (key && !inboxMap.has(key)) {
1227
+ inboxMap.set(key, f);
1228
+ }
1229
+ }
1230
+
1231
+ const uniqueRecipients = [...inboxMap.values()];
1232
+ const BATCH_SIZE = 25;
1233
+ const BATCH_DELAY_MS = 5000;
1234
+ let delivered = 0;
1235
+ let failed = 0;
1236
+
1237
+ console.info(
1238
+ `[ActivityPub] Broadcasting Update for ${properties.url} to ${uniqueRecipients.length} unique inboxes`,
1239
+ );
1240
+
1241
+ for (let i = 0; i < uniqueRecipients.length; i += BATCH_SIZE) {
1242
+ const batch = uniqueRecipients.slice(i, i + BATCH_SIZE);
1243
+ const recipients = batch.map((f) => ({
1244
+ id: new URL(f.actorUrl),
1245
+ inboxId: new URL(f.inbox || f.sharedInbox),
1246
+ endpoints: f.sharedInbox
1247
+ ? { sharedInbox: new URL(f.sharedInbox) }
1248
+ : undefined,
1249
+ }));
1250
+
1251
+ try {
1252
+ await ctx.sendActivity(
1253
+ { identifier: handle },
1254
+ recipients,
1255
+ activity,
1256
+ { preferSharedInbox: true },
1257
+ );
1258
+ delivered += batch.length;
1259
+ } catch (error) {
1260
+ failed += batch.length;
1261
+ console.warn(
1262
+ `[ActivityPub] Update batch ${Math.floor(i / BATCH_SIZE) + 1} failed: ${error.message}`,
1263
+ );
1264
+ }
1265
+
1266
+ if (i + BATCH_SIZE < uniqueRecipients.length) {
1267
+ await new Promise((resolve) => setTimeout(resolve, BATCH_DELAY_MS));
1268
+ }
1269
+ }
1270
+
1271
+ console.info(
1272
+ `[ActivityPub] Update broadcast complete for ${properties.url}: ${delivered} delivered, ${failed} failed`,
1273
+ );
1274
+
1275
+ await logActivity(this._collections.ap_activities, {
1276
+ direction: "outbound",
1277
+ type: "Update",
1278
+ actorUrl: this._publicationUrl,
1279
+ objectUrl: properties.url,
1280
+ summary: `Sent Update for ${properties.url} to ${delivered} inboxes`,
1281
+ }).catch(() => {});
1282
+ } catch (error) {
1283
+ console.warn("[ActivityPub] broadcastPostUpdate failed:", error.message);
1284
+ }
1285
+ }
1286
+
1184
1287
  /**
1185
1288
  * Build the full actor URL from config.
1186
1289
  * @returns {string}
@@ -1615,6 +1718,7 @@ export default class ActivityPubEndpoint {
1615
1718
  }, 10_000);
1616
1719
 
1617
1720
  // Run one-time migrations (idempotent — safe to run on every startup)
1721
+ console.info("[ActivityPub] Init: starting post-refollow setup");
1618
1722
  runSeparateMentionsMigration(this._collections).then(({ skipped, updated }) => {
1619
1723
  if (!skipped) {
1620
1724
  console.log(`[ActivityPub] Migration separate-mentions: updated ${updated} timeline items`);
@@ -1661,6 +1765,7 @@ export default class ActivityPubEndpoint {
1661
1765
  });
1662
1766
 
1663
1767
  // Start async inbox queue processor (processes one item every 3s)
1768
+ console.info("[ActivityPub] Init: starting inbox queue processor");
1664
1769
  this._inboxProcessorInterval = startInboxProcessor(
1665
1770
  this._collections,
1666
1771
  () => this._federation?.createContext(new URL(this._publicationUrl), {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "3.8.5",
3
+ "version": "3.8.6",
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",