@rmdes/indiekit-endpoint-activitypub 1.1.8 → 1.1.9
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 +19 -5
- package/index.js +14 -2
- package/lib/batch-refollow.js +7 -2
- package/lib/controllers/compose.js +30 -6
- package/lib/controllers/interactions-boost.js +8 -3
- package/lib/controllers/interactions-like.js +75 -22
- package/lib/controllers/moderation.js +12 -2
- package/lib/controllers/profile.remote.js +5 -2
- package/package.json +1 -1
- package/views/activitypub-compose.njk +3 -3
package/assets/reader.css
CHANGED
|
@@ -296,24 +296,38 @@
|
|
|
296
296
|
max-width: 100%;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
/* @mentions —
|
|
300
|
-
.ap-card__content .h-card
|
|
299
|
+
/* @mentions — keep inline, style as subtle links */
|
|
300
|
+
.ap-card__content .h-card {
|
|
301
|
+
display: inline;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.ap-card__content .h-card a,
|
|
301
305
|
.ap-card__content a.u-url.mention {
|
|
302
306
|
color: var(--color-on-offset);
|
|
303
|
-
font-size: var(--font-size-s);
|
|
304
307
|
text-decoration: none;
|
|
308
|
+
white-space: nowrap;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.ap-card__content .h-card a span,
|
|
312
|
+
.ap-card__content a.u-url.mention span {
|
|
313
|
+
display: inline;
|
|
305
314
|
}
|
|
306
315
|
|
|
316
|
+
.ap-card__content .h-card a:hover,
|
|
307
317
|
.ap-card__content a.u-url.mention:hover {
|
|
308
318
|
color: var(--color-primary);
|
|
309
319
|
text-decoration: underline;
|
|
310
320
|
}
|
|
311
321
|
|
|
312
|
-
/* Hashtag mentions — subtle
|
|
322
|
+
/* Hashtag mentions — keep inline, subtle styling */
|
|
313
323
|
.ap-card__content a.mention.hashtag {
|
|
314
324
|
color: var(--color-on-offset);
|
|
315
|
-
font-size: var(--font-size-s);
|
|
316
325
|
text-decoration: none;
|
|
326
|
+
white-space: nowrap;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.ap-card__content a.mention.hashtag span {
|
|
330
|
+
display: inline;
|
|
317
331
|
}
|
|
318
332
|
|
|
319
333
|
.ap-card__content a.mention.hashtag:hover {
|
package/index.js
CHANGED
|
@@ -496,7 +496,13 @@ export default class ActivityPubEndpoint {
|
|
|
496
496
|
);
|
|
497
497
|
|
|
498
498
|
// Resolve the remote actor to get their inbox
|
|
499
|
-
|
|
499
|
+
// Use authenticated document loader for servers requiring Authorized Fetch
|
|
500
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
501
|
+
identifier: handle,
|
|
502
|
+
});
|
|
503
|
+
const remoteActor = await ctx.lookupObject(actorUrl, {
|
|
504
|
+
documentLoader,
|
|
505
|
+
});
|
|
500
506
|
if (!remoteActor) {
|
|
501
507
|
return { ok: false, error: "Could not resolve remote actor" };
|
|
502
508
|
}
|
|
@@ -591,7 +597,13 @@ export default class ActivityPubEndpoint {
|
|
|
591
597
|
{ handle, publicationUrl: this._publicationUrl },
|
|
592
598
|
);
|
|
593
599
|
|
|
594
|
-
|
|
600
|
+
// Use authenticated document loader for servers requiring Authorized Fetch
|
|
601
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
602
|
+
identifier: handle,
|
|
603
|
+
});
|
|
604
|
+
const remoteActor = await ctx.lookupObject(actorUrl, {
|
|
605
|
+
documentLoader,
|
|
606
|
+
});
|
|
595
607
|
if (!remoteActor) {
|
|
596
608
|
// Even if we can't resolve, remove locally
|
|
597
609
|
await this._collections.ap_following.deleteOne({ actorUrl });
|
package/lib/batch-refollow.js
CHANGED
|
@@ -227,8 +227,13 @@ async function processOneFollow(options, entry) {
|
|
|
227
227
|
try {
|
|
228
228
|
const ctx = federation.createContext(new URL(publicationUrl), { handle, publicationUrl });
|
|
229
229
|
|
|
230
|
-
// Resolve the remote actor
|
|
231
|
-
const
|
|
230
|
+
// Resolve the remote actor (signed request for Authorized Fetch)
|
|
231
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
232
|
+
identifier: handle,
|
|
233
|
+
});
|
|
234
|
+
const remoteActor = await ctx.lookupObject(entry.actorUrl, {
|
|
235
|
+
documentLoader,
|
|
236
|
+
});
|
|
232
237
|
if (!remoteActor) {
|
|
233
238
|
throw new Error("Could not resolve remote actor");
|
|
234
239
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Temporal } from "@js-temporal/polyfill";
|
|
6
6
|
import { getTimelineItem } from "../storage/timeline.js";
|
|
7
7
|
import { getToken, validateToken } from "../csrf.js";
|
|
8
|
+
import { sanitizeContent } from "../timeline-store.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Fetch syndication targets from the Micropub config endpoint.
|
|
@@ -71,14 +72,22 @@ export function composeController(mountPath, plugin) {
|
|
|
71
72
|
new URL(plugin._publicationUrl),
|
|
72
73
|
{ handle, publicationUrl: plugin._publicationUrl },
|
|
73
74
|
);
|
|
74
|
-
|
|
75
|
+
// Use authenticated document loader for Authorized Fetch
|
|
76
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
77
|
+
identifier: handle,
|
|
78
|
+
});
|
|
79
|
+
const remoteObject = await ctx.lookupObject(new URL(replyTo), {
|
|
80
|
+
documentLoader,
|
|
81
|
+
});
|
|
75
82
|
|
|
76
83
|
if (remoteObject) {
|
|
77
84
|
let authorName = "";
|
|
78
85
|
let authorUrl = "";
|
|
79
86
|
|
|
80
87
|
if (typeof remoteObject.getAttributedTo === "function") {
|
|
81
|
-
const author = await remoteObject.getAttributedTo(
|
|
88
|
+
const author = await remoteObject.getAttributedTo({
|
|
89
|
+
documentLoader,
|
|
90
|
+
});
|
|
82
91
|
const actor = Array.isArray(author) ? author[0] : author;
|
|
83
92
|
|
|
84
93
|
if (actor) {
|
|
@@ -90,12 +99,13 @@ export function composeController(mountPath, plugin) {
|
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
const rawHtml = remoteObject.content?.toString() || "";
|
|
93
103
|
replyContext = {
|
|
94
104
|
url: replyTo,
|
|
95
105
|
name: remoteObject.name?.toString() || "",
|
|
96
106
|
content: {
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
html: sanitizeContent(rawHtml),
|
|
108
|
+
text: rawHtml.replace(/<[^>]*>/g, "").slice(0, 300),
|
|
99
109
|
},
|
|
100
110
|
author: { name: authorName, url: authorUrl },
|
|
101
111
|
};
|
|
@@ -112,6 +122,13 @@ export function composeController(mountPath, plugin) {
|
|
|
112
122
|
? await getSyndicationTargets(application, token)
|
|
113
123
|
: [];
|
|
114
124
|
|
|
125
|
+
// Default-check only AP (Fedify) and Bluesky targets
|
|
126
|
+
// "@rick@rmendes.net" = AP Fedify, "@rmendes.net" = Bluesky
|
|
127
|
+
for (const target of syndicationTargets) {
|
|
128
|
+
const name = target.name || "";
|
|
129
|
+
target.defaultChecked = name === "@rick@rmendes.net" || name === "@rmendes.net";
|
|
130
|
+
}
|
|
131
|
+
|
|
115
132
|
const csrfToken = getToken(request.session);
|
|
116
133
|
|
|
117
134
|
response.render("activitypub-compose", {
|
|
@@ -198,13 +215,20 @@ export function submitComposeController(mountPath, plugin) {
|
|
|
198
215
|
// If replying, also send to the original author
|
|
199
216
|
if (inReplyTo) {
|
|
200
217
|
try {
|
|
201
|
-
const
|
|
218
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
219
|
+
identifier: handle,
|
|
220
|
+
});
|
|
221
|
+
const remoteObject = await ctx.lookupObject(new URL(inReplyTo), {
|
|
222
|
+
documentLoader,
|
|
223
|
+
});
|
|
202
224
|
|
|
203
225
|
if (
|
|
204
226
|
remoteObject &&
|
|
205
227
|
typeof remoteObject.getAttributedTo === "function"
|
|
206
228
|
) {
|
|
207
|
-
const author = await remoteObject.getAttributedTo(
|
|
229
|
+
const author = await remoteObject.getAttributedTo({
|
|
230
|
+
documentLoader,
|
|
231
|
+
});
|
|
208
232
|
const recipient = Array.isArray(author)
|
|
209
233
|
? author[0]
|
|
210
234
|
: author;
|
|
@@ -57,15 +57,20 @@ export function boostController(mountPath, plugin) {
|
|
|
57
57
|
orderingKey: url,
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
// Also send to the original post author
|
|
60
|
+
// Also send to the original post author (signed request for Authorized Fetch)
|
|
61
61
|
try {
|
|
62
|
-
const
|
|
62
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
63
|
+
identifier: handle,
|
|
64
|
+
});
|
|
65
|
+
const remoteObject = await ctx.lookupObject(new URL(url), {
|
|
66
|
+
documentLoader,
|
|
67
|
+
});
|
|
63
68
|
|
|
64
69
|
if (
|
|
65
70
|
remoteObject &&
|
|
66
71
|
typeof remoteObject.getAttributedTo === "function"
|
|
67
72
|
) {
|
|
68
|
-
const author = await remoteObject.getAttributedTo();
|
|
73
|
+
const author = await remoteObject.getAttributedTo({ documentLoader });
|
|
69
74
|
const recipient = Array.isArray(author) ? author[0] : author;
|
|
70
75
|
|
|
71
76
|
if (recipient) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { validateToken } from "../csrf.js";
|
|
7
|
+
import { getTimelineItem } from "../storage/timeline.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* POST /admin/reader/like — send a Like activity to the post author.
|
|
@@ -43,29 +44,52 @@ export function likeController(mountPath, plugin) {
|
|
|
43
44
|
{ handle, publicationUrl: plugin._publicationUrl },
|
|
44
45
|
);
|
|
45
46
|
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return response.status(404).json({
|
|
51
|
-
success: false,
|
|
52
|
-
error: "Could not resolve remote post",
|
|
53
|
-
});
|
|
54
|
-
}
|
|
47
|
+
// Use authenticated document loader for servers requiring Authorized Fetch
|
|
48
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
49
|
+
identifier: handle,
|
|
50
|
+
});
|
|
55
51
|
|
|
56
|
-
//
|
|
52
|
+
// Resolve author for delivery — try multiple strategies
|
|
57
53
|
let recipient = null;
|
|
58
54
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
// Strategy 1: Look up remote post via Fedify (signed request)
|
|
56
|
+
try {
|
|
57
|
+
const remoteObject = await ctx.lookupObject(new URL(url), {
|
|
58
|
+
documentLoader,
|
|
59
|
+
});
|
|
60
|
+
if (remoteObject && typeof remoteObject.getAttributedTo === "function") {
|
|
61
|
+
const author = await remoteObject.getAttributedTo({ documentLoader });
|
|
62
|
+
recipient = Array.isArray(author) ? author[0] : author;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Network failure — fall through to timeline
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
// Strategy 2: Use author URL from our timeline (already stored)
|
|
64
69
|
if (!recipient) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
70
|
+
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 authorUrl = timelineItem?.author?.url;
|
|
76
|
+
|
|
77
|
+
if (authorUrl) {
|
|
78
|
+
try {
|
|
79
|
+
recipient = await ctx.lookupObject(new URL(authorUrl), {
|
|
80
|
+
documentLoader,
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
// Could not resolve author actor either
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!recipient) {
|
|
88
|
+
return response.status(404).json({
|
|
89
|
+
success: false,
|
|
90
|
+
error: "Could not resolve post author",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
69
93
|
}
|
|
70
94
|
|
|
71
95
|
// Generate a unique activity ID
|
|
@@ -170,13 +194,42 @@ export function unlikeController(mountPath, plugin) {
|
|
|
170
194
|
{ handle, publicationUrl: plugin._publicationUrl },
|
|
171
195
|
);
|
|
172
196
|
|
|
173
|
-
//
|
|
174
|
-
const
|
|
197
|
+
// Use authenticated document loader for servers requiring Authorized Fetch
|
|
198
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
199
|
+
identifier: handle,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Resolve the recipient — try remote first, then timeline fallback
|
|
175
203
|
let recipient = null;
|
|
176
204
|
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
205
|
+
try {
|
|
206
|
+
const remoteObject = await ctx.lookupObject(new URL(url), {
|
|
207
|
+
documentLoader,
|
|
208
|
+
});
|
|
209
|
+
if (remoteObject && typeof remoteObject.getAttributedTo === "function") {
|
|
210
|
+
const author = await remoteObject.getAttributedTo({ documentLoader });
|
|
211
|
+
recipient = Array.isArray(author) ? author[0] : author;
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// Network failure
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!recipient) {
|
|
218
|
+
const collections = {
|
|
219
|
+
ap_timeline: application?.collections?.get("ap_timeline"),
|
|
220
|
+
};
|
|
221
|
+
const timelineItem = await getTimelineItem(collections, url);
|
|
222
|
+
const authorUrl = timelineItem?.author?.url;
|
|
223
|
+
|
|
224
|
+
if (authorUrl) {
|
|
225
|
+
try {
|
|
226
|
+
recipient = await ctx.lookupObject(new URL(authorUrl), {
|
|
227
|
+
documentLoader,
|
|
228
|
+
});
|
|
229
|
+
} catch {
|
|
230
|
+
// Could not resolve — will proceed to cleanup
|
|
231
|
+
}
|
|
232
|
+
}
|
|
180
233
|
}
|
|
181
234
|
|
|
182
235
|
if (!recipient) {
|
|
@@ -151,7 +151,12 @@ export function blockController(mountPath, plugin) {
|
|
|
151
151
|
{ handle, publicationUrl: plugin._publicationUrl },
|
|
152
152
|
);
|
|
153
153
|
|
|
154
|
-
const
|
|
154
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
155
|
+
identifier: handle,
|
|
156
|
+
});
|
|
157
|
+
const remoteActor = await ctx.lookupObject(new URL(url), {
|
|
158
|
+
documentLoader,
|
|
159
|
+
});
|
|
155
160
|
|
|
156
161
|
if (remoteActor) {
|
|
157
162
|
const block = new Block({
|
|
@@ -225,7 +230,12 @@ export function unblockController(mountPath, plugin) {
|
|
|
225
230
|
{ handle, publicationUrl: plugin._publicationUrl },
|
|
226
231
|
);
|
|
227
232
|
|
|
228
|
-
const
|
|
233
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
234
|
+
identifier: handle,
|
|
235
|
+
});
|
|
236
|
+
const remoteActor = await ctx.lookupObject(new URL(url), {
|
|
237
|
+
documentLoader,
|
|
238
|
+
});
|
|
229
239
|
|
|
230
240
|
if (remoteActor) {
|
|
231
241
|
const block = new Block({
|
|
@@ -36,11 +36,14 @@ export function remoteProfileController(mountPath, plugin) {
|
|
|
36
36
|
{ handle, publicationUrl: plugin._publicationUrl },
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
-
// Look up the remote actor
|
|
39
|
+
// Look up the remote actor (signed request for Authorized Fetch)
|
|
40
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
41
|
+
identifier: handle,
|
|
42
|
+
});
|
|
40
43
|
let actor;
|
|
41
44
|
|
|
42
45
|
try {
|
|
43
|
-
actor = await ctx.lookupObject(new URL(actorUrl));
|
|
46
|
+
actor = await ctx.lookupObject(new URL(actorUrl), { documentLoader });
|
|
44
47
|
} catch {
|
|
45
48
|
return response.status(404).render("error", {
|
|
46
49
|
title: "Error",
|
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.9",
|
|
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",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
<a href="{{ replyContext.author.url }}">{{ replyContext.author.name }}</a>
|
|
19
19
|
</div>
|
|
20
20
|
{% endif %}
|
|
21
|
-
{% if replyContext.content and replyContext.content.text %}
|
|
21
|
+
{% if replyContext.content and (replyContext.content.html or replyContext.content.text) %}
|
|
22
22
|
<blockquote class="ap-compose__context-text">
|
|
23
|
-
{{ replyContext.content.text | truncate(300) }}
|
|
23
|
+
{{ replyContext.content.html | safe if replyContext.content.html else replyContext.content.text | truncate(300) }}
|
|
24
24
|
</blockquote>
|
|
25
25
|
{% endif %}
|
|
26
26
|
<a href="{{ replyTo }}" class="ap-compose__context-link" target="_blank" rel="noopener">{{ replyTo }}</a>
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
<legend>{{ __("activitypub.compose.syndicateLabel") }}</legend>
|
|
75
75
|
{% for target in syndicationTargets %}
|
|
76
76
|
<label class="ap-compose__syndication-target">
|
|
77
|
-
<input type="checkbox" name="mp-syndicate-to" value="{{ target.uid }}" checked>
|
|
77
|
+
<input type="checkbox" name="mp-syndicate-to" value="{{ target.uid }}" {{ "checked" if target.defaultChecked }}>
|
|
78
78
|
{{ target.name }}
|
|
79
79
|
</label>
|
|
80
80
|
{% endfor %}
|