@rmdes/indiekit-endpoint-activitypub 3.11.6 → 3.11.8

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, replyIdMap } = {}) {
47
+ export function serializeStatus(item, { baseUrl, favouritedIds, rebloggedIds, bookmarkedIds, pinnedIds, replyIdMap, replyAccountIdMap } = {}) {
48
48
  if (!item) return null;
49
49
 
50
50
  // Use published-based cursor as the status ID so pagination cursors
@@ -205,7 +205,7 @@ export function serializeStatus(item, { baseUrl, favouritedIds, rebloggedIds, bo
205
205
  id,
206
206
  created_at: published,
207
207
  in_reply_to_id: replyIdMap?.get(item.inReplyTo) ?? null,
208
- in_reply_to_account_id: null,
208
+ in_reply_to_account_id: replyAccountIdMap?.get(item.inReplyTo) ?? null,
209
209
  sensitive,
210
210
  spoiler_text: spoilerText,
211
211
  visibility,
@@ -1,19 +1,23 @@
1
1
  /**
2
- * Batch-resolve inReplyTo URLs to Mastodon cursor IDs.
2
+ * Batch-resolve inReplyTo URLs to Mastodon cursor IDs and account IDs.
3
+ *
4
+ * Looks up parent posts in ap_timeline by uid/url and returns two Maps:
5
+ * - replyIdMap: inReplyTo URL → cursor ID (status ID)
6
+ * - replyAccountIdMap: inReplyTo URL → author account ID
3
7
  *
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
8
  * Used by route handlers before calling serializeStatus().
7
9
  *
8
10
  * @param {object} collection - ap_timeline MongoDB collection
9
11
  * @param {Array<object>} items - Timeline items with optional inReplyTo
10
- * @returns {Promise<Map<string, string>>} Map of URL → cursor ID
12
+ * @returns {Promise<{replyIdMap: Map<string, string>, replyAccountIdMap: Map<string, string>}>}
11
13
  */
12
14
  import { encodeCursor } from "./pagination.js";
15
+ import { remoteActorId } from "./id-mapping.js";
13
16
 
14
17
  export async function resolveReplyIds(collection, items) {
15
- const map = new Map();
16
- if (!collection || !items?.length) return map;
18
+ const replyIdMap = new Map();
19
+ const replyAccountIdMap = new Map();
20
+ if (!collection || !items?.length) return { replyIdMap, replyAccountIdMap };
17
21
 
18
22
  // Collect unique inReplyTo URLs
19
23
  const urls = [
@@ -23,22 +27,27 @@ export async function resolveReplyIds(collection, items) {
23
27
  .filter(Boolean),
24
28
  ),
25
29
  ];
26
- if (urls.length === 0) return map;
30
+ if (urls.length === 0) return { replyIdMap, replyAccountIdMap };
27
31
 
28
32
  // Batch lookup parents by uid or url
29
33
  const parents = await collection
30
34
  .find({ $or: [{ uid: { $in: urls } }, { url: { $in: urls } }] })
31
- .project({ uid: 1, url: 1, published: 1 })
35
+ .project({ uid: 1, url: 1, published: 1, "author.url": 1 })
32
36
  .toArray();
33
37
 
34
38
  for (const parent of parents) {
35
39
  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
- }
40
+ const authorUrl = parent.author?.url;
41
+ const authorAccountId = authorUrl ? remoteActorId(authorUrl) : null;
42
+
43
+ const setMaps = (key) => {
44
+ if (cursorId && cursorId !== "0") replyIdMap.set(key, cursorId);
45
+ if (authorAccountId) replyAccountIdMap.set(key, authorAccountId);
46
+ };
47
+
48
+ if (parent.uid) setMaps(parent.uid);
49
+ if (parent.url && parent.url !== parent.uid) setMaps(parent.url);
41
50
  }
42
51
 
43
- return map;
52
+ return { replyIdMap, replyAccountIdMap };
44
53
  }
@@ -44,13 +44,14 @@ router.get("/api/v1/statuses/:id", tokenRequired, scopeRequired("read", "read:st
44
44
 
45
45
  // Load interaction state if authenticated
46
46
  const interactionState = await loadItemInteractions(collections, item);
47
- const replyIdMap = await resolveReplyIds(collections.ap_timeline, [item]);
47
+ const { replyIdMap, replyAccountIdMap } = await resolveReplyIds(collections.ap_timeline, [item]);
48
48
 
49
49
  const status = serializeStatus(item, {
50
50
  baseUrl,
51
51
  ...interactionState,
52
52
  pinnedIds: new Set(),
53
53
  replyIdMap,
54
+ replyAccountIdMap,
54
55
  });
55
56
 
56
57
  res.json(status);
@@ -126,8 +127,8 @@ router.get("/api/v1/statuses/:id/context", tokenRequired, scopeRequired("read",
126
127
  };
127
128
 
128
129
  const allItems = [...ancestors, ...descendants];
129
- const replyIdMap = await resolveReplyIds(collections.ap_timeline, allItems);
130
- const serializeOpts = { baseUrl, ...emptyInteractions, replyIdMap };
130
+ const { replyIdMap, replyAccountIdMap } = await resolveReplyIds(collections.ap_timeline, allItems);
131
+ const serializeOpts = { baseUrl, ...emptyInteractions, replyIdMap, replyAccountIdMap };
131
132
 
132
133
  res.json({
133
134
  ancestors: ancestors.map((a) => serializeStatus(a, serializeOpts)),
@@ -65,7 +65,7 @@ router.get("/api/v1/timelines/home", tokenRequired, scopeRequired("read", "read:
65
65
  );
66
66
 
67
67
  // Resolve reply parent IDs for threading
68
- const replyIdMap = await resolveReplyIds(collections.ap_timeline, items);
68
+ const { replyIdMap, replyAccountIdMap } = await resolveReplyIds(collections.ap_timeline, items);
69
69
 
70
70
  // Serialize to Mastodon Status entities
71
71
  const statuses = items.map((item) =>
@@ -76,6 +76,7 @@ router.get("/api/v1/timelines/home", tokenRequired, scopeRequired("read", "read:
76
76
  bookmarkedIds,
77
77
  pinnedIds: new Set(),
78
78
  replyIdMap,
79
+ replyAccountIdMap,
79
80
  }),
80
81
  );
81
82
 
@@ -168,7 +169,7 @@ router.get("/api/v1/timelines/public", async (req, res, next) => {
168
169
  ));
169
170
  }
170
171
 
171
- const replyIdMap = await resolveReplyIds(collections.ap_timeline, items);
172
+ const { replyIdMap: rIdMap, replyAccountIdMap: rAcctMap } = await resolveReplyIds(collections.ap_timeline, items);
172
173
 
173
174
  const statuses = items.map((item) =>
174
175
  serializeStatus(item, {
@@ -177,7 +178,8 @@ router.get("/api/v1/timelines/public", async (req, res, next) => {
177
178
  rebloggedIds,
178
179
  bookmarkedIds,
179
180
  pinnedIds: new Set(),
180
- replyIdMap,
181
+ replyIdMap: rIdMap,
182
+ replyAccountIdMap: rAcctMap,
181
183
  }),
182
184
  );
183
185
 
@@ -234,7 +236,7 @@ router.get("/api/v1/timelines/tag/:hashtag", async (req, res, next) => {
234
236
  ));
235
237
  }
236
238
 
237
- const replyIdMap = await resolveReplyIds(collections.ap_timeline, items);
239
+ const { replyIdMap: rIdMap, replyAccountIdMap: rAcctMap } = await resolveReplyIds(collections.ap_timeline, items);
238
240
 
239
241
  const statuses = items.map((item) =>
240
242
  serializeStatus(item, {
@@ -243,7 +245,8 @@ router.get("/api/v1/timelines/tag/:hashtag", async (req, res, next) => {
243
245
  rebloggedIds,
244
246
  bookmarkedIds,
245
247
  pinnedIds: new Set(),
246
- replyIdMap,
248
+ replyIdMap: rIdMap,
249
+ replyAccountIdMap: rAcctMap,
247
250
  }),
248
251
  );
249
252
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "3.11.6",
3
+ "version": "3.11.8",
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",