@rmdes/indiekit-endpoint-activitypub 3.11.4 → 3.11.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.
|
@@ -44,7 +44,7 @@ export function setLocalIdentity(publicationUrl, handle) {
|
|
|
44
44
|
* @param {Set<string>} [options.pinnedIds] - UIDs the user has pinned
|
|
45
45
|
* @returns {object} Mastodon Status entity
|
|
46
46
|
*/
|
|
47
|
-
export function serializeStatus(item, { baseUrl, favouritedIds, rebloggedIds, bookmarkedIds, pinnedIds }) {
|
|
47
|
+
export function serializeStatus(item, { baseUrl, favouritedIds, rebloggedIds, bookmarkedIds, pinnedIds, replyIdMap } = {}) {
|
|
48
48
|
if (!item) return null;
|
|
49
49
|
|
|
50
50
|
// Use published-based cursor as the status ID so pagination cursors
|
|
@@ -204,8 +204,8 @@ export function serializeStatus(item, { baseUrl, favouritedIds, rebloggedIds, bo
|
|
|
204
204
|
return {
|
|
205
205
|
id,
|
|
206
206
|
created_at: published,
|
|
207
|
-
in_reply_to_id: item.inReplyTo
|
|
208
|
-
in_reply_to_account_id: null,
|
|
207
|
+
in_reply_to_id: replyIdMap?.get(item.inReplyTo) ?? null,
|
|
208
|
+
in_reply_to_account_id: null,
|
|
209
209
|
sensitive,
|
|
210
210
|
spoiler_text: spoilerText,
|
|
211
211
|
visibility,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch-resolve inReplyTo URLs to Mastodon cursor IDs.
|
|
3
|
+
*
|
|
4
|
+
* Looks up parent posts in ap_timeline by uid/url and returns a Map
|
|
5
|
+
* of inReplyTo URL → cursor ID (milliseconds since epoch as string).
|
|
6
|
+
* Used by route handlers before calling serializeStatus().
|
|
7
|
+
*
|
|
8
|
+
* @param {object} collection - ap_timeline MongoDB collection
|
|
9
|
+
* @param {Array<object>} items - Timeline items with optional inReplyTo
|
|
10
|
+
* @returns {Promise<Map<string, string>>} Map of URL → cursor ID
|
|
11
|
+
*/
|
|
12
|
+
import { encodeCursor } from "./pagination.js";
|
|
13
|
+
|
|
14
|
+
export async function resolveReplyIds(collection, items) {
|
|
15
|
+
const map = new Map();
|
|
16
|
+
if (!collection || !items?.length) return map;
|
|
17
|
+
|
|
18
|
+
// Collect unique inReplyTo URLs
|
|
19
|
+
const urls = [
|
|
20
|
+
...new Set(
|
|
21
|
+
items
|
|
22
|
+
.map((item) => item.inReplyTo)
|
|
23
|
+
.filter(Boolean),
|
|
24
|
+
),
|
|
25
|
+
];
|
|
26
|
+
if (urls.length === 0) return map;
|
|
27
|
+
|
|
28
|
+
// Batch lookup parents by uid or url
|
|
29
|
+
const parents = await collection
|
|
30
|
+
.find({ $or: [{ uid: { $in: urls } }, { url: { $in: urls } }] })
|
|
31
|
+
.project({ uid: 1, url: 1, published: 1 })
|
|
32
|
+
.toArray();
|
|
33
|
+
|
|
34
|
+
for (const parent of parents) {
|
|
35
|
+
const cursorId = encodeCursor(parent.published);
|
|
36
|
+
if (cursorId && cursorId !== "0") {
|
|
37
|
+
// Map both uid and url to the cursor ID
|
|
38
|
+
if (parent.uid) map.set(parent.uid, cursorId);
|
|
39
|
+
if (parent.url && parent.url !== parent.uid) map.set(parent.url, cursorId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return map;
|
|
44
|
+
}
|
|
@@ -18,6 +18,7 @@ import express from "express";
|
|
|
18
18
|
import { ObjectId } from "mongodb";
|
|
19
19
|
import { serializeStatus } from "../entities/status.js";
|
|
20
20
|
import { decodeCursor } from "../helpers/pagination.js";
|
|
21
|
+
import { resolveReplyIds } from "../helpers/resolve-reply-ids.js";
|
|
21
22
|
import {
|
|
22
23
|
likePost, unlikePost,
|
|
23
24
|
boostPost, unboostPost,
|
|
@@ -43,11 +44,13 @@ router.get("/api/v1/statuses/:id", tokenRequired, scopeRequired("read", "read:st
|
|
|
43
44
|
|
|
44
45
|
// Load interaction state if authenticated
|
|
45
46
|
const interactionState = await loadItemInteractions(collections, item);
|
|
47
|
+
const replyIdMap = await resolveReplyIds(collections.ap_timeline, [item]);
|
|
46
48
|
|
|
47
49
|
const status = serializeStatus(item, {
|
|
48
50
|
baseUrl,
|
|
49
51
|
...interactionState,
|
|
50
52
|
pinnedIds: new Set(),
|
|
53
|
+
replyIdMap,
|
|
51
54
|
});
|
|
52
55
|
|
|
53
56
|
res.json(status);
|
|
@@ -122,7 +125,9 @@ router.get("/api/v1/statuses/:id/context", tokenRequired, scopeRequired("read",
|
|
|
122
125
|
pinnedIds: new Set(),
|
|
123
126
|
};
|
|
124
127
|
|
|
125
|
-
const
|
|
128
|
+
const allItems = [...ancestors, ...descendants];
|
|
129
|
+
const replyIdMap = await resolveReplyIds(collections.ap_timeline, allItems);
|
|
130
|
+
const serializeOpts = { baseUrl, ...emptyInteractions, replyIdMap };
|
|
126
131
|
|
|
127
132
|
res.json({
|
|
128
133
|
ancestors: ancestors.map((a) => serializeStatus(a, serializeOpts)),
|
|
@@ -245,7 +250,7 @@ router.post("/api/v1/statuses", tokenRequired, scopeRequired("write", "write:sta
|
|
|
245
250
|
for (const m of mediaUrls) {
|
|
246
251
|
if (m.type?.startsWith("image/")) {
|
|
247
252
|
if (!jf2.photo) jf2.photo = [];
|
|
248
|
-
jf2.photo.push({
|
|
253
|
+
jf2.photo.push({ url: m.url, alt: m.alt });
|
|
249
254
|
} else if (m.type?.startsWith("video/")) {
|
|
250
255
|
if (!jf2.video) jf2.video = [];
|
|
251
256
|
jf2.video.push(m.url);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import express from "express";
|
|
9
9
|
import { serializeStatus } from "../entities/status.js";
|
|
10
10
|
import { buildPaginationQuery, parseLimit, setPaginationHeaders } from "../helpers/pagination.js";
|
|
11
|
+
import { resolveReplyIds } from "../helpers/resolve-reply-ids.js";
|
|
11
12
|
import { loadModerationData, applyModerationFilters } from "../../item-processing.js";
|
|
12
13
|
import { enrichAccountStats } from "../helpers/enrich-accounts.js";
|
|
13
14
|
import { tokenRequired } from "../middleware/token-required.js";
|
|
@@ -63,6 +64,9 @@ router.get("/api/v1/timelines/home", tokenRequired, scopeRequired("read", "read:
|
|
|
63
64
|
items,
|
|
64
65
|
);
|
|
65
66
|
|
|
67
|
+
// Resolve reply parent IDs for threading
|
|
68
|
+
const replyIdMap = await resolveReplyIds(collections.ap_timeline, items);
|
|
69
|
+
|
|
66
70
|
// Serialize to Mastodon Status entities
|
|
67
71
|
const statuses = items.map((item) =>
|
|
68
72
|
serializeStatus(item, {
|
|
@@ -71,6 +75,7 @@ router.get("/api/v1/timelines/home", tokenRequired, scopeRequired("read", "read:
|
|
|
71
75
|
rebloggedIds,
|
|
72
76
|
bookmarkedIds,
|
|
73
77
|
pinnedIds: new Set(),
|
|
78
|
+
replyIdMap,
|
|
74
79
|
}),
|
|
75
80
|
);
|
|
76
81
|
|
|
@@ -163,6 +168,8 @@ router.get("/api/v1/timelines/public", async (req, res, next) => {
|
|
|
163
168
|
));
|
|
164
169
|
}
|
|
165
170
|
|
|
171
|
+
const replyIdMap = await resolveReplyIds(collections.ap_timeline, items);
|
|
172
|
+
|
|
166
173
|
const statuses = items.map((item) =>
|
|
167
174
|
serializeStatus(item, {
|
|
168
175
|
baseUrl,
|
|
@@ -170,6 +177,7 @@ router.get("/api/v1/timelines/public", async (req, res, next) => {
|
|
|
170
177
|
rebloggedIds,
|
|
171
178
|
bookmarkedIds,
|
|
172
179
|
pinnedIds: new Set(),
|
|
180
|
+
replyIdMap,
|
|
173
181
|
}),
|
|
174
182
|
);
|
|
175
183
|
|
|
@@ -226,6 +234,8 @@ router.get("/api/v1/timelines/tag/:hashtag", async (req, res, next) => {
|
|
|
226
234
|
));
|
|
227
235
|
}
|
|
228
236
|
|
|
237
|
+
const replyIdMap = await resolveReplyIds(collections.ap_timeline, items);
|
|
238
|
+
|
|
229
239
|
const statuses = items.map((item) =>
|
|
230
240
|
serializeStatus(item, {
|
|
231
241
|
baseUrl,
|
|
@@ -233,6 +243,7 @@ router.get("/api/v1/timelines/tag/:hashtag", async (req, res, next) => {
|
|
|
233
243
|
rebloggedIds,
|
|
234
244
|
bookmarkedIds,
|
|
235
245
|
pinnedIds: new Set(),
|
|
246
|
+
replyIdMap,
|
|
236
247
|
}),
|
|
237
248
|
);
|
|
238
249
|
|
package/lib/syndicator.js
CHANGED
|
@@ -344,8 +344,19 @@ function buildTimelineContent(properties) {
|
|
|
344
344
|
};
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
// Regular post —
|
|
347
|
+
// Regular post — append permalink to match federated AS2 content.
|
|
348
|
+
// Without this, the Mastodon API timeline entry lacks the link back
|
|
349
|
+
// to the source post that fediverse users see via federation.
|
|
348
350
|
if (bodyText || bodyHtml) {
|
|
351
|
+
const postUrl = properties.url;
|
|
352
|
+
if (postUrl) {
|
|
353
|
+
const linkText = `\n\n\u{1F517} ${postUrl}`;
|
|
354
|
+
const linkHtml = `<p>\u{1F517} <a href="${esc(postUrl)}">${esc(postUrl)}</a></p>`;
|
|
355
|
+
return {
|
|
356
|
+
text: `${bodyText}${linkText}`,
|
|
357
|
+
html: `${bodyHtml}\n${linkHtml}`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
349
360
|
return { text: bodyText, html: bodyHtml };
|
|
350
361
|
}
|
|
351
362
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.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",
|