@rmdes/indiekit-endpoint-activitypub 1.1.9 → 1.1.11

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.
package/assets/reader.css CHANGED
@@ -303,6 +303,7 @@
303
303
 
304
304
  .ap-card__content .h-card a,
305
305
  .ap-card__content a.u-url.mention {
306
+ display: inline;
306
307
  color: var(--color-on-offset);
307
308
  text-decoration: none;
308
309
  white-space: nowrap;
@@ -321,6 +322,7 @@
321
322
 
322
323
  /* Hashtag mentions — keep inline, subtle styling */
323
324
  .ap-card__content a.mention.hashtag {
325
+ display: inline;
324
326
  color: var(--color-on-offset);
325
327
  text-decoration: none;
326
328
  white-space: nowrap;
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  import { Temporal } from "@js-temporal/polyfill";
6
- import { getTimelineItem } from "../storage/timeline.js";
7
6
  import { getToken, validateToken } from "../csrf.js";
8
7
  import { sanitizeContent } from "../timeline-store.js";
9
8
 
@@ -62,7 +61,12 @@ export function composeController(mountPath, plugin) {
62
61
  };
63
62
 
64
63
  // Try to find the post in our timeline first
65
- replyContext = await getTimelineItem(collections, replyTo);
64
+ // Note: Timeline stores uid (canonical AP URL) and url (display URL).
65
+ // The card link passes the display URL, so search both fields.
66
+ const ap_timeline = collections.ap_timeline;
67
+ replyContext = ap_timeline
68
+ ? await ap_timeline.findOne({ $or: [{ uid: replyTo }, { url: replyTo }] })
69
+ : null;
66
70
 
67
71
  // If not in timeline, try to look up remotely
68
72
  if (!replyContext && plugin._federation) {
@@ -110,8 +114,11 @@ export function composeController(mountPath, plugin) {
110
114
  author: { name: authorName, url: authorUrl },
111
115
  };
112
116
  }
113
- } catch {
114
- // Could not resolve — form still works without context
117
+ } catch (error) {
118
+ console.warn(
119
+ `[ActivityPub] lookupObject failed for ${replyTo} (compose):`,
120
+ error.message,
121
+ );
115
122
  }
116
123
  }
117
124
  }
@@ -82,8 +82,11 @@ export function boostController(mountPath, plugin) {
82
82
  );
83
83
  }
84
84
  }
85
- } catch {
86
- // Non-critical — followers still received the boost
85
+ } catch (error) {
86
+ console.warn(
87
+ `[ActivityPub] lookupObject failed for ${url} (boost):`,
88
+ error.message,
89
+ );
87
90
  }
88
91
 
89
92
  // Track the interaction
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import { validateToken } from "../csrf.js";
7
- import { getTimelineItem } from "../storage/timeline.js";
8
7
 
9
8
  /**
10
9
  * POST /admin/reader/like — send a Like activity to the post author.
@@ -61,17 +60,22 @@ export function likeController(mountPath, plugin) {
61
60
  const author = await remoteObject.getAttributedTo({ documentLoader });
62
61
  recipient = Array.isArray(author) ? author[0] : author;
63
62
  }
64
- } catch {
65
- // Network failure — fall through to timeline
63
+ } catch (error) {
64
+ console.warn(
65
+ `[ActivityPub] lookupObject failed for ${url}:`,
66
+ error.message,
67
+ );
66
68
  }
67
69
 
68
70
  // Strategy 2: Use author URL from our timeline (already stored)
71
+ // Note: Timeline items store both uid (canonical AP URL) and url (display URL).
72
+ // The card passes the display URL, so we search by both fields.
69
73
  if (!recipient) {
70
74
  const { application } = request.app.locals;
71
- const collections = {
72
- ap_timeline: application?.collections?.get("ap_timeline"),
73
- };
74
- const timelineItem = await getTimelineItem(collections, url);
75
+ const ap_timeline = application?.collections?.get("ap_timeline");
76
+ const timelineItem = ap_timeline
77
+ ? await ap_timeline.findOne({ $or: [{ uid: url }, { url }] })
78
+ : null;
75
79
  const authorUrl = timelineItem?.author?.url;
76
80
 
77
81
  if (authorUrl) {
@@ -210,15 +214,18 @@ export function unlikeController(mountPath, plugin) {
210
214
  const author = await remoteObject.getAttributedTo({ documentLoader });
211
215
  recipient = Array.isArray(author) ? author[0] : author;
212
216
  }
213
- } catch {
214
- // Network failure
217
+ } catch (error) {
218
+ console.warn(
219
+ `[ActivityPub] lookupObject failed for ${url} (unlike):`,
220
+ error.message,
221
+ );
215
222
  }
216
223
 
217
224
  if (!recipient) {
218
- const collections = {
219
- ap_timeline: application?.collections?.get("ap_timeline"),
220
- };
221
- const timelineItem = await getTimelineItem(collections, url);
225
+ const ap_timeline = application?.collections?.get("ap_timeline");
226
+ const timelineItem = ap_timeline
227
+ ? await ap_timeline.findOne({ $or: [{ uid: url }, { url }] })
228
+ : null;
222
229
  const authorUrl = timelineItem?.author?.url;
223
230
 
224
231
  if (authorUrl) {
@@ -107,26 +107,47 @@ export function readerController(mountPath) {
107
107
  const unreadCount = await getUnreadNotificationCount(collections);
108
108
 
109
109
  // Get interaction state for liked/boosted indicators
110
+ // Interactions are keyed by canonical AP uid (new) or display url (legacy).
111
+ // Query by both, normalize map keys to uid for template lookup.
110
112
  const interactionsCol =
111
113
  application?.collections?.get("ap_interactions");
112
114
  const interactionMap = {};
113
115
 
114
116
  if (interactionsCol) {
115
- const itemUrls = items
116
- .map((item) => item.url || item.originalUrl)
117
- .filter(Boolean);
117
+ const lookupUrls = new Set();
118
+ const objectUrlToUid = new Map();
118
119
 
119
- if (itemUrls.length > 0) {
120
+ for (const item of items) {
121
+ const uid = item.uid;
122
+ const displayUrl = item.url || item.originalUrl;
123
+
124
+ if (uid) {
125
+ lookupUrls.add(uid);
126
+ objectUrlToUid.set(uid, uid);
127
+ }
128
+
129
+ if (displayUrl) {
130
+ lookupUrls.add(displayUrl);
131
+ objectUrlToUid.set(displayUrl, uid || displayUrl);
132
+ }
133
+ }
134
+
135
+ if (lookupUrls.size > 0) {
120
136
  const interactions = await interactionsCol
121
- .find({ objectUrl: { $in: itemUrls } })
137
+ .find({ objectUrl: { $in: [...lookupUrls] } })
122
138
  .toArray();
123
139
 
124
140
  for (const interaction of interactions) {
125
- if (!interactionMap[interaction.objectUrl]) {
126
- interactionMap[interaction.objectUrl] = {};
141
+ // Normalize to uid so template can look up by itemUid
142
+ const key =
143
+ objectUrlToUid.get(interaction.objectUrl) ||
144
+ interaction.objectUrl;
145
+
146
+ if (!interactionMap[key]) {
147
+ interactionMap[key] = {};
127
148
  }
128
149
 
129
- interactionMap[interaction.objectUrl][interaction.type] = true;
150
+ interactionMap[key][interaction.type] = true;
130
151
  }
131
152
  }
132
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
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",
@@ -97,11 +97,13 @@
97
97
  {% endif %}
98
98
 
99
99
  {# Interaction buttons — Alpine.js for optimistic updates #}
100
- {# Dynamic data moved to data-* attributes to prevent XSS from inline interpolation #}
100
+ {# Use canonical AP uid for interactions (Fedify lookupObject), display url for links #}
101
101
  {% set itemUrl = item.url or item.originalUrl %}
102
- {% set isLiked = interactionMap[itemUrl].like if interactionMap[itemUrl] else false %}
103
- {% set isBoosted = interactionMap[itemUrl].boost if interactionMap[itemUrl] else false %}
102
+ {% set itemUid = item.uid or item.url or item.originalUrl %}
103
+ {% set isLiked = interactionMap[itemUid].like if interactionMap[itemUid] else false %}
104
+ {% set isBoosted = interactionMap[itemUid].boost if interactionMap[itemUid] else false %}
104
105
  <footer class="ap-card__actions"
106
+ data-item-uid="{{ itemUid }}"
105
107
  data-item-url="{{ itemUrl }}"
106
108
  data-csrf-token="{{ csrfToken }}"
107
109
  data-mount-path="{{ mountPath }}"
@@ -115,7 +117,7 @@
115
117
  this.loading = true;
116
118
  this.error = '';
117
119
  const el = this.$root;
118
- const itemUrl = el.dataset.itemUrl;
120
+ const itemUid = el.dataset.itemUid;
119
121
  const csrfToken = el.dataset.csrfToken;
120
122
  const basePath = el.dataset.mountPath;
121
123
  const prev = { liked: this.liked, boosted: this.boosted };
@@ -130,7 +132,7 @@
130
132
  'Content-Type': 'application/json',
131
133
  'X-CSRF-Token': csrfToken
132
134
  },
133
- body: JSON.stringify({ url: itemUrl })
135
+ body: JSON.stringify({ url: itemUid })
134
136
  });
135
137
  const data = await res.json();
136
138
  if (!data.success) {
@@ -147,7 +149,7 @@
147
149
  if (this.error) setTimeout(() => this.error = '', 3000);
148
150
  }
149
151
  }">
150
- <a href="{{ mountPath }}/admin/reader/compose?replyTo={{ itemUrl | urlencode }}"
152
+ <a href="{{ mountPath }}/admin/reader/compose?replyTo={{ itemUid | urlencode }}"
151
153
  class="ap-card__action ap-card__action--reply"
152
154
  title="{{ __('activitypub.reader.actions.reply') }}">
153
155
  ↩ {{ __("activitypub.reader.actions.reply") }}