@rmdes/indiekit-endpoint-activitypub 1.0.5 → 1.0.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.
- package/index.js +219 -0
- package/lib/activity-log.js +31 -0
- package/lib/inbox-listeners.js +168 -18
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
profileGetController,
|
|
22
22
|
profilePostController,
|
|
23
23
|
} from "./lib/controllers/profile.js";
|
|
24
|
+
import { logActivity } from "./lib/activity-log.js";
|
|
24
25
|
|
|
25
26
|
const defaults = {
|
|
26
27
|
mountPath: "/activitypub",
|
|
@@ -256,6 +257,13 @@ export default class ActivityPubEndpoint {
|
|
|
256
257
|
);
|
|
257
258
|
|
|
258
259
|
if (!activity) {
|
|
260
|
+
await logActivity(self._collections.ap_activities, {
|
|
261
|
+
direction: "outbound",
|
|
262
|
+
type: "Syndicate",
|
|
263
|
+
actorUrl: self._publicationUrl,
|
|
264
|
+
objectUrl: properties.url,
|
|
265
|
+
summary: `Syndication skipped: could not convert post to AS2`,
|
|
266
|
+
});
|
|
259
267
|
return undefined;
|
|
260
268
|
}
|
|
261
269
|
|
|
@@ -264,21 +272,225 @@ export default class ActivityPubEndpoint {
|
|
|
264
272
|
{},
|
|
265
273
|
);
|
|
266
274
|
|
|
275
|
+
// Count followers for logging
|
|
276
|
+
const followerCount =
|
|
277
|
+
await self._collections.ap_followers.countDocuments();
|
|
278
|
+
|
|
279
|
+
console.info(
|
|
280
|
+
`[ActivityPub] Sending ${activity.constructor?.name || "activity"} for ${properties.url} to ${followerCount} followers`,
|
|
281
|
+
);
|
|
282
|
+
|
|
267
283
|
await ctx.sendActivity(
|
|
268
284
|
{ identifier: self.options.actor.handle },
|
|
269
285
|
"followers",
|
|
270
286
|
activity,
|
|
271
287
|
);
|
|
272
288
|
|
|
289
|
+
// Determine activity type name
|
|
290
|
+
const typeName =
|
|
291
|
+
activity.constructor?.name || "Create";
|
|
292
|
+
|
|
293
|
+
await logActivity(self._collections.ap_activities, {
|
|
294
|
+
direction: "outbound",
|
|
295
|
+
type: typeName,
|
|
296
|
+
actorUrl: self._publicationUrl,
|
|
297
|
+
objectUrl: properties.url,
|
|
298
|
+
summary: `Sent ${typeName} for ${properties.url} to ${followerCount} followers`,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
console.info(
|
|
302
|
+
`[ActivityPub] Syndication queued: ${typeName} for ${properties.url}`,
|
|
303
|
+
);
|
|
304
|
+
|
|
273
305
|
return properties.url || undefined;
|
|
274
306
|
} catch (error) {
|
|
275
307
|
console.error("[ActivityPub] Syndication failed:", error.message);
|
|
308
|
+
await logActivity(self._collections.ap_activities, {
|
|
309
|
+
direction: "outbound",
|
|
310
|
+
type: "Syndicate",
|
|
311
|
+
actorUrl: self._publicationUrl,
|
|
312
|
+
objectUrl: properties.url,
|
|
313
|
+
summary: `Syndication failed: ${error.message}`,
|
|
314
|
+
}).catch(() => {});
|
|
276
315
|
return undefined;
|
|
277
316
|
}
|
|
278
317
|
},
|
|
279
318
|
};
|
|
280
319
|
}
|
|
281
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Send a Follow activity to a remote actor and store in ap_following.
|
|
323
|
+
* @param {string} actorUrl - The remote actor's URL
|
|
324
|
+
* @param {object} [actorInfo] - Optional pre-fetched actor info
|
|
325
|
+
* @param {string} [actorInfo.name] - Actor display name
|
|
326
|
+
* @param {string} [actorInfo.handle] - Actor handle
|
|
327
|
+
* @param {string} [actorInfo.photo] - Actor avatar URL
|
|
328
|
+
* @returns {Promise<{ok: boolean, error?: string}>}
|
|
329
|
+
*/
|
|
330
|
+
async followActor(actorUrl, actorInfo = {}) {
|
|
331
|
+
if (!this._federation) {
|
|
332
|
+
return { ok: false, error: "Federation not initialized" };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const { Follow } = await import("@fedify/fedify");
|
|
337
|
+
const handle = this.options.actor.handle;
|
|
338
|
+
const ctx = this._federation.createContext(
|
|
339
|
+
new URL(this._publicationUrl),
|
|
340
|
+
{},
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Resolve the remote actor to get their inbox
|
|
344
|
+
const remoteActor = await ctx.lookupObject(actorUrl);
|
|
345
|
+
if (!remoteActor) {
|
|
346
|
+
return { ok: false, error: "Could not resolve remote actor" };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Send Follow activity
|
|
350
|
+
const follow = new Follow({
|
|
351
|
+
actor: ctx.getActorUri(handle),
|
|
352
|
+
object: new URL(actorUrl),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await ctx.sendActivity({ identifier: handle }, remoteActor, follow);
|
|
356
|
+
|
|
357
|
+
// Store in ap_following
|
|
358
|
+
const name =
|
|
359
|
+
actorInfo.name ||
|
|
360
|
+
remoteActor.name?.toString() ||
|
|
361
|
+
remoteActor.preferredUsername?.toString() ||
|
|
362
|
+
actorUrl;
|
|
363
|
+
const actorHandle =
|
|
364
|
+
actorInfo.handle ||
|
|
365
|
+
remoteActor.preferredUsername?.toString() ||
|
|
366
|
+
"";
|
|
367
|
+
const avatar =
|
|
368
|
+
actorInfo.photo ||
|
|
369
|
+
(remoteActor.icon
|
|
370
|
+
? (await remoteActor.icon)?.url?.href || ""
|
|
371
|
+
: "");
|
|
372
|
+
const inbox = remoteActor.inbox?.id?.href || "";
|
|
373
|
+
const sharedInbox = remoteActor.endpoints?.sharedInbox?.href || "";
|
|
374
|
+
|
|
375
|
+
await this._collections.ap_following.updateOne(
|
|
376
|
+
{ actorUrl },
|
|
377
|
+
{
|
|
378
|
+
$set: {
|
|
379
|
+
actorUrl,
|
|
380
|
+
handle: actorHandle,
|
|
381
|
+
name,
|
|
382
|
+
avatar,
|
|
383
|
+
inbox,
|
|
384
|
+
sharedInbox,
|
|
385
|
+
followedAt: new Date().toISOString(),
|
|
386
|
+
source: "microsub-reader",
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
{ upsert: true },
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
console.info(`[ActivityPub] Sent Follow to ${actorUrl}`);
|
|
393
|
+
|
|
394
|
+
await logActivity(this._collections.ap_activities, {
|
|
395
|
+
direction: "outbound",
|
|
396
|
+
type: "Follow",
|
|
397
|
+
actorUrl: this._publicationUrl,
|
|
398
|
+
objectUrl: actorUrl,
|
|
399
|
+
actorName: name,
|
|
400
|
+
summary: `Sent Follow to ${name} (${actorUrl})`,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
return { ok: true };
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error(`[ActivityPub] Follow failed for ${actorUrl}:`, error.message);
|
|
406
|
+
|
|
407
|
+
await logActivity(this._collections.ap_activities, {
|
|
408
|
+
direction: "outbound",
|
|
409
|
+
type: "Follow",
|
|
410
|
+
actorUrl: this._publicationUrl,
|
|
411
|
+
objectUrl: actorUrl,
|
|
412
|
+
summary: `Follow failed for ${actorUrl}: ${error.message}`,
|
|
413
|
+
}).catch(() => {});
|
|
414
|
+
|
|
415
|
+
return { ok: false, error: error.message };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Send an Undo(Follow) activity and remove from ap_following.
|
|
421
|
+
* @param {string} actorUrl - The remote actor's URL
|
|
422
|
+
* @returns {Promise<{ok: boolean, error?: string}>}
|
|
423
|
+
*/
|
|
424
|
+
async unfollowActor(actorUrl) {
|
|
425
|
+
if (!this._federation) {
|
|
426
|
+
return { ok: false, error: "Federation not initialized" };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
const { Follow, Undo } = await import("@fedify/fedify");
|
|
431
|
+
const handle = this.options.actor.handle;
|
|
432
|
+
const ctx = this._federation.createContext(
|
|
433
|
+
new URL(this._publicationUrl),
|
|
434
|
+
{},
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const remoteActor = await ctx.lookupObject(actorUrl);
|
|
438
|
+
if (!remoteActor) {
|
|
439
|
+
// Even if we can't resolve, remove locally
|
|
440
|
+
await this._collections.ap_following.deleteOne({ actorUrl });
|
|
441
|
+
|
|
442
|
+
await logActivity(this._collections.ap_activities, {
|
|
443
|
+
direction: "outbound",
|
|
444
|
+
type: "Undo(Follow)",
|
|
445
|
+
actorUrl: this._publicationUrl,
|
|
446
|
+
objectUrl: actorUrl,
|
|
447
|
+
summary: `Removed ${actorUrl} locally (could not resolve remote actor)`,
|
|
448
|
+
}).catch(() => {});
|
|
449
|
+
|
|
450
|
+
return { ok: true };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const follow = new Follow({
|
|
454
|
+
actor: ctx.getActorUri(handle),
|
|
455
|
+
object: new URL(actorUrl),
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const undo = new Undo({
|
|
459
|
+
actor: ctx.getActorUri(handle),
|
|
460
|
+
object: follow,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await ctx.sendActivity({ identifier: handle }, remoteActor, undo);
|
|
464
|
+
await this._collections.ap_following.deleteOne({ actorUrl });
|
|
465
|
+
|
|
466
|
+
console.info(`[ActivityPub] Sent Undo(Follow) to ${actorUrl}`);
|
|
467
|
+
|
|
468
|
+
await logActivity(this._collections.ap_activities, {
|
|
469
|
+
direction: "outbound",
|
|
470
|
+
type: "Undo(Follow)",
|
|
471
|
+
actorUrl: this._publicationUrl,
|
|
472
|
+
objectUrl: actorUrl,
|
|
473
|
+
summary: `Sent Undo(Follow) to ${actorUrl}`,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
return { ok: true };
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error(`[ActivityPub] Unfollow failed for ${actorUrl}:`, error.message);
|
|
479
|
+
|
|
480
|
+
await logActivity(this._collections.ap_activities, {
|
|
481
|
+
direction: "outbound",
|
|
482
|
+
type: "Undo(Follow)",
|
|
483
|
+
actorUrl: this._publicationUrl,
|
|
484
|
+
objectUrl: actorUrl,
|
|
485
|
+
summary: `Unfollow failed for ${actorUrl}: ${error.message}`,
|
|
486
|
+
}).catch(() => {});
|
|
487
|
+
|
|
488
|
+
// Remove locally even if remote delivery fails
|
|
489
|
+
await this._collections.ap_following.deleteOne({ actorUrl }).catch(() => {});
|
|
490
|
+
return { ok: false, error: error.message };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
282
494
|
/**
|
|
283
495
|
* Build the full actor URL from config.
|
|
284
496
|
* @returns {string}
|
|
@@ -316,6 +528,13 @@ export default class ActivityPubEndpoint {
|
|
|
316
528
|
get posts() {
|
|
317
529
|
return indiekitCollections.get("posts");
|
|
318
530
|
},
|
|
531
|
+
// Lazy access to Microsub collections (may not exist if plugin not loaded)
|
|
532
|
+
get microsub_items() {
|
|
533
|
+
return indiekitCollections.get("microsub_items");
|
|
534
|
+
},
|
|
535
|
+
get microsub_channels() {
|
|
536
|
+
return indiekitCollections.get("microsub_channels");
|
|
537
|
+
},
|
|
319
538
|
_publicationUrl: this._publicationUrl,
|
|
320
539
|
};
|
|
321
540
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared activity logging utility.
|
|
3
|
+
*
|
|
4
|
+
* Logs inbound and outbound ActivityPub activities to the ap_activities
|
|
5
|
+
* collection so they appear in the Activity Log admin UI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Log an activity to the ap_activities collection.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} collection - The ap_activities MongoDB collection
|
|
12
|
+
* @param {object} record - Activity record
|
|
13
|
+
* @param {string} record.direction - "inbound" or "outbound"
|
|
14
|
+
* @param {string} record.type - Activity type (e.g. "Create", "Follow", "Undo(Follow)")
|
|
15
|
+
* @param {string} [record.actorUrl] - Actor URL
|
|
16
|
+
* @param {string} [record.actorName] - Actor display name
|
|
17
|
+
* @param {string} [record.objectUrl] - Object URL
|
|
18
|
+
* @param {string} [record.targetUrl] - Target URL (e.g. reply target)
|
|
19
|
+
* @param {string} [record.content] - Content excerpt
|
|
20
|
+
* @param {string} record.summary - Human-readable summary
|
|
21
|
+
*/
|
|
22
|
+
export async function logActivity(collection, record) {
|
|
23
|
+
try {
|
|
24
|
+
await collection.insertOne({
|
|
25
|
+
...record,
|
|
26
|
+
receivedAt: new Date().toISOString(),
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.warn("[ActivityPub] Failed to log activity:", error.message);
|
|
30
|
+
}
|
|
31
|
+
}
|
package/lib/inbox-listeners.js
CHANGED
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
Update,
|
|
22
22
|
} from "@fedify/fedify";
|
|
23
23
|
|
|
24
|
+
import { logActivity as logActivityShared } from "./activity-log.js";
|
|
25
|
+
|
|
24
26
|
/**
|
|
25
27
|
* Register all inbox listeners on a federation's inbox chain.
|
|
26
28
|
*
|
|
@@ -157,12 +159,6 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
157
159
|
const object = await create.getObject();
|
|
158
160
|
if (!object) return;
|
|
159
161
|
|
|
160
|
-
const inReplyTo =
|
|
161
|
-
object instanceof Note
|
|
162
|
-
? (await object.getInReplyTo())?.id?.href
|
|
163
|
-
: null;
|
|
164
|
-
if (!inReplyTo) return;
|
|
165
|
-
|
|
166
162
|
const actorObj = await create.getActor();
|
|
167
163
|
const actorUrl = actorObj?.id?.href || "";
|
|
168
164
|
const actorName =
|
|
@@ -170,18 +166,33 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
170
166
|
actorObj?.preferredUsername?.toString() ||
|
|
171
167
|
actorUrl;
|
|
172
168
|
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
const inReplyTo =
|
|
170
|
+
object instanceof Note
|
|
171
|
+
? (await object.getInReplyTo())?.id?.href
|
|
172
|
+
: null;
|
|
173
|
+
|
|
174
|
+
// Log replies to our posts (existing behavior for conversations)
|
|
175
|
+
if (inReplyTo) {
|
|
176
|
+
const content = object.content?.toString() || "";
|
|
177
|
+
await logActivity(collections, storeRawActivities, {
|
|
178
|
+
direction: "inbound",
|
|
179
|
+
type: "Reply",
|
|
180
|
+
actorUrl,
|
|
181
|
+
actorName,
|
|
182
|
+
objectUrl: object.id?.href || "",
|
|
183
|
+
targetUrl: inReplyTo,
|
|
184
|
+
content,
|
|
185
|
+
summary: `${actorName} replied to ${inReplyTo}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
175
188
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
type: "Reply",
|
|
189
|
+
// Store timeline items from accounts we follow
|
|
190
|
+
await storeTimelineItem(collections, {
|
|
179
191
|
actorUrl,
|
|
180
192
|
actorName,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
summary: `${actorName} replied to ${inReplyTo}`,
|
|
193
|
+
actorObj,
|
|
194
|
+
object,
|
|
195
|
+
inReplyTo,
|
|
185
196
|
});
|
|
186
197
|
})
|
|
187
198
|
.on(Delete, async (ctx, del) => {
|
|
@@ -255,10 +266,149 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
255
266
|
|
|
256
267
|
/**
|
|
257
268
|
* Log an activity to the ap_activities collection.
|
|
269
|
+
* Wrapper around the shared utility that accepts the (collections, storeRaw, record) signature
|
|
270
|
+
* used throughout this file.
|
|
258
271
|
*/
|
|
259
272
|
async function logActivity(collections, storeRaw, record) {
|
|
260
|
-
await collections.ap_activities
|
|
261
|
-
|
|
262
|
-
|
|
273
|
+
await logActivityShared(collections.ap_activities, record);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Cached ActivityPub channel ObjectId
|
|
277
|
+
let _apChannelId = null;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Look up the ActivityPub channel's ObjectId (cached after first call).
|
|
281
|
+
* @param {object} collections - MongoDB collections
|
|
282
|
+
* @returns {Promise<import("mongodb").ObjectId|null>}
|
|
283
|
+
*/
|
|
284
|
+
async function getApChannelId(collections) {
|
|
285
|
+
if (_apChannelId) return _apChannelId;
|
|
286
|
+
const channel = await collections.microsub_channels?.findOne({
|
|
287
|
+
uid: "activitypub",
|
|
263
288
|
});
|
|
289
|
+
_apChannelId = channel?._id || null;
|
|
290
|
+
return _apChannelId;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Store a Create activity as a Microsub timeline item if the actor
|
|
295
|
+
* is someone we follow. Skips gracefully if the Microsub plugin
|
|
296
|
+
* isn't loaded or the AP channel doesn't exist yet.
|
|
297
|
+
*
|
|
298
|
+
* @param {object} collections - MongoDB collections
|
|
299
|
+
* @param {object} params
|
|
300
|
+
* @param {string} params.actorUrl - Actor URL
|
|
301
|
+
* @param {string} params.actorName - Actor display name
|
|
302
|
+
* @param {object} params.actorObj - Fedify actor object
|
|
303
|
+
* @param {object} params.object - Fedify Note/Article object
|
|
304
|
+
* @param {string|null} params.inReplyTo - URL this is a reply to (if any)
|
|
305
|
+
*/
|
|
306
|
+
async function storeTimelineItem(
|
|
307
|
+
collections,
|
|
308
|
+
{ actorUrl, actorName, actorObj, object, inReplyTo },
|
|
309
|
+
) {
|
|
310
|
+
// Skip if Microsub plugin not loaded
|
|
311
|
+
if (!collections.microsub_items || !collections.microsub_channels) return;
|
|
312
|
+
|
|
313
|
+
// Only store posts from accounts we follow
|
|
314
|
+
const following = await collections.ap_following.findOne({ actorUrl });
|
|
315
|
+
if (!following) return;
|
|
316
|
+
|
|
317
|
+
const channelId = await getApChannelId(collections);
|
|
318
|
+
if (!channelId) return;
|
|
319
|
+
|
|
320
|
+
const objectUrl = object.id?.href || "";
|
|
321
|
+
if (!objectUrl) return;
|
|
322
|
+
|
|
323
|
+
// Extract content
|
|
324
|
+
const contentHtml = object.content?.toString() || "";
|
|
325
|
+
const contentText = contentHtml.replace(/<[^>]*>/g, "").trim();
|
|
326
|
+
|
|
327
|
+
// Name (usually only on Article, not Note)
|
|
328
|
+
const name = object.name?.toString() || undefined;
|
|
329
|
+
const summary = object.summary?.toString() || undefined;
|
|
330
|
+
|
|
331
|
+
// Published date — Fedify returns Temporal.Instant
|
|
332
|
+
let published;
|
|
333
|
+
if (object.published) {
|
|
334
|
+
try {
|
|
335
|
+
published = new Date(Number(object.published.epochMilliseconds));
|
|
336
|
+
} catch {
|
|
337
|
+
published = new Date();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Author avatar
|
|
342
|
+
let authorPhoto = "";
|
|
343
|
+
try {
|
|
344
|
+
if (actorObj.icon) {
|
|
345
|
+
const iconObj = await actorObj.icon;
|
|
346
|
+
authorPhoto = iconObj?.url?.href || "";
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
/* remote fetch may fail */
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Tags / categories
|
|
353
|
+
const category = [];
|
|
354
|
+
try {
|
|
355
|
+
for await (const tag of object.getTags()) {
|
|
356
|
+
const tagName = tag.name?.toString();
|
|
357
|
+
if (tagName) category.push(tagName.replace(/^#/, ""));
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
/* ignore */
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Attachments (photos, videos, audio)
|
|
364
|
+
const photo = [];
|
|
365
|
+
const video = [];
|
|
366
|
+
const audio = [];
|
|
367
|
+
try {
|
|
368
|
+
for await (const att of object.getAttachments()) {
|
|
369
|
+
const mediaType = att.mediaType?.toString() || "";
|
|
370
|
+
const url = att.url?.href || att.id?.href || "";
|
|
371
|
+
if (!url) continue;
|
|
372
|
+
if (mediaType.startsWith("image/")) photo.push(url);
|
|
373
|
+
else if (mediaType.startsWith("video/")) video.push(url);
|
|
374
|
+
else if (mediaType.startsWith("audio/")) audio.push(url);
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
/* ignore */
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const item = {
|
|
381
|
+
channelId,
|
|
382
|
+
feedId: null,
|
|
383
|
+
uid: objectUrl,
|
|
384
|
+
type: "entry",
|
|
385
|
+
url: objectUrl,
|
|
386
|
+
name,
|
|
387
|
+
content: contentHtml ? { text: contentText, html: contentHtml } : undefined,
|
|
388
|
+
summary,
|
|
389
|
+
published: published || new Date(),
|
|
390
|
+
author: {
|
|
391
|
+
name: actorName,
|
|
392
|
+
url: actorUrl,
|
|
393
|
+
photo: authorPhoto,
|
|
394
|
+
},
|
|
395
|
+
category,
|
|
396
|
+
photo,
|
|
397
|
+
video,
|
|
398
|
+
audio,
|
|
399
|
+
inReplyTo: inReplyTo ? [inReplyTo] : [],
|
|
400
|
+
source: {
|
|
401
|
+
type: "activitypub",
|
|
402
|
+
actorUrl,
|
|
403
|
+
},
|
|
404
|
+
readBy: [],
|
|
405
|
+
createdAt: new Date().toISOString(),
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Atomic upsert — prevents duplicates without a separate check+insert
|
|
409
|
+
await collections.microsub_items.updateOne(
|
|
410
|
+
{ channelId, uid: objectUrl },
|
|
411
|
+
{ $setOnInsert: item },
|
|
412
|
+
{ upsert: true },
|
|
413
|
+
);
|
|
264
414
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.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",
|