@rmdes/indiekit-endpoint-activitypub 3.8.5 → 3.8.7
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.
- package/index.js +137 -32
- package/lib/inbox-queue.js +3 -4
- 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/lib/inbox-queue.js
CHANGED
|
@@ -28,10 +28,9 @@ async function processNextItem(collections, ctx, handle) {
|
|
|
28
28
|
|
|
29
29
|
try {
|
|
30
30
|
await routeToHandler(item, collections, ctx, handle);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
);
|
|
31
|
+
// Delete completed items immediately — prevents unbounded collection growth
|
|
32
|
+
// that caused the inbox processor to hang on restart (95K+ documents).
|
|
33
|
+
await ap_inbox_queue.deleteOne({ _id: item._id });
|
|
35
34
|
} catch (error) {
|
|
36
35
|
const attempts = (item.attempts || 0) + 1;
|
|
37
36
|
await ap_inbox_queue.updateOne(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.7",
|
|
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",
|