@rmdes/indiekit-endpoint-activitypub 2.0.7 → 2.0.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/index.js +7 -0
- package/lib/controllers/compose.js +26 -23
- package/lib/controllers/interactions-boost.js +28 -29
- package/lib/controllers/interactions-like.js +18 -82
- package/lib/controllers/note-object.js +51 -0
- package/lib/inbox-listeners.js +3 -1
- package/lib/resolve-author.js +156 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
} from "./lib/controllers/featured-tags.js";
|
|
60
60
|
import { resolveController } from "./lib/controllers/resolve.js";
|
|
61
61
|
import { publicProfileController } from "./lib/controllers/public-profile.js";
|
|
62
|
+
import { noteObjectController } from "./lib/controllers/note-object.js";
|
|
62
63
|
import {
|
|
63
64
|
refollowPauseController,
|
|
64
65
|
refollowResumeController,
|
|
@@ -161,6 +162,10 @@ export default class ActivityPubEndpoint {
|
|
|
161
162
|
return self._fedifyMiddleware(req, res, next);
|
|
162
163
|
});
|
|
163
164
|
|
|
165
|
+
// Serve stored quick reply Notes as JSON-LD so remote servers can
|
|
166
|
+
// dereference the Note ID during Create activity verification.
|
|
167
|
+
router.get("/quick-replies/:id", noteObjectController(self));
|
|
168
|
+
|
|
164
169
|
// HTML fallback for actor URL — serve a public profile page.
|
|
165
170
|
// Fedify only serves JSON-LD; browsers get 406 and fall through here.
|
|
166
171
|
router.get("/users/:identifier", publicProfileController(self));
|
|
@@ -835,6 +840,7 @@ export default class ActivityPubEndpoint {
|
|
|
835
840
|
Indiekit.addCollection("ap_muted");
|
|
836
841
|
Indiekit.addCollection("ap_blocked");
|
|
837
842
|
Indiekit.addCollection("ap_interactions");
|
|
843
|
+
Indiekit.addCollection("ap_notes");
|
|
838
844
|
|
|
839
845
|
// Store collection references (posts resolved lazily)
|
|
840
846
|
const indiekitCollections = Indiekit.collections;
|
|
@@ -853,6 +859,7 @@ export default class ActivityPubEndpoint {
|
|
|
853
859
|
ap_muted: indiekitCollections.get("ap_muted"),
|
|
854
860
|
ap_blocked: indiekitCollections.get("ap_blocked"),
|
|
855
861
|
ap_interactions: indiekitCollections.get("ap_interactions"),
|
|
862
|
+
ap_notes: indiekitCollections.get("ap_notes"),
|
|
856
863
|
get posts() {
|
|
857
864
|
return indiekitCollections.get("posts");
|
|
858
865
|
},
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Temporal } from "@js-temporal/polyfill";
|
|
6
6
|
import { getToken, validateToken } from "../csrf.js";
|
|
7
7
|
import { sanitizeContent } from "../timeline-store.js";
|
|
8
|
+
import { resolveAuthor } from "../resolve-author.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Fetch syndication targets from the Micropub config endpoint.
|
|
@@ -205,33 +206,20 @@ export function submitComposeController(mountPath, plugin) {
|
|
|
205
206
|
);
|
|
206
207
|
const followersUri = ctx.getFollowersUri(handle);
|
|
207
208
|
|
|
209
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
210
|
+
identifier: handle,
|
|
211
|
+
});
|
|
212
|
+
|
|
208
213
|
// Resolve the original author BEFORE constructing the Note,
|
|
209
214
|
// so we can include them in cc (required for threading/notification)
|
|
210
215
|
let recipient = null;
|
|
211
216
|
if (inReplyTo) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
remoteObject &&
|
|
222
|
-
typeof remoteObject.getAttributedTo === "function"
|
|
223
|
-
) {
|
|
224
|
-
const author = await remoteObject.getAttributedTo({
|
|
225
|
-
documentLoader,
|
|
226
|
-
});
|
|
227
|
-
recipient = Array.isArray(author) ? author[0] : author;
|
|
228
|
-
}
|
|
229
|
-
} catch (error) {
|
|
230
|
-
console.warn(
|
|
231
|
-
`[ActivityPub] lookupObject failed for ${inReplyTo} (quick reply):`,
|
|
232
|
-
error.message,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
217
|
+
recipient = await resolveAuthor(
|
|
218
|
+
inReplyTo,
|
|
219
|
+
ctx,
|
|
220
|
+
documentLoader,
|
|
221
|
+
application?.collections,
|
|
222
|
+
);
|
|
235
223
|
}
|
|
236
224
|
|
|
237
225
|
// Build cc list: always include followers, add original author for replies
|
|
@@ -258,6 +246,21 @@ export function submitComposeController(mountPath, plugin) {
|
|
|
258
246
|
ccs: ccList,
|
|
259
247
|
});
|
|
260
248
|
|
|
249
|
+
// Store the Note so remote servers can dereference its ID
|
|
250
|
+
const ap_notes = application?.collections?.get("ap_notes");
|
|
251
|
+
if (ap_notes) {
|
|
252
|
+
await ap_notes.insertOne({
|
|
253
|
+
_id: uuid,
|
|
254
|
+
noteId,
|
|
255
|
+
actorUrl: actorUri.href,
|
|
256
|
+
content: content.trim(),
|
|
257
|
+
inReplyTo: inReplyTo || null,
|
|
258
|
+
published: new Date().toISOString(),
|
|
259
|
+
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
|
260
|
+
cc: ccList.map((u) => (u instanceof URL ? u.href : u.href || u)),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
261
264
|
// Send to followers
|
|
262
265
|
await ctx.sendActivity({ identifier: handle }, "followers", create, {
|
|
263
266
|
preferSharedInbox: true,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { validateToken } from "../csrf.js";
|
|
7
|
+
import { resolveAuthor } from "../resolve-author.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* POST /admin/reader/boost — send an Announce activity to followers.
|
|
@@ -66,40 +67,38 @@ export function boostController(mountPath, plugin) {
|
|
|
66
67
|
orderingKey: url,
|
|
67
68
|
});
|
|
68
69
|
|
|
69
|
-
// Also send to the original post author
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
// Also send directly to the original post author
|
|
71
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
72
|
+
identifier: handle,
|
|
73
|
+
});
|
|
74
|
+
const { application } = request.app.locals;
|
|
75
|
+
const recipient = await resolveAuthor(
|
|
76
|
+
url,
|
|
77
|
+
ctx,
|
|
78
|
+
documentLoader,
|
|
79
|
+
application?.collections,
|
|
80
|
+
);
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
if (recipient) {
|
|
83
|
+
try {
|
|
84
|
+
await ctx.sendActivity(
|
|
85
|
+
{ identifier: handle },
|
|
86
|
+
recipient,
|
|
87
|
+
announce,
|
|
88
|
+
{ orderingKey: url },
|
|
89
|
+
);
|
|
90
|
+
console.info(
|
|
91
|
+
`[ActivityPub] Sent boost directly to ${recipient.id?.href || "author"}`,
|
|
92
|
+
);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.warn(
|
|
95
|
+
`[ActivityPub] Direct boost delivery to author failed:`,
|
|
96
|
+
error.message,
|
|
97
|
+
);
|
|
93
98
|
}
|
|
94
|
-
} catch (error) {
|
|
95
|
-
console.warn(
|
|
96
|
-
`[ActivityPub] lookupObject failed for ${url} (boost):`,
|
|
97
|
-
error.message,
|
|
98
|
-
);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// Track the interaction
|
|
102
|
-
const { application } = request.app.locals;
|
|
103
102
|
const interactions = application?.collections?.get("ap_interactions");
|
|
104
103
|
|
|
105
104
|
if (interactions) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { validateToken } from "../csrf.js";
|
|
7
|
+
import { resolveAuthor } from "../resolve-author.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* POST /admin/reader/like — send a Like activity to the post author.
|
|
@@ -43,57 +44,23 @@ export function likeController(mountPath, plugin) {
|
|
|
43
44
|
{ handle, publicationUrl: plugin._publicationUrl },
|
|
44
45
|
);
|
|
45
46
|
|
|
46
|
-
// Use authenticated document loader for servers requiring Authorized Fetch
|
|
47
47
|
const documentLoader = await ctx.getDocumentLoader({
|
|
48
48
|
identifier: handle,
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
59
|
-
if (remoteObject && typeof remoteObject.getAttributedTo === "function") {
|
|
60
|
-
const author = await remoteObject.getAttributedTo({ documentLoader });
|
|
61
|
-
recipient = Array.isArray(author) ? author[0] : author;
|
|
62
|
-
}
|
|
63
|
-
} catch (error) {
|
|
64
|
-
console.warn(
|
|
65
|
-
`[ActivityPub] lookupObject failed for ${url}:`,
|
|
66
|
-
error.message,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
51
|
+
const { application } = request.app.locals;
|
|
52
|
+
const recipient = await resolveAuthor(
|
|
53
|
+
url,
|
|
54
|
+
ctx,
|
|
55
|
+
documentLoader,
|
|
56
|
+
application?.collections,
|
|
57
|
+
);
|
|
69
58
|
|
|
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.
|
|
73
59
|
if (!recipient) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
: null;
|
|
79
|
-
const authorUrl = timelineItem?.author?.url;
|
|
80
|
-
|
|
81
|
-
if (authorUrl) {
|
|
82
|
-
try {
|
|
83
|
-
recipient = await ctx.lookupObject(new URL(authorUrl), {
|
|
84
|
-
documentLoader,
|
|
85
|
-
});
|
|
86
|
-
} catch {
|
|
87
|
-
// Could not resolve author actor either
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!recipient) {
|
|
92
|
-
return response.status(404).json({
|
|
93
|
-
success: false,
|
|
94
|
-
error: "Could not resolve post author",
|
|
95
|
-
});
|
|
96
|
-
}
|
|
60
|
+
return response.status(404).json({
|
|
61
|
+
success: false,
|
|
62
|
+
error: "Could not resolve post author",
|
|
63
|
+
});
|
|
97
64
|
}
|
|
98
65
|
|
|
99
66
|
// Generate a unique activity ID
|
|
@@ -113,7 +80,6 @@ export function likeController(mountPath, plugin) {
|
|
|
113
80
|
});
|
|
114
81
|
|
|
115
82
|
// Track the interaction for undo
|
|
116
|
-
const { application } = request.app.locals;
|
|
117
83
|
const interactions = application?.collections?.get("ap_interactions");
|
|
118
84
|
|
|
119
85
|
if (interactions) {
|
|
@@ -200,46 +166,16 @@ export function unlikeController(mountPath, plugin) {
|
|
|
200
166
|
{ handle, publicationUrl: plugin._publicationUrl },
|
|
201
167
|
);
|
|
202
168
|
|
|
203
|
-
// Use authenticated document loader for servers requiring Authorized Fetch
|
|
204
169
|
const documentLoader = await ctx.getDocumentLoader({
|
|
205
170
|
identifier: handle,
|
|
206
171
|
});
|
|
207
172
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
});
|
|
215
|
-
if (remoteObject && typeof remoteObject.getAttributedTo === "function") {
|
|
216
|
-
const author = await remoteObject.getAttributedTo({ documentLoader });
|
|
217
|
-
recipient = Array.isArray(author) ? author[0] : author;
|
|
218
|
-
}
|
|
219
|
-
} catch (error) {
|
|
220
|
-
console.warn(
|
|
221
|
-
`[ActivityPub] lookupObject failed for ${url} (unlike):`,
|
|
222
|
-
error.message,
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (!recipient) {
|
|
227
|
-
const ap_timeline = application?.collections?.get("ap_timeline");
|
|
228
|
-
const timelineItem = ap_timeline
|
|
229
|
-
? await ap_timeline.findOne({ $or: [{ uid: url }, { url }] })
|
|
230
|
-
: null;
|
|
231
|
-
const authorUrl = timelineItem?.author?.url;
|
|
232
|
-
|
|
233
|
-
if (authorUrl) {
|
|
234
|
-
try {
|
|
235
|
-
recipient = await ctx.lookupObject(new URL(authorUrl), {
|
|
236
|
-
documentLoader,
|
|
237
|
-
});
|
|
238
|
-
} catch {
|
|
239
|
-
// Could not resolve — will proceed to cleanup
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
173
|
+
const recipient = await resolveAuthor(
|
|
174
|
+
url,
|
|
175
|
+
ctx,
|
|
176
|
+
documentLoader,
|
|
177
|
+
application?.collections,
|
|
178
|
+
);
|
|
243
179
|
|
|
244
180
|
if (!recipient) {
|
|
245
181
|
// Clean up the local record even if we can't send Undo
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public route handler for serving quick reply Notes as ActivityPub JSON-LD.
|
|
3
|
+
*
|
|
4
|
+
* Remote servers dereference Note IDs to verify Create activities.
|
|
5
|
+
* Without this, quick replies are rejected by servers that validate
|
|
6
|
+
* the Note's ID URL (Mastodon with Authorized Fetch, Bonfire, etc.).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GET /quick-replies/:id — serve a stored Note as JSON-LD.
|
|
11
|
+
* @param {object} plugin - ActivityPub plugin instance
|
|
12
|
+
*/
|
|
13
|
+
export function noteObjectController(plugin) {
|
|
14
|
+
return async (request, response) => {
|
|
15
|
+
const { id } = request.params;
|
|
16
|
+
|
|
17
|
+
const { application } = request.app.locals;
|
|
18
|
+
const ap_notes = application?.collections?.get("ap_notes");
|
|
19
|
+
|
|
20
|
+
if (!ap_notes) {
|
|
21
|
+
return response.status(404).json({ error: "Not Found" });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const note = await ap_notes.findOne({ _id: id });
|
|
25
|
+
|
|
26
|
+
if (!note) {
|
|
27
|
+
return response.status(404).json({ error: "Not Found" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const noteJson = {
|
|
31
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
32
|
+
id: note.noteId,
|
|
33
|
+
type: "Note",
|
|
34
|
+
attributedTo: note.actorUrl,
|
|
35
|
+
content: note.content,
|
|
36
|
+
published: note.published,
|
|
37
|
+
to: note.to,
|
|
38
|
+
cc: note.cc,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (note.inReplyTo) {
|
|
42
|
+
noteJson.inReplyTo = note.inReplyTo;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
response
|
|
46
|
+
.status(200)
|
|
47
|
+
.set("Content-Type", "application/activity+json; charset=utf-8")
|
|
48
|
+
.set("Cache-Control", "public, max-age=3600")
|
|
49
|
+
.json(noteJson);
|
|
50
|
+
};
|
|
51
|
+
}
|
package/lib/inbox-listeners.js
CHANGED
|
@@ -340,7 +340,9 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
340
340
|
|
|
341
341
|
await addTimelineItem(collections, timelineItem);
|
|
342
342
|
} catch (error) {
|
|
343
|
-
|
|
343
|
+
// Remote object unreachable (timeout, Authorized Fetch, deleted, etc.) — skip
|
|
344
|
+
const cause = error?.cause?.code || error?.message || "unknown";
|
|
345
|
+
console.warn(`[AP] Skipped boost from ${actorUrl}: ${cause}`);
|
|
344
346
|
}
|
|
345
347
|
}
|
|
346
348
|
})
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-strategy author resolution for interaction delivery.
|
|
3
|
+
*
|
|
4
|
+
* Resolves a post URL to the author's Actor object so that Like, Announce,
|
|
5
|
+
* and other activities can be delivered to the correct inbox.
|
|
6
|
+
*
|
|
7
|
+
* Strategies (tried in order):
|
|
8
|
+
* 1. lookupObject on post URL → getAttributedTo
|
|
9
|
+
* 2. Timeline/notification DB lookup → lookupObject on stored author URL
|
|
10
|
+
* 3. Extract author URL from post URL pattern → lookupObject
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract a probable author URL from a post URL using common fediverse patterns.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} postUrl - The post URL
|
|
17
|
+
* @returns {string|null} - Author URL or null
|
|
18
|
+
*
|
|
19
|
+
* Patterns matched:
|
|
20
|
+
* https://instance/users/USERNAME/statuses/ID → https://instance/users/USERNAME
|
|
21
|
+
* https://instance/@USERNAME/ID → https://instance/users/USERNAME
|
|
22
|
+
* https://instance/p/USERNAME/ID → https://instance/users/USERNAME (Pixelfed)
|
|
23
|
+
* https://instance/notice/ID → null (no username in URL)
|
|
24
|
+
*/
|
|
25
|
+
export function extractAuthorUrl(postUrl) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(postUrl);
|
|
28
|
+
const path = parsed.pathname;
|
|
29
|
+
|
|
30
|
+
// /users/USERNAME/statuses/ID — Mastodon, GoToSocial, Akkoma canonical
|
|
31
|
+
const usersMatch = path.match(/^\/users\/([^/]+)\//);
|
|
32
|
+
if (usersMatch) {
|
|
33
|
+
return `${parsed.origin}/users/${usersMatch[1]}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// /@USERNAME/ID — Mastodon display URL
|
|
37
|
+
const atMatch = path.match(/^\/@([^/]+)\/\d/);
|
|
38
|
+
if (atMatch) {
|
|
39
|
+
return `${parsed.origin}/users/${atMatch[1]}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// /p/USERNAME/ID — Pixelfed
|
|
43
|
+
const pixelfedMatch = path.match(/^\/p\/([^/]+)\/\d/);
|
|
44
|
+
if (pixelfedMatch) {
|
|
45
|
+
return `${parsed.origin}/users/${pixelfedMatch[1]}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the author Actor for a given post URL.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} postUrl - The post URL to resolve the author for
|
|
58
|
+
* @param {object} ctx - Fedify context
|
|
59
|
+
* @param {object} documentLoader - Authenticated document loader
|
|
60
|
+
* @param {object} [collections] - Optional MongoDB collections map (application.collections)
|
|
61
|
+
* @returns {Promise<object|null>} - Fedify Actor object or null
|
|
62
|
+
*/
|
|
63
|
+
export async function resolveAuthor(
|
|
64
|
+
postUrl,
|
|
65
|
+
ctx,
|
|
66
|
+
documentLoader,
|
|
67
|
+
collections,
|
|
68
|
+
) {
|
|
69
|
+
// Strategy 1: Look up remote post via Fedify (signed request)
|
|
70
|
+
try {
|
|
71
|
+
const remoteObject = await ctx.lookupObject(new URL(postUrl), {
|
|
72
|
+
documentLoader,
|
|
73
|
+
});
|
|
74
|
+
if (remoteObject && typeof remoteObject.getAttributedTo === "function") {
|
|
75
|
+
const author = await remoteObject.getAttributedTo({ documentLoader });
|
|
76
|
+
const recipient = Array.isArray(author) ? author[0] : author;
|
|
77
|
+
if (recipient) {
|
|
78
|
+
console.info(
|
|
79
|
+
`[ActivityPub] Resolved author via lookupObject for ${postUrl}`,
|
|
80
|
+
);
|
|
81
|
+
return recipient;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.warn(
|
|
86
|
+
`[ActivityPub] lookupObject failed for ${postUrl}:`,
|
|
87
|
+
error.message,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Strategy 2: Use author URL from timeline or notifications
|
|
92
|
+
if (collections) {
|
|
93
|
+
const ap_timeline = collections.get("ap_timeline");
|
|
94
|
+
const ap_notifications = collections.get("ap_notifications");
|
|
95
|
+
|
|
96
|
+
// Search timeline by both uid (canonical) and url (display)
|
|
97
|
+
let authorUrl = null;
|
|
98
|
+
if (ap_timeline) {
|
|
99
|
+
const item = await ap_timeline.findOne({
|
|
100
|
+
$or: [{ uid: postUrl }, { url: postUrl }],
|
|
101
|
+
});
|
|
102
|
+
authorUrl = item?.author?.url;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fall back to notifications if not in timeline
|
|
106
|
+
if (!authorUrl && ap_notifications) {
|
|
107
|
+
const notif = await ap_notifications.findOne({
|
|
108
|
+
$or: [{ objectUrl: postUrl }, { targetUrl: postUrl }],
|
|
109
|
+
});
|
|
110
|
+
authorUrl = notif?.actorUrl;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (authorUrl) {
|
|
114
|
+
try {
|
|
115
|
+
const actor = await ctx.lookupObject(new URL(authorUrl), {
|
|
116
|
+
documentLoader,
|
|
117
|
+
});
|
|
118
|
+
if (actor) {
|
|
119
|
+
console.info(
|
|
120
|
+
`[ActivityPub] Resolved author via DB for ${postUrl} → ${authorUrl}`,
|
|
121
|
+
);
|
|
122
|
+
return actor;
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.warn(
|
|
126
|
+
`[ActivityPub] lookupObject failed for author ${authorUrl}:`,
|
|
127
|
+
error.message,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Strategy 3: Extract author URL from post URL pattern
|
|
134
|
+
const extractedUrl = extractAuthorUrl(postUrl);
|
|
135
|
+
if (extractedUrl) {
|
|
136
|
+
try {
|
|
137
|
+
const actor = await ctx.lookupObject(new URL(extractedUrl), {
|
|
138
|
+
documentLoader,
|
|
139
|
+
});
|
|
140
|
+
if (actor) {
|
|
141
|
+
console.info(
|
|
142
|
+
`[ActivityPub] Resolved author via URL pattern for ${postUrl} → ${extractedUrl}`,
|
|
143
|
+
);
|
|
144
|
+
return actor;
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.warn(
|
|
148
|
+
`[ActivityPub] lookupObject failed for extracted author ${extractedUrl}:`,
|
|
149
|
+
error.message,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.warn(`[ActivityPub] All author resolution strategies failed for ${postUrl}`);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.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",
|