@rmdes/indiekit-endpoint-conversations 2.2.0 → 2.3.0
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.
|
@@ -166,54 +166,99 @@ async function apiMentions(request, response) {
|
|
|
166
166
|
const children = items.map(conversationItemToJf2);
|
|
167
167
|
|
|
168
168
|
// Enrich with owner replies from the posts collection
|
|
169
|
-
// Owner replies are Micropub posts with in-reply-to matching an interaction URL
|
|
169
|
+
// Owner replies are Micropub posts with in-reply-to matching an interaction URL.
|
|
170
|
+
// We collect reply URLs from conversations DB items, but also need to find
|
|
171
|
+
// owner replies to interactions that only exist in webmention.io (e.g., Bluesky
|
|
172
|
+
// replies via Bridgy). Strategy: query for reply URLs from conversations items,
|
|
173
|
+
// plus find owner posts replying to any URL that the frontend might display
|
|
174
|
+
// by checking the canonical post's syndication targets.
|
|
170
175
|
const replyUrls = children
|
|
171
176
|
.filter((c) => c["wm-property"] === "in-reply-to")
|
|
172
177
|
.map((c) => c.url)
|
|
173
178
|
.filter(Boolean);
|
|
174
179
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
180
|
+
const postsCollection = application.collections?.get("posts");
|
|
181
|
+
if (postsCollection) {
|
|
182
|
+
const siteUrl = application.publication?.me || application.url || "";
|
|
183
|
+
const ownerName =
|
|
184
|
+
process.env.AUTHOR_NAME ||
|
|
185
|
+
(siteUrl ? new URL(siteUrl).hostname : "Owner");
|
|
186
|
+
|
|
187
|
+
// Find the canonical post to get its syndication URLs
|
|
188
|
+
// Interactions on syndicated copies (e.g., Bluesky replies to the bsky.app
|
|
189
|
+
// syndicated post) arrive via webmention.io but not conversations DB.
|
|
190
|
+
// Owner replies to those interactions have in-reply-to pointing to external
|
|
191
|
+
// URLs (bsky.app, mastodon, etc.) — we need to find them too.
|
|
192
|
+
let syndicationDomains = [];
|
|
193
|
+
if (target) {
|
|
194
|
+
const targetWithout = target.endsWith("/") ? target.slice(0, -1) : target;
|
|
195
|
+
const canonicalPost = await postsCollection.findOne({
|
|
196
|
+
$or: [
|
|
197
|
+
{ "properties.url": target },
|
|
198
|
+
{ "properties.url": targetWithout },
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
if (canonicalPost?.properties?.syndication) {
|
|
202
|
+
const syns = Array.isArray(canonicalPost.properties.syndication)
|
|
203
|
+
? canonicalPost.properties.syndication
|
|
204
|
+
: [canonicalPost.properties.syndication];
|
|
205
|
+
for (const syn of syns) {
|
|
206
|
+
try {
|
|
207
|
+
const domain = new URL(syn).hostname;
|
|
208
|
+
if (domain && !domain.includes(new URL(siteUrl).hostname)) {
|
|
209
|
+
syndicationDomains.push(domain);
|
|
210
|
+
}
|
|
211
|
+
} catch { /* skip invalid URLs */ }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Build query: replies to known conversation URLs OR replies to URLs
|
|
217
|
+
// on syndication domains (for webmention.io items not in our DB)
|
|
218
|
+
const orClauses = [];
|
|
219
|
+
if (replyUrls.length > 0) {
|
|
220
|
+
orClauses.push({ "properties.in-reply-to": { $in: replyUrls } });
|
|
221
|
+
}
|
|
222
|
+
for (const domain of syndicationDomains) {
|
|
223
|
+
orClauses.push({
|
|
224
|
+
"properties.in-reply-to": { $regex: domain.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let ownerPosts = [];
|
|
229
|
+
if (orClauses.length > 0) {
|
|
230
|
+
ownerPosts = await postsCollection
|
|
231
|
+
.find({ $or: orClauses })
|
|
187
232
|
.sort({ "properties.published": -1 })
|
|
188
233
|
.limit(50)
|
|
189
234
|
.toArray();
|
|
235
|
+
}
|
|
190
236
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
237
|
+
for (const post of ownerPosts) {
|
|
238
|
+
const inReplyTo = post.properties?.["in-reply-to"];
|
|
239
|
+
if (!inReplyTo || typeof inReplyTo !== "string") continue;
|
|
240
|
+
|
|
241
|
+
children.push({
|
|
242
|
+
type: "entry",
|
|
243
|
+
"wm-id": `owner-reply-${post._id}`,
|
|
244
|
+
"wm-property": "in-reply-to",
|
|
245
|
+
"wm-target": target || "",
|
|
246
|
+
"wm-received": post.properties?.published || "",
|
|
247
|
+
author: {
|
|
248
|
+
type: "card",
|
|
249
|
+
name: ownerName,
|
|
250
|
+
url: siteUrl,
|
|
251
|
+
photo: process.env.AUTHOR_AVATAR || "",
|
|
252
|
+
},
|
|
253
|
+
url: post.properties?.url || "",
|
|
254
|
+
published: post.properties?.published || "",
|
|
255
|
+
content: {
|
|
256
|
+
text: post.properties?.content?.text || "",
|
|
257
|
+
html: post.properties?.content?.html || "",
|
|
258
|
+
},
|
|
259
|
+
is_owner: true,
|
|
260
|
+
parent_url: inReplyTo,
|
|
261
|
+
});
|
|
217
262
|
}
|
|
218
263
|
}
|
|
219
264
|
|
|
@@ -27,9 +27,11 @@ export async function fetchBlueskyNotifications(options) {
|
|
|
27
27
|
// Get or refresh session
|
|
28
28
|
const session = await getSession(serviceUrl, identifier, password);
|
|
29
29
|
|
|
30
|
-
// Fetch notifications
|
|
30
|
+
// Fetch the most recent notifications (no cursor)
|
|
31
|
+
// Bluesky's listNotifications returns newest first; the cursor pages backward
|
|
32
|
+
// into history. For polling, we always want the latest batch and rely on
|
|
33
|
+
// upsert deduplication (platform_id) to skip already-stored items.
|
|
31
34
|
const params = new URLSearchParams({ limit: "50" });
|
|
32
|
-
if (options.cursor) params.set("cursor", options.cursor);
|
|
33
35
|
|
|
34
36
|
let notifResponse = await fetch(
|
|
35
37
|
`${serviceUrl}/xrpc/app.bsky.notification.listNotifications?${params.toString()}`,
|
package/lib/polling/scheduler.js
CHANGED
|
@@ -425,7 +425,6 @@ async function pollBluesky(indiekit, stateCollection, state, credentials) {
|
|
|
425
425
|
const result = await fetchBlueskyNotifications({
|
|
426
426
|
identifier: credentials.identifier,
|
|
427
427
|
password: credentials.password,
|
|
428
|
-
cursor: state.bluesky_cursor,
|
|
429
428
|
});
|
|
430
429
|
|
|
431
430
|
let stored = 0;
|
|
@@ -457,22 +456,19 @@ async function pollBluesky(indiekit, stateCollection, state, credentials) {
|
|
|
457
456
|
}
|
|
458
457
|
}
|
|
459
458
|
|
|
460
|
-
// Update
|
|
461
|
-
const updateFields = {
|
|
462
|
-
bluesky_last_poll: new Date().toISOString(),
|
|
463
|
-
bluesky_last_error: null,
|
|
464
|
-
};
|
|
465
|
-
if (result.cursor) {
|
|
466
|
-
updateFields.bluesky_cursor = result.cursor;
|
|
467
|
-
}
|
|
468
|
-
|
|
459
|
+
// Update poll timestamp
|
|
469
460
|
await stateCollection.findOneAndUpdate(
|
|
470
461
|
{ _id: "poll_cursors" },
|
|
471
|
-
{
|
|
462
|
+
{
|
|
463
|
+
$set: {
|
|
464
|
+
bluesky_last_poll: new Date().toISOString(),
|
|
465
|
+
bluesky_last_error: null,
|
|
466
|
+
},
|
|
467
|
+
},
|
|
472
468
|
{ upsert: true },
|
|
473
469
|
);
|
|
474
470
|
|
|
475
|
-
if (stored > 0) {
|
|
471
|
+
if (stored > 0 || result.items.length > 0) {
|
|
476
472
|
console.info(
|
|
477
473
|
`[Conversations] Bluesky: stored ${stored}/${result.items.length} interactions`,
|
|
478
474
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-conversations",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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",
|