@rmdes/indiekit-endpoint-activitypub 3.4.0 → 3.5.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.
@@ -984,6 +984,24 @@ export async function handleUpdate(item, collections, ctx, handle) {
984
984
 
985
985
  const existing = await collections.ap_followers.findOne({ actorUrl });
986
986
  if (existing) {
987
+ let updatedAvatar = "";
988
+ try {
989
+ updatedAvatar = actorObj.icon
990
+ ? (await actorObj.icon)?.url?.href || ""
991
+ : "";
992
+ } catch {
993
+ // Icon fetch failed
994
+ }
995
+
996
+ let updatedBanner = "";
997
+ try {
998
+ updatedBanner = actorObj.image
999
+ ? (await actorObj.image)?.url?.href || ""
1000
+ : "";
1001
+ } catch {
1002
+ // Banner fetch failed
1003
+ }
1004
+
987
1005
  await collections.ap_followers.updateOne(
988
1006
  { actorUrl },
989
1007
  {
@@ -993,9 +1011,8 @@ export async function handleUpdate(item, collections, ctx, handle) {
993
1011
  actorObj.preferredUsername?.toString() ||
994
1012
  actorUrl,
995
1013
  handle: actorObj.preferredUsername?.toString() || "",
996
- avatar: actorObj.icon
997
- ? (await actorObj.icon)?.url?.href || ""
998
- : "",
1014
+ avatar: updatedAvatar,
1015
+ banner: updatedBanner,
999
1016
  updatedAt: new Date().toISOString(),
1000
1017
  },
1001
1018
  },
@@ -66,13 +66,30 @@ export function registerInboxListeners(inboxChain, options) {
66
66
  followerActor.preferredUsername?.toString() ||
67
67
  followerUrl;
68
68
 
69
+ let followerAvatar = "";
70
+ try {
71
+ followerAvatar = followerActor.icon
72
+ ? (await followerActor.icon)?.url?.href || ""
73
+ : "";
74
+ } catch {
75
+ // Icon fetch failed
76
+ }
77
+
78
+ let followerBanner = "";
79
+ try {
80
+ followerBanner = followerActor.image
81
+ ? (await followerActor.image)?.url?.href || ""
82
+ : "";
83
+ } catch {
84
+ // Banner fetch failed
85
+ }
86
+
69
87
  const followerData = {
70
88
  actorUrl: followerUrl,
71
89
  handle: followerActor.preferredUsername?.toString() || "",
72
90
  name: followerName,
73
- avatar: followerActor.icon
74
- ? (await followerActor.icon)?.url?.href || ""
75
- : "",
91
+ avatar: followerAvatar,
92
+ banner: followerBanner,
76
93
  inbox: followerActor.inbox?.id?.href || "",
77
94
  sharedInbox: followerActor.endpoints?.sharedInbox?.href || "",
78
95
  };
@@ -55,17 +55,26 @@ export function serializeNotification(notif, { baseUrl, statusMap, interactionSt
55
55
  );
56
56
 
57
57
  // Resolve the associated status (for favourite, reblog, mention types)
58
+ // For mention types, prefer the triggering post (notif.url) over the target post (notif.targetUrl)
59
+ // because targetUrl for replies points to the user's OWN post being replied to
58
60
  let status = null;
59
- if (notif.targetUrl && statusMap) {
60
- const timelineItem = statusMap.get(notif.targetUrl);
61
- if (timelineItem) {
62
- status = serializeStatus(timelineItem, {
63
- baseUrl,
64
- favouritedIds: interactionState?.favouritedIds || new Set(),
65
- rebloggedIds: interactionState?.rebloggedIds || new Set(),
66
- bookmarkedIds: interactionState?.bookmarkedIds || new Set(),
67
- pinnedIds: new Set(),
68
- });
61
+ if (statusMap) {
62
+ const isMentionType = mastodonType === "mention";
63
+ const lookupUrl = isMentionType
64
+ ? (notif.url || notif.targetUrl)
65
+ : (notif.targetUrl || notif.url);
66
+
67
+ if (lookupUrl) {
68
+ const timelineItem = statusMap.get(lookupUrl);
69
+ if (timelineItem) {
70
+ status = serializeStatus(timelineItem, {
71
+ baseUrl,
72
+ favouritedIds: interactionState?.favouritedIds || new Set(),
73
+ rebloggedIds: interactionState?.rebloggedIds || new Set(),
74
+ bookmarkedIds: interactionState?.bookmarkedIds || new Set(),
75
+ pinnedIds: new Set(),
76
+ });
77
+ }
69
78
  }
70
79
  }
71
80
 
@@ -82,7 +91,7 @@ export function serializeNotification(notif, { baseUrl, statusMap, interactionSt
82
91
  visibility: notif.type === "dm" ? "direct" : "public",
83
92
  language: null,
84
93
  uri: notif.uid || "",
85
- url: notif.targetUrl || notif.uid || "",
94
+ url: notif.url || notif.targetUrl || notif.uid || "",
86
95
  replies_count: 0,
87
96
  reblogs_count: 0,
88
97
  favourites_count: 0,
@@ -110,7 +110,7 @@ router.get("/api/v1/accounts/lookup", async (req, res, next) => {
110
110
  if (follower) {
111
111
  return res.json(
112
112
  serializeAccount(
113
- { name: follower.name, url: follower.actorUrl, photo: follower.avatar, handle: follower.handle },
113
+ { name: follower.name, url: follower.actorUrl, photo: follower.avatar, handle: follower.handle, bannerUrl: follower.banner || "" },
114
114
  { baseUrl },
115
115
  ),
116
116
  );
@@ -277,7 +277,7 @@ router.get("/api/v1/accounts/:id/followers", async (req, res, next) => {
277
277
 
278
278
  const accounts = followers.map((f) =>
279
279
  serializeAccount(
280
- { name: f.name, url: f.actorUrl, photo: f.avatar, handle: f.handle },
280
+ { name: f.name, url: f.actorUrl, photo: f.avatar, handle: f.handle, bannerUrl: f.banner || "" },
281
281
  { baseUrl },
282
282
  ),
283
283
  );
@@ -310,7 +310,7 @@ router.get("/api/v1/accounts/:id/following", async (req, res, next) => {
310
310
 
311
311
  const accounts = following.map((f) =>
312
312
  serializeAccount(
313
- { name: f.name, url: f.actorUrl, photo: f.avatar, handle: f.handle },
313
+ { name: f.name, url: f.actorUrl, photo: f.avatar, handle: f.handle, bannerUrl: f.banner || "" },
314
314
  { baseUrl },
315
315
  ),
316
316
  );
@@ -712,23 +712,35 @@ async function resolveActorUrl(id, collections) {
712
712
  * Returns { actor, actorUrl } or { actor: null, actorUrl: null }.
713
713
  */
714
714
  async function resolveActorData(id, collections) {
715
- // Check followers
715
+ // Check followers — pass through all stored fields for richer serialization
716
716
  const followers = await collections.ap_followers.find({}).toArray();
717
717
  for (const f of followers) {
718
718
  if (remoteActorId(f.actorUrl) === id) {
719
719
  return {
720
- actor: { name: f.name, url: f.actorUrl, photo: f.avatar, handle: f.handle },
720
+ actor: {
721
+ name: f.name,
722
+ url: f.actorUrl,
723
+ photo: f.avatar,
724
+ handle: f.handle,
725
+ bannerUrl: f.banner || "",
726
+ },
721
727
  actorUrl: f.actorUrl,
722
728
  };
723
729
  }
724
730
  }
725
731
 
726
- // Check following
732
+ // Check following — pass through all stored fields
727
733
  const following = await collections.ap_following.find({}).toArray();
728
734
  for (const f of following) {
729
735
  if (remoteActorId(f.actorUrl) === id) {
730
736
  return {
731
- actor: { name: f.name, url: f.actorUrl, photo: f.avatar, handle: f.handle },
737
+ actor: {
738
+ name: f.name,
739
+ url: f.actorUrl,
740
+ photo: f.avatar,
741
+ handle: f.handle,
742
+ bannerUrl: f.banner || "",
743
+ },
732
744
  actorUrl: f.actorUrl,
733
745
  };
734
746
  }
@@ -225,23 +225,25 @@ function resolveInternalTypes(mastodonTypes) {
225
225
  async function batchFetchStatuses(collections, notifications) {
226
226
  const statusMap = new Map();
227
227
 
228
- const targetUrls = [
228
+ // Collect both targetUrl (the post being acted on) and url (the triggering post)
229
+ // For mention/reply notifications, the url is the actual mention/reply post
230
+ const allUrls = [
229
231
  ...new Set(
230
232
  notifications
231
- .map((n) => n.targetUrl)
233
+ .flatMap((n) => [n.targetUrl, n.url])
232
234
  .filter(Boolean),
233
235
  ),
234
236
  ];
235
237
 
236
- if (targetUrls.length === 0 || !collections.ap_timeline) {
238
+ if (allUrls.length === 0 || !collections.ap_timeline) {
237
239
  return statusMap;
238
240
  }
239
241
 
240
242
  const items = await collections.ap_timeline
241
243
  .find({
242
244
  $or: [
243
- { uid: { $in: targetUrls } },
244
- { url: { $in: targetUrls } },
245
+ { uid: { $in: allUrls } },
246
+ { url: { $in: allUrls } },
245
247
  ],
246
248
  })
247
249
  .toArray();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
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",