@rmdes/indiekit-endpoint-conversations 2.1.2 → 2.1.4
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.
|
@@ -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,138 @@ 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.
|
|
104
|
+
* Tries four strategies in order:
|
|
105
|
+
* 1. ap_notifications (actorPhoto)
|
|
106
|
+
* 2. ap_activities (actorAvatar — new field from inbox handler fix)
|
|
107
|
+
* 3. ap_followers (avatar)
|
|
108
|
+
* 4. Live fetch of the actor's ActivityPub profile (icon field)
|
|
109
|
+
*/
|
|
110
|
+
async function backfillMissingAvatars(indiekit, stateCollection) {
|
|
111
|
+
try {
|
|
112
|
+
const itemsCollection = indiekit.collections.get("conversation_items");
|
|
113
|
+
const notificationsCollection = indiekit.collections.get("ap_notifications");
|
|
114
|
+
const activitiesCollection = indiekit.collections.get("ap_activities");
|
|
115
|
+
const followersCollection = indiekit.collections.get("ap_followers");
|
|
116
|
+
|
|
117
|
+
if (!itemsCollection) return;
|
|
118
|
+
|
|
119
|
+
// Check if backfill already completed
|
|
120
|
+
const state = await stateCollection.findOne({ _id: "poll_cursors" });
|
|
121
|
+
if (state?.avatar_backfill_complete) return;
|
|
122
|
+
|
|
123
|
+
// Find conversation_items with empty author.photo
|
|
124
|
+
const itemsWithoutPhoto = await itemsCollection
|
|
125
|
+
.find({
|
|
126
|
+
$or: [
|
|
127
|
+
{ "author.photo": "" },
|
|
128
|
+
{ "author.photo": null },
|
|
129
|
+
{ "author.photo": { $exists: false } },
|
|
130
|
+
],
|
|
131
|
+
})
|
|
132
|
+
.limit(200)
|
|
133
|
+
.toArray();
|
|
134
|
+
|
|
135
|
+
if (itemsWithoutPhoto.length === 0) {
|
|
136
|
+
// Mark backfill as complete so we don't query every cycle
|
|
137
|
+
await stateCollection.findOneAndUpdate(
|
|
138
|
+
{ _id: "poll_cursors" },
|
|
139
|
+
{ $set: { avatar_backfill_complete: true } },
|
|
140
|
+
{ upsert: true },
|
|
141
|
+
);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let updated = 0;
|
|
146
|
+
|
|
147
|
+
for (const item of itemsWithoutPhoto) {
|
|
148
|
+
const actorUrl = item.author?.url;
|
|
149
|
+
if (!actorUrl) continue;
|
|
150
|
+
|
|
151
|
+
let photo = "";
|
|
152
|
+
|
|
153
|
+
// Strategy 1: Check ap_notifications (most reliable — has actorPhoto for all interaction types)
|
|
154
|
+
if (!photo && notificationsCollection) {
|
|
155
|
+
try {
|
|
156
|
+
const notification = await notificationsCollection.findOne({
|
|
157
|
+
actorUrl,
|
|
158
|
+
actorPhoto: { $ne: "" },
|
|
159
|
+
});
|
|
160
|
+
if (notification?.actorPhoto) photo = notification.actorPhoto;
|
|
161
|
+
} catch { /* ignore */ }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Strategy 2: Check ap_activities for actorAvatar (new field from inbox handler fix)
|
|
165
|
+
if (!photo && activitiesCollection) {
|
|
166
|
+
try {
|
|
167
|
+
const activity = await activitiesCollection.findOne({
|
|
168
|
+
actorUrl,
|
|
169
|
+
actorAvatar: { $ne: "" },
|
|
170
|
+
});
|
|
171
|
+
if (activity?.actorAvatar) photo = activity.actorAvatar;
|
|
172
|
+
} catch { /* ignore */ }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Strategy 3: Check ap_followers
|
|
176
|
+
if (!photo && followersCollection) {
|
|
177
|
+
try {
|
|
178
|
+
const follower = await followersCollection.findOne({ actorUrl });
|
|
179
|
+
if (follower?.avatar) photo = follower.avatar;
|
|
180
|
+
} catch { /* ignore */ }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Strategy 4: Fetch actor profile from the fediverse (last resort)
|
|
184
|
+
if (!photo && actorUrl) {
|
|
185
|
+
try {
|
|
186
|
+
const resp = await fetch(actorUrl, {
|
|
187
|
+
headers: { Accept: "application/activity+json, application/ld+json" },
|
|
188
|
+
signal: AbortSignal.timeout(5000),
|
|
189
|
+
});
|
|
190
|
+
if (resp.ok) {
|
|
191
|
+
const actor = await resp.json();
|
|
192
|
+
const icon = actor.icon;
|
|
193
|
+
if (typeof icon === "string") {
|
|
194
|
+
photo = icon;
|
|
195
|
+
} else if (icon?.url) {
|
|
196
|
+
photo = icon.url;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch { /* timeout or network error — skip */ }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (photo) {
|
|
203
|
+
await itemsCollection.updateMany(
|
|
204
|
+
{ "author.url": actorUrl, "author.photo": { $in: ["", null] } },
|
|
205
|
+
{ $set: { "author.photo": photo } },
|
|
206
|
+
);
|
|
207
|
+
updated++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (updated > 0) {
|
|
212
|
+
console.info(
|
|
213
|
+
`[Conversations] Avatar backfill: updated ${updated} actors with photos`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// If fewer than 200 items found, backfill is complete
|
|
218
|
+
if (itemsWithoutPhoto.length < 200) {
|
|
219
|
+
await stateCollection.findOneAndUpdate(
|
|
220
|
+
{ _id: "poll_cursors" },
|
|
221
|
+
{ $set: { avatar_backfill_complete: true } },
|
|
222
|
+
{ upsert: true },
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
// Non-critical — log and continue
|
|
227
|
+
console.warn("[Conversations] Avatar backfill error:", error.message);
|
|
228
|
+
}
|
|
97
229
|
}
|
|
98
230
|
|
|
99
231
|
/**
|
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.4",
|
|
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",
|