@rmdes/indiekit-endpoint-conversations 2.1.1 → 2.1.3
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/lib/notifications/activitypub.js +5 -1
- package/lib/polling/scheduler.js +144 -15
- package/package.json +1 -1
|
@@ -48,7 +48,11 @@ export async function fetchActivityPubInteractions(options) {
|
|
|
48
48
|
const items = [];
|
|
49
49
|
|
|
50
50
|
for (const activity of activities) {
|
|
51
|
-
|
|
51
|
+
// Prefer avatar stored directly on the activity (added by inbox handler),
|
|
52
|
+
// fall back to ap_followers lookup for historical data without actorAvatar
|
|
53
|
+
const avatar =
|
|
54
|
+
activity.actorAvatar ||
|
|
55
|
+
(await lookupAvatar(ap_followers, activity.actorUrl));
|
|
52
56
|
items.push(normalizeActivity(activity, avatar));
|
|
53
57
|
}
|
|
54
58
|
|
package/lib/polling/scheduler.js
CHANGED
|
@@ -94,6 +94,117 @@ export async function runPollCycle(indiekit, options) {
|
|
|
94
94
|
}
|
|
95
95
|
await pollActivityPub(indiekit, stateCollection, state);
|
|
96
96
|
}
|
|
97
|
+
|
|
98
|
+
// Backfill missing avatars from ap_notifications (one-time sweep per cycle)
|
|
99
|
+
await backfillMissingAvatars(indiekit, stateCollection);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Backfill empty author.photo fields in conversation_items using ap_notifications.
|
|
104
|
+
* The AP endpoint stores actorPhoto in notifications for all interactions,
|
|
105
|
+
* so we can use those to enrich conversation_items that were stored before
|
|
106
|
+
* the actorAvatar field was added to ap_activities.
|
|
107
|
+
*/
|
|
108
|
+
async function backfillMissingAvatars(indiekit, stateCollection) {
|
|
109
|
+
try {
|
|
110
|
+
const itemsCollection = indiekit.collections.get("conversation_items");
|
|
111
|
+
const notificationsCollection = indiekit.collections.get("ap_notifications");
|
|
112
|
+
const activitiesCollection = indiekit.collections.get("ap_activities");
|
|
113
|
+
const followersCollection = indiekit.collections.get("ap_followers");
|
|
114
|
+
|
|
115
|
+
if (!itemsCollection) return;
|
|
116
|
+
|
|
117
|
+
// Check if backfill already completed
|
|
118
|
+
const state = await stateCollection.findOne({ _id: "poll_cursors" });
|
|
119
|
+
if (state?.avatar_backfill_complete) return;
|
|
120
|
+
|
|
121
|
+
// Find conversation_items with empty author.photo
|
|
122
|
+
const itemsWithoutPhoto = await itemsCollection
|
|
123
|
+
.find({
|
|
124
|
+
$or: [
|
|
125
|
+
{ "author.photo": "" },
|
|
126
|
+
{ "author.photo": null },
|
|
127
|
+
{ "author.photo": { $exists: false } },
|
|
128
|
+
],
|
|
129
|
+
})
|
|
130
|
+
.limit(200)
|
|
131
|
+
.toArray();
|
|
132
|
+
|
|
133
|
+
if (itemsWithoutPhoto.length === 0) {
|
|
134
|
+
// Mark backfill as complete so we don't query every cycle
|
|
135
|
+
await stateCollection.findOneAndUpdate(
|
|
136
|
+
{ _id: "poll_cursors" },
|
|
137
|
+
{ $set: { avatar_backfill_complete: true } },
|
|
138
|
+
{ upsert: true },
|
|
139
|
+
);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let updated = 0;
|
|
144
|
+
|
|
145
|
+
for (const item of itemsWithoutPhoto) {
|
|
146
|
+
const actorUrl = item.author?.url;
|
|
147
|
+
if (!actorUrl) continue;
|
|
148
|
+
|
|
149
|
+
let photo = "";
|
|
150
|
+
|
|
151
|
+
// Strategy 1: Check ap_notifications (most reliable — has actorPhoto for all interaction types)
|
|
152
|
+
if (!photo && notificationsCollection) {
|
|
153
|
+
try {
|
|
154
|
+
const notification = await notificationsCollection.findOne({
|
|
155
|
+
actorUrl,
|
|
156
|
+
actorPhoto: { $ne: "" },
|
|
157
|
+
});
|
|
158
|
+
if (notification?.actorPhoto) photo = notification.actorPhoto;
|
|
159
|
+
} catch { /* ignore */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Strategy 2: Check ap_activities for actorAvatar (new field from inbox handler fix)
|
|
163
|
+
if (!photo && activitiesCollection) {
|
|
164
|
+
try {
|
|
165
|
+
const activity = await activitiesCollection.findOne({
|
|
166
|
+
actorUrl,
|
|
167
|
+
actorAvatar: { $ne: "" },
|
|
168
|
+
});
|
|
169
|
+
if (activity?.actorAvatar) photo = activity.actorAvatar;
|
|
170
|
+
} catch { /* ignore */ }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Strategy 3: Check ap_followers
|
|
174
|
+
if (!photo && followersCollection) {
|
|
175
|
+
try {
|
|
176
|
+
const follower = await followersCollection.findOne({ actorUrl });
|
|
177
|
+
if (follower?.avatar) photo = follower.avatar;
|
|
178
|
+
} catch { /* ignore */ }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (photo) {
|
|
182
|
+
await itemsCollection.updateMany(
|
|
183
|
+
{ "author.url": actorUrl, "author.photo": { $in: ["", null] } },
|
|
184
|
+
{ $set: { "author.photo": photo } },
|
|
185
|
+
);
|
|
186
|
+
updated++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (updated > 0) {
|
|
191
|
+
console.info(
|
|
192
|
+
`[Conversations] Avatar backfill: updated ${updated} actors with photos`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// If fewer than 200 items found, backfill is complete
|
|
197
|
+
if (itemsWithoutPhoto.length < 200) {
|
|
198
|
+
await stateCollection.findOneAndUpdate(
|
|
199
|
+
{ _id: "poll_cursors" },
|
|
200
|
+
{ $set: { avatar_backfill_complete: true } },
|
|
201
|
+
{ upsert: true },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
// Non-critical — log and continue
|
|
206
|
+
console.warn("[Conversations] Avatar backfill error:", error.message);
|
|
207
|
+
}
|
|
97
208
|
}
|
|
98
209
|
|
|
99
210
|
/**
|
|
@@ -323,6 +434,16 @@ async function pollActivityPub(indiekit, stateCollection, state) {
|
|
|
323
434
|
|
|
324
435
|
if (!ap_activities) return;
|
|
325
436
|
|
|
437
|
+
// Resolve the site URL so we only store interactions about OUR content.
|
|
438
|
+
// indiekit may be the Indiekit class (from startPolling) or the
|
|
439
|
+
// application object (from triggerPoll via controller).
|
|
440
|
+
const siteUrl = (
|
|
441
|
+
indiekit.publication?.me ||
|
|
442
|
+
indiekit.url ||
|
|
443
|
+
process.env.PUBLICATION_URL ||
|
|
444
|
+
""
|
|
445
|
+
).replace(/\/$/, "");
|
|
446
|
+
|
|
326
447
|
const result = await fetchActivityPubInteractions({
|
|
327
448
|
ap_activities,
|
|
328
449
|
ap_followers,
|
|
@@ -330,22 +451,30 @@ async function pollActivityPub(indiekit, stateCollection, state) {
|
|
|
330
451
|
});
|
|
331
452
|
|
|
332
453
|
let stored = 0;
|
|
454
|
+
let skipped = 0;
|
|
333
455
|
|
|
334
456
|
for (const interaction of result.items) {
|
|
335
|
-
if (interaction.canonical_url)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
url: interaction.url,
|
|
343
|
-
bridgy_url: null,
|
|
344
|
-
platform_id: interaction.platform_id,
|
|
345
|
-
created_at: interaction.created_at,
|
|
346
|
-
});
|
|
347
|
-
stored++;
|
|
457
|
+
if (!interaction.canonical_url) continue;
|
|
458
|
+
|
|
459
|
+
// Only store interactions targeting our own content — skip activities
|
|
460
|
+
// about posts on other domains (e.g. forwarded via shared inbox).
|
|
461
|
+
if (siteUrl && !interaction.canonical_url.startsWith(siteUrl)) {
|
|
462
|
+
skipped++;
|
|
463
|
+
continue;
|
|
348
464
|
}
|
|
465
|
+
|
|
466
|
+
await upsertConversationItem(indiekit, {
|
|
467
|
+
canonical_url: interaction.canonical_url,
|
|
468
|
+
source: "activitypub",
|
|
469
|
+
type: interaction.type,
|
|
470
|
+
author: interaction.author,
|
|
471
|
+
content: interaction.content,
|
|
472
|
+
url: interaction.url,
|
|
473
|
+
bridgy_url: null,
|
|
474
|
+
platform_id: interaction.platform_id,
|
|
475
|
+
created_at: interaction.created_at,
|
|
476
|
+
});
|
|
477
|
+
stored++;
|
|
349
478
|
}
|
|
350
479
|
|
|
351
480
|
// Update cursor and status
|
|
@@ -363,9 +492,9 @@ async function pollActivityPub(indiekit, stateCollection, state) {
|
|
|
363
492
|
{ upsert: true },
|
|
364
493
|
);
|
|
365
494
|
|
|
366
|
-
if (stored > 0) {
|
|
495
|
+
if (stored > 0 || skipped > 0) {
|
|
367
496
|
console.info(
|
|
368
|
-
`[Conversations] ActivityPub: stored ${stored}
|
|
497
|
+
`[Conversations] ActivityPub: stored ${stored}, skipped ${skipped} (not our content) of ${result.items.length} interactions`,
|
|
369
498
|
);
|
|
370
499
|
}
|
|
371
500
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-conversations",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "Conversation aggregation endpoint for Indiekit. Backend enrichment service that polls Mastodon/Bluesky notifications and serves JF2-compatible data for the interactions page.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"indiekit",
|