@rmdes/indiekit-endpoint-activitypub 1.1.10 → 1.1.12
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/lib/controllers/compose.js +11 -4
- package/lib/controllers/interactions-boost.js +5 -2
- package/lib/controllers/interactions-like.js +20 -13
- package/lib/controllers/profile.remote.js +2 -2
- package/lib/controllers/reader.js +29 -8
- package/lib/inbox-listeners.js +4 -9
- package/lib/timeline-store.js +33 -23
- package/package.json +1 -1
- package/views/activitypub-remote-profile.njk +2 -2
- package/views/partials/ap-item-card.njk +8 -6
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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) {
|
|
@@ -64,7 +64,7 @@ export function remoteProfileController(mountPath, plugin) {
|
|
|
64
64
|
actor.preferredUsername?.toString() ||
|
|
65
65
|
actorUrl;
|
|
66
66
|
const actorHandle = actor.preferredUsername?.toString() || "";
|
|
67
|
-
const
|
|
67
|
+
const bio = sanitizeContent(actor.summary?.toString() || "");
|
|
68
68
|
let icon = "";
|
|
69
69
|
let image = "";
|
|
70
70
|
|
|
@@ -129,7 +129,7 @@ export function remoteProfileController(mountPath, plugin) {
|
|
|
129
129
|
actorUrl,
|
|
130
130
|
name,
|
|
131
131
|
actorHandle,
|
|
132
|
-
|
|
132
|
+
bio,
|
|
133
133
|
icon,
|
|
134
134
|
image,
|
|
135
135
|
instanceHost,
|
|
@@ -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
|
|
116
|
-
|
|
117
|
-
.filter(Boolean);
|
|
117
|
+
const lookupUrls = new Set();
|
|
118
|
+
const objectUrlToUid = new Map();
|
|
118
119
|
|
|
119
|
-
|
|
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:
|
|
137
|
+
.find({ objectUrl: { $in: [...lookupUrls] } })
|
|
122
138
|
.toArray();
|
|
123
139
|
|
|
124
140
|
for (const interaction of interactions) {
|
|
125
|
-
|
|
126
|
-
|
|
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[
|
|
150
|
+
interactionMap[key][interaction.type] = true;
|
|
130
151
|
}
|
|
131
152
|
}
|
|
132
153
|
}
|
package/lib/inbox-listeners.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Accept,
|
|
10
10
|
Add,
|
|
11
11
|
Announce,
|
|
12
|
+
Article,
|
|
12
13
|
Block,
|
|
13
14
|
Create,
|
|
14
15
|
Delete,
|
|
@@ -365,14 +366,8 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
365
366
|
actorObj?.preferredUsername?.toString() ||
|
|
366
367
|
actorUrl;
|
|
367
368
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
try {
|
|
371
|
-
inReplyTo = (await object.getInReplyTo())?.id?.href ?? null;
|
|
372
|
-
} catch {
|
|
373
|
-
/* remote fetch may fail */
|
|
374
|
-
}
|
|
375
|
-
}
|
|
369
|
+
// Use replyTargetId (non-fetching) for the inReplyTo URL
|
|
370
|
+
const inReplyTo = object.replyTargetId?.href || null;
|
|
376
371
|
|
|
377
372
|
// Log replies to our posts (existing behavior for conversations)
|
|
378
373
|
const pubUrl = collections._publicationUrl;
|
|
@@ -505,7 +500,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
505
500
|
}
|
|
506
501
|
|
|
507
502
|
// PATH 1: If object is a Note/Article → Update timeline item content
|
|
508
|
-
if (object && (object instanceof Note || object
|
|
503
|
+
if (object && (object instanceof Note || object instanceof Article)) {
|
|
509
504
|
const objectUrl = object.id?.href || "";
|
|
510
505
|
if (objectUrl) {
|
|
511
506
|
try {
|
package/lib/timeline-store.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @module timeline-store
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { Article } from "@fedify/fedify";
|
|
6
7
|
import sanitizeHtml from "sanitize-html";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -98,9 +99,9 @@ export async function extractObjectData(object, options = {}) {
|
|
|
98
99
|
const uid = object.id?.href || "";
|
|
99
100
|
const url = object.url?.href || uid;
|
|
100
101
|
|
|
101
|
-
// Determine type
|
|
102
|
+
// Determine type — use instanceof for Fedify vocab objects
|
|
102
103
|
let type = "note";
|
|
103
|
-
if (object
|
|
104
|
+
if (object instanceof Article) {
|
|
104
105
|
type = "article";
|
|
105
106
|
}
|
|
106
107
|
if (options.boostedBy) {
|
|
@@ -179,42 +180,51 @@ export async function extractObjectData(object, options = {}) {
|
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
// Extract tags/categories
|
|
183
|
+
// Extract tags/categories — Fedify uses async getTags()
|
|
183
184
|
const category = [];
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
try {
|
|
186
|
+
if (typeof object.getTags === "function") {
|
|
187
|
+
const tags = await object.getTags();
|
|
188
|
+
for (const tag of tags) {
|
|
189
|
+
if (tag.name) {
|
|
190
|
+
const tagName = tag.name.toString().replace(/^#/, "");
|
|
191
|
+
if (tagName) category.push(tagName);
|
|
192
|
+
}
|
|
189
193
|
}
|
|
190
194
|
}
|
|
195
|
+
} catch {
|
|
196
|
+
// Tags extraction failed — non-critical
|
|
191
197
|
}
|
|
192
198
|
|
|
193
|
-
// Extract media attachments
|
|
199
|
+
// Extract media attachments — Fedify uses async getAttachments()
|
|
194
200
|
const photo = [];
|
|
195
201
|
const video = [];
|
|
196
202
|
const audio = [];
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
204
|
+
try {
|
|
205
|
+
if (typeof object.getAttachments === "function") {
|
|
206
|
+
const attachments = await object.getAttachments();
|
|
207
|
+
for (const att of attachments) {
|
|
208
|
+
const mediaUrl = att.url?.href || "";
|
|
209
|
+
if (!mediaUrl) continue;
|
|
203
210
|
|
|
204
|
-
|
|
211
|
+
const mediaType = att.mediaType?.toLowerCase() || "";
|
|
205
212
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
213
|
+
if (mediaType.startsWith("image/")) {
|
|
214
|
+
photo.push(mediaUrl);
|
|
215
|
+
} else if (mediaType.startsWith("video/")) {
|
|
216
|
+
video.push(mediaUrl);
|
|
217
|
+
} else if (mediaType.startsWith("audio/")) {
|
|
218
|
+
audio.push(mediaUrl);
|
|
219
|
+
}
|
|
212
220
|
}
|
|
213
221
|
}
|
|
222
|
+
} catch {
|
|
223
|
+
// Attachment extraction failed — non-critical
|
|
214
224
|
}
|
|
215
225
|
|
|
216
|
-
// In-reply-to
|
|
217
|
-
const inReplyTo = object.
|
|
226
|
+
// In-reply-to — Fedify uses replyTargetId (non-fetching)
|
|
227
|
+
const inReplyTo = object.replyTargetId?.href || "";
|
|
218
228
|
|
|
219
229
|
// Build base timeline item
|
|
220
230
|
const item = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.12",
|
|
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",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
{% if actorHandle %}
|
|
58
58
|
<div class="ap-profile__handle">@{{ actorHandle }}@{{ instanceHost }}</div>
|
|
59
59
|
{% endif %}
|
|
60
|
-
{% if
|
|
61
|
-
<div class="ap-profile__bio">{{
|
|
60
|
+
{% if bio %}
|
|
61
|
+
<div class="ap-profile__bio">{{ bio | safe }}</div>
|
|
62
62
|
{% endif %}
|
|
63
63
|
</div>
|
|
64
64
|
|
|
@@ -97,11 +97,13 @@
|
|
|
97
97
|
{% endif %}
|
|
98
98
|
|
|
99
99
|
{# Interaction buttons — Alpine.js for optimistic updates #}
|
|
100
|
-
{#
|
|
100
|
+
{# Use canonical AP uid for interactions (Fedify lookupObject), display url for links #}
|
|
101
101
|
{% set itemUrl = item.url or item.originalUrl %}
|
|
102
|
-
{% set
|
|
103
|
-
{% set
|
|
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
|
|
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:
|
|
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={{
|
|
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") }}
|