@rmdes/indiekit-endpoint-activitypub 3.13.10 → 3.13.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/index.js +29 -806
- package/lib/defaults.js +37 -0
- package/lib/endpoint-federation.js +407 -0
- package/lib/mastodon/routes/oauth.js +9 -0
- package/lib/navigation.js +22 -0
- package/lib/routes/admin-routes.js +204 -0
- package/lib/routes/public-routes.js +175 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,171 +1,44 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { waitForReady } from "@rmdes/indiekit-startup-gate";
|
|
3
3
|
import { ACTIVITYPUB_BLOCKS } from "./lib/blocks.js";
|
|
4
|
+
import { resolveOptions } from "./lib/defaults.js";
|
|
5
|
+
import { buildNavigationItems } from "./lib/navigation.js";
|
|
4
6
|
|
|
5
|
-
import { setupFederation
|
|
7
|
+
import { setupFederation } from "./lib/federation-setup.js";
|
|
6
8
|
import { createMastodonRouter } from "./lib/mastodon/router.js";
|
|
7
9
|
import { setLocalIdentity } from "./lib/mastodon/entities/status.js";
|
|
8
10
|
import { initRedisCache } from "./lib/redis-cache.js";
|
|
9
11
|
import { createIndexes } from "./lib/init-indexes.js";
|
|
10
12
|
import { lookupWithSecurity } from "./lib/lookup-helpers.js";
|
|
11
|
-
import {
|
|
12
|
-
needsDirectFollow,
|
|
13
|
-
sendDirectFollow,
|
|
14
|
-
sendDirectUnfollow,
|
|
15
|
-
} from "./lib/direct-follow.js";
|
|
16
13
|
import {
|
|
17
14
|
createFedifyMiddleware,
|
|
18
15
|
} from "./lib/federation-bridge.js";
|
|
19
|
-
import {
|
|
20
|
-
jf2ToActivityStreams,
|
|
21
|
-
jf2ToAS2Activity,
|
|
22
|
-
} from "./lib/jf2-to-as2.js";
|
|
23
16
|
import { createSyndicator } from "./lib/syndicator.js";
|
|
24
|
-
import { dashboardController } from "./lib/controllers/dashboard.js";
|
|
25
|
-
import {
|
|
26
|
-
readerController,
|
|
27
|
-
notificationsController,
|
|
28
|
-
markAllNotificationsReadController,
|
|
29
|
-
clearAllNotificationsController,
|
|
30
|
-
deleteNotificationController,
|
|
31
|
-
composeController,
|
|
32
|
-
submitComposeController,
|
|
33
|
-
remoteProfileController,
|
|
34
|
-
followController,
|
|
35
|
-
unfollowController,
|
|
36
|
-
postDetailController,
|
|
37
|
-
} from "./lib/controllers/reader.js";
|
|
38
|
-
import {
|
|
39
|
-
likeController,
|
|
40
|
-
unlikeController,
|
|
41
|
-
boostController,
|
|
42
|
-
unboostController,
|
|
43
|
-
} from "./lib/controllers/interactions.js";
|
|
44
|
-
import {
|
|
45
|
-
muteController,
|
|
46
|
-
unmuteController,
|
|
47
|
-
blockController,
|
|
48
|
-
unblockController,
|
|
49
|
-
blockServerController,
|
|
50
|
-
unblockServerController,
|
|
51
|
-
moderationController,
|
|
52
|
-
filterModeController,
|
|
53
|
-
} from "./lib/controllers/moderation.js";
|
|
54
|
-
import { followersController } from "./lib/controllers/followers.js";
|
|
55
|
-
import {
|
|
56
|
-
approveFollowController,
|
|
57
|
-
rejectFollowController,
|
|
58
|
-
} from "./lib/controllers/follow-requests.js";
|
|
59
|
-
import { followingController } from "./lib/controllers/following.js";
|
|
60
|
-
import { activitiesController } from "./lib/controllers/activities.js";
|
|
61
|
-
import {
|
|
62
|
-
migrateGetController,
|
|
63
|
-
migratePostController,
|
|
64
|
-
migrateImportController,
|
|
65
|
-
} from "./lib/controllers/migrate.js";
|
|
66
|
-
import {
|
|
67
|
-
profileGetController,
|
|
68
|
-
profilePostController,
|
|
69
|
-
} from "./lib/controllers/profile.js";
|
|
70
|
-
import {
|
|
71
|
-
featuredGetController,
|
|
72
|
-
featuredPinController,
|
|
73
|
-
featuredUnpinController,
|
|
74
|
-
} from "./lib/controllers/featured.js";
|
|
75
|
-
import {
|
|
76
|
-
featuredTagsGetController,
|
|
77
|
-
featuredTagsAddController,
|
|
78
|
-
featuredTagsRemoveController,
|
|
79
|
-
} from "./lib/controllers/featured-tags.js";
|
|
80
|
-
import { resolveController } from "./lib/controllers/resolve.js";
|
|
81
|
-
import { tagTimelineController } from "./lib/controllers/tag-timeline.js";
|
|
82
|
-
import { apiTimelineController, countNewController, markReadController } from "./lib/controllers/api-timeline.js";
|
|
83
17
|
import {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
} from "./lib/
|
|
96
|
-
import {
|
|
97
|
-
listTabsController,
|
|
98
|
-
addTabController,
|
|
99
|
-
removeTabController,
|
|
100
|
-
reorderTabsController,
|
|
101
|
-
} from "./lib/controllers/tabs.js";
|
|
102
|
-
import { hashtagExploreApiController } from "./lib/controllers/hashtag-explore.js";
|
|
103
|
-
import { publicProfileController } from "./lib/controllers/public-profile.js";
|
|
104
|
-
import {
|
|
105
|
-
messagesController,
|
|
106
|
-
messageComposeController,
|
|
107
|
-
submitMessageController,
|
|
108
|
-
markAllMessagesReadController,
|
|
109
|
-
clearAllMessagesController,
|
|
110
|
-
deleteMessageController,
|
|
111
|
-
} from "./lib/controllers/messages.js";
|
|
112
|
-
import { authorizeInteractionController } from "./lib/controllers/authorize-interaction.js";
|
|
113
|
-
import { myProfileController } from "./lib/controllers/my-profile.js";
|
|
114
|
-
import {
|
|
115
|
-
refollowPauseController,
|
|
116
|
-
refollowResumeController,
|
|
117
|
-
refollowStatusController,
|
|
118
|
-
} from "./lib/controllers/refollow.js";
|
|
18
|
+
loadRsaPrivateKey,
|
|
19
|
+
followActor,
|
|
20
|
+
unfollowActor,
|
|
21
|
+
broadcastActorUpdate,
|
|
22
|
+
broadcastDelete,
|
|
23
|
+
broadcastPostUpdate,
|
|
24
|
+
deletePost,
|
|
25
|
+
updatePost,
|
|
26
|
+
getActorUrl,
|
|
27
|
+
} from "./lib/endpoint-federation.js";
|
|
28
|
+
import { buildAdminRoutes } from "./lib/routes/admin-routes.js";
|
|
29
|
+
import { buildRoutesPublic, buildContentNegotiationRoutes } from "./lib/routes/public-routes.js";
|
|
119
30
|
import { startBatchRefollow } from "./lib/batch-refollow.js";
|
|
120
|
-
import { logActivity } from "./lib/activity-log.js";
|
|
121
|
-
import { batchBroadcast } from "./lib/batch-broadcast.js";
|
|
122
31
|
import { scheduleCleanup } from "./lib/timeline-cleanup.js";
|
|
123
32
|
import { runSeparateMentionsMigration } from "./lib/migrations/separate-mentions.js";
|
|
124
33
|
import { loadBlockedServersToRedis } from "./lib/storage/server-blocks.js";
|
|
125
34
|
import { scheduleKeyRefresh } from "./lib/key-refresh.js";
|
|
126
35
|
import { startInboxProcessor } from "./lib/inbox-queue.js";
|
|
127
|
-
import { deleteFederationController } from "./lib/controllers/federation-delete.js";
|
|
128
|
-
import {
|
|
129
|
-
federationMgmtController,
|
|
130
|
-
rebroadcastController,
|
|
131
|
-
viewApJsonController,
|
|
132
|
-
broadcastActorUpdateController,
|
|
133
|
-
lookupObjectController,
|
|
134
|
-
} from "./lib/controllers/federation-mgmt.js";
|
|
135
|
-
import {
|
|
136
|
-
settingsGetController,
|
|
137
|
-
settingsPostController,
|
|
138
|
-
} from "./lib/controllers/settings.js";
|
|
139
|
-
|
|
140
|
-
const defaults = {
|
|
141
|
-
mountPath: "/activitypub",
|
|
142
|
-
actor: {
|
|
143
|
-
handle: "rick",
|
|
144
|
-
name: "",
|
|
145
|
-
summary: "",
|
|
146
|
-
icon: "",
|
|
147
|
-
},
|
|
148
|
-
checked: true,
|
|
149
|
-
alsoKnownAs: "",
|
|
150
|
-
activityRetentionDays: 90,
|
|
151
|
-
storeRawActivities: false,
|
|
152
|
-
redisUrl: "",
|
|
153
|
-
parallelWorkers: 5,
|
|
154
|
-
actorType: "Person",
|
|
155
|
-
logLevel: "warning",
|
|
156
|
-
timelineRetention: 1000,
|
|
157
|
-
notificationRetentionDays: 30,
|
|
158
|
-
debugDashboard: false,
|
|
159
|
-
debugPassword: "",
|
|
160
|
-
defaultVisibility: "public", // "public" | "unlisted" | "followers"
|
|
161
|
-
};
|
|
162
36
|
|
|
163
37
|
export default class ActivityPubEndpoint {
|
|
164
38
|
name = "ActivityPub endpoint";
|
|
165
39
|
|
|
166
40
|
constructor(options = {}) {
|
|
167
|
-
this.options =
|
|
168
|
-
this.options.actor = { ...defaults.actor, ...options.actor };
|
|
41
|
+
this.options = resolveOptions(options);
|
|
169
42
|
this.mountPath = this.options.mountPath;
|
|
170
43
|
|
|
171
44
|
this._publicationUrl = "";
|
|
@@ -179,48 +52,7 @@ export default class ActivityPubEndpoint {
|
|
|
179
52
|
}
|
|
180
53
|
|
|
181
54
|
get navigationItems() {
|
|
182
|
-
return
|
|
183
|
-
{
|
|
184
|
-
href: this.options.mountPath,
|
|
185
|
-
text: "activitypub.title",
|
|
186
|
-
requiresDatabase: true,
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
href: `${this.options.mountPath}/admin/reader`,
|
|
190
|
-
text: "activitypub.reader.title",
|
|
191
|
-
requiresDatabase: true,
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
href: `${this.options.mountPath}/admin/reader/notifications`,
|
|
195
|
-
text: "activitypub.notifications.title",
|
|
196
|
-
requiresDatabase: true,
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
href: `${this.options.mountPath}/admin/reader/messages`,
|
|
200
|
-
text: "activitypub.messages.title",
|
|
201
|
-
requiresDatabase: true,
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
href: `${this.options.mountPath}/admin/reader/moderation`,
|
|
205
|
-
text: "activitypub.moderation.title",
|
|
206
|
-
requiresDatabase: true,
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
href: `${this.options.mountPath}/admin/my-profile`,
|
|
210
|
-
text: "activitypub.myProfile.title",
|
|
211
|
-
requiresDatabase: true,
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
href: `${this.options.mountPath}/admin/federation`,
|
|
215
|
-
text: "activitypub.federationMgmt.title",
|
|
216
|
-
requiresDatabase: true,
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
href: `${this.options.mountPath}/admin/settings`,
|
|
220
|
-
text: "activitypub.settings.title",
|
|
221
|
-
requiresDatabase: true,
|
|
222
|
-
},
|
|
223
|
-
];
|
|
55
|
+
return buildNavigationItems(this.options.mountPath);
|
|
224
56
|
}
|
|
225
57
|
|
|
226
58
|
/**
|
|
@@ -244,159 +76,14 @@ export default class ActivityPubEndpoint {
|
|
|
244
76
|
* Fedify handles actor, inbox, outbox, followers, following.
|
|
245
77
|
*/
|
|
246
78
|
get routesPublic() {
|
|
247
|
-
|
|
248
|
-
const self = this;
|
|
249
|
-
|
|
250
|
-
router.use((req, res, next) => {
|
|
251
|
-
if (!self._fedifyMiddleware) return next();
|
|
252
|
-
// Skip Fedify for admin UI routes — they're handled by the
|
|
253
|
-
// authenticated `routes` getter, not the federation layer.
|
|
254
|
-
if (req.path.startsWith("/admin")) return next();
|
|
255
|
-
|
|
256
|
-
// Fedify's acceptsJsonLd() treats Accept: */* as NOT accepting JSON-LD
|
|
257
|
-
// (it only returns true for explicit application/activity+json etc.).
|
|
258
|
-
// Remote servers fetching actor URLs for HTTP Signature verification
|
|
259
|
-
// (e.g. tags.pub) often omit Accept or use */* — they get HTML back
|
|
260
|
-
// instead of the actor JSON, causing "public key not found" errors.
|
|
261
|
-
// Fix: for GET requests to actor paths, upgrade ambiguous Accept headers
|
|
262
|
-
// to application/activity+json so Fedify serves JSON-LD. Explicit
|
|
263
|
-
// text/html requests (browsers) are unaffected.
|
|
264
|
-
if (req.method === "GET" && /^\/users\/[^/]+\/?$/.test(req.path)) {
|
|
265
|
-
const accept = req.get("accept") || "";
|
|
266
|
-
if (!accept.includes("text/html") && !accept.includes("application/xhtml+xml")) {
|
|
267
|
-
req.headers["accept"] = "application/activity+json";
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return self._fedifyMiddleware(req, res, next);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// Authorize interaction — remote follow / subscribe endpoint.
|
|
275
|
-
// Remote servers redirect users here via the WebFinger subscribe template.
|
|
276
|
-
router.get("/authorize_interaction", authorizeInteractionController(self));
|
|
277
|
-
|
|
278
|
-
// HTML fallback for actor URL — serve a public profile page.
|
|
279
|
-
// Fedify only serves JSON-LD; browsers get 406 and fall through here.
|
|
280
|
-
router.get("/users/:identifier", publicProfileController(self));
|
|
281
|
-
|
|
282
|
-
// Catch-all for federation paths that Fedify didn't handle (e.g. GET
|
|
283
|
-
// on inbox). Without this, they fall through to Indiekit's auth
|
|
284
|
-
// middleware and redirect to the login page.
|
|
285
|
-
router.all("/users/:identifier/inbox", (req, res) => {
|
|
286
|
-
res
|
|
287
|
-
.status(405)
|
|
288
|
-
.set("Allow", "POST")
|
|
289
|
-
.type("application/activity+json")
|
|
290
|
-
.json({
|
|
291
|
-
error: "Method Not Allowed",
|
|
292
|
-
message: "The inbox only accepts POST requests",
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
router.all("/inbox", (req, res) => {
|
|
296
|
-
res
|
|
297
|
-
.status(405)
|
|
298
|
-
.set("Allow", "POST")
|
|
299
|
-
.type("application/activity+json")
|
|
300
|
-
.json({
|
|
301
|
-
error: "Method Not Allowed",
|
|
302
|
-
message: "The shared inbox only accepts POST requests",
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
return router;
|
|
79
|
+
return buildRoutesPublic(this);
|
|
307
80
|
}
|
|
308
81
|
|
|
309
82
|
/**
|
|
310
83
|
* Authenticated admin routes — mounted at mountPath, behind IndieAuth.
|
|
311
84
|
*/
|
|
312
85
|
get routes() {
|
|
313
|
-
|
|
314
|
-
const mp = this.options.mountPath;
|
|
315
|
-
|
|
316
|
-
router.get("/", dashboardController(mp));
|
|
317
|
-
router.get("/admin/reader", readerController(mp));
|
|
318
|
-
router.get("/admin/reader/tag", tagTimelineController(mp));
|
|
319
|
-
router.get("/admin/reader/api/timeline", apiTimelineController(mp));
|
|
320
|
-
router.get("/admin/reader/api/timeline/count-new", countNewController());
|
|
321
|
-
router.post("/admin/reader/api/timeline/mark-read", markReadController());
|
|
322
|
-
router.get("/admin/reader/explore", exploreController(mp));
|
|
323
|
-
router.get("/admin/reader/api/explore", exploreApiController(mp));
|
|
324
|
-
router.get("/admin/reader/api/explore/hashtag", hashtagExploreApiController(mp));
|
|
325
|
-
router.get("/admin/reader/api/instances", instanceSearchApiController(mp));
|
|
326
|
-
router.get("/admin/reader/api/instance-check", instanceCheckApiController(mp));
|
|
327
|
-
router.get("/admin/reader/api/popular-accounts", popularAccountsApiController(mp));
|
|
328
|
-
router.get("/admin/reader/api/tabs", listTabsController(mp));
|
|
329
|
-
router.post("/admin/reader/api/tabs", addTabController(mp));
|
|
330
|
-
router.post("/admin/reader/api/tabs/remove", removeTabController(mp));
|
|
331
|
-
router.patch("/admin/reader/api/tabs/reorder", reorderTabsController(mp));
|
|
332
|
-
router.post("/admin/reader/follow-tag", followTagController(mp));
|
|
333
|
-
router.post("/admin/reader/unfollow-tag", unfollowTagController(mp));
|
|
334
|
-
router.post("/admin/reader/follow-tag-global", followTagGloballyController(mp, this));
|
|
335
|
-
router.post("/admin/reader/unfollow-tag-global", unfollowTagGloballyController(mp, this));
|
|
336
|
-
router.get("/admin/reader/notifications", notificationsController(mp));
|
|
337
|
-
router.post("/admin/reader/notifications/mark-read", markAllNotificationsReadController(mp));
|
|
338
|
-
router.post("/admin/reader/notifications/clear", clearAllNotificationsController(mp));
|
|
339
|
-
router.post("/admin/reader/notifications/delete", deleteNotificationController(mp));
|
|
340
|
-
router.get("/admin/reader/messages", messagesController(mp));
|
|
341
|
-
router.get("/admin/reader/messages/compose", messageComposeController(mp, this));
|
|
342
|
-
router.post("/admin/reader/messages/compose", submitMessageController(mp, this));
|
|
343
|
-
router.post("/admin/reader/messages/mark-read", markAllMessagesReadController(mp));
|
|
344
|
-
router.post("/admin/reader/messages/clear", clearAllMessagesController(mp));
|
|
345
|
-
router.post("/admin/reader/messages/delete", deleteMessageController(mp));
|
|
346
|
-
router.get("/admin/reader/compose", composeController(mp, this));
|
|
347
|
-
router.post("/admin/reader/compose", submitComposeController(mp, this));
|
|
348
|
-
router.post("/admin/reader/like", likeController(mp, this));
|
|
349
|
-
router.post("/admin/reader/unlike", unlikeController(mp, this));
|
|
350
|
-
router.post("/admin/reader/boost", boostController(mp, this));
|
|
351
|
-
router.post("/admin/reader/unboost", unboostController(mp, this));
|
|
352
|
-
router.get("/admin/reader/resolve", resolveController(mp, this));
|
|
353
|
-
router.get("/admin/reader/profile", remoteProfileController(mp, this));
|
|
354
|
-
router.get("/admin/reader/post", postDetailController(mp, this));
|
|
355
|
-
router.post("/admin/reader/follow", followController(mp, this));
|
|
356
|
-
router.post("/admin/reader/unfollow", unfollowController(mp, this));
|
|
357
|
-
router.get("/admin/reader/moderation", moderationController(mp));
|
|
358
|
-
router.post("/admin/reader/moderation/filter-mode", filterModeController(mp));
|
|
359
|
-
router.post("/admin/reader/mute", muteController(mp, this));
|
|
360
|
-
router.post("/admin/reader/unmute", unmuteController(mp, this));
|
|
361
|
-
router.post("/admin/reader/block", blockController(mp, this));
|
|
362
|
-
router.post("/admin/reader/unblock", unblockController(mp, this));
|
|
363
|
-
router.post("/admin/reader/block-server", blockServerController(mp));
|
|
364
|
-
router.post("/admin/reader/unblock-server", unblockServerController(mp));
|
|
365
|
-
router.get("/admin/followers", followersController(mp));
|
|
366
|
-
router.post("/admin/followers/approve", approveFollowController(mp, this));
|
|
367
|
-
router.post("/admin/followers/reject", rejectFollowController(mp, this));
|
|
368
|
-
router.get("/admin/following", followingController(mp));
|
|
369
|
-
router.get("/admin/activities", activitiesController(mp));
|
|
370
|
-
router.get("/admin/featured", featuredGetController(mp));
|
|
371
|
-
router.post("/admin/featured/pin", featuredPinController(mp, this));
|
|
372
|
-
router.post("/admin/featured/unpin", featuredUnpinController(mp, this));
|
|
373
|
-
router.get("/admin/tags", featuredTagsGetController(mp));
|
|
374
|
-
router.post("/admin/tags/add", featuredTagsAddController(mp, this));
|
|
375
|
-
router.post("/admin/tags/remove", featuredTagsRemoveController(mp, this));
|
|
376
|
-
router.get("/admin/profile", profileGetController(mp));
|
|
377
|
-
router.post("/admin/profile", profilePostController(mp, this));
|
|
378
|
-
router.get("/admin/my-profile", myProfileController(this));
|
|
379
|
-
router.get("/admin/migrate", migrateGetController(mp, this.options));
|
|
380
|
-
router.post("/admin/migrate", migratePostController(mp, this.options));
|
|
381
|
-
router.post(
|
|
382
|
-
"/admin/migrate/import",
|
|
383
|
-
migrateImportController(mp, this.options),
|
|
384
|
-
);
|
|
385
|
-
router.post("/admin/refollow/pause", refollowPauseController(mp, this));
|
|
386
|
-
router.post("/admin/refollow/resume", refollowResumeController(mp, this));
|
|
387
|
-
router.get("/admin/refollow/status", refollowStatusController(mp));
|
|
388
|
-
router.post("/admin/federation/delete", deleteFederationController(mp, this));
|
|
389
|
-
router.get("/admin/federation", federationMgmtController(mp, this));
|
|
390
|
-
router.post("/admin/federation/rebroadcast", rebroadcastController(mp, this));
|
|
391
|
-
router.get("/admin/federation/ap-json", viewApJsonController(mp, this));
|
|
392
|
-
router.post("/admin/federation/broadcast-actor", broadcastActorUpdateController(mp, this));
|
|
393
|
-
router.get("/admin/federation/lookup", lookupObjectController(mp, this));
|
|
394
|
-
|
|
395
|
-
// Settings
|
|
396
|
-
router.get("/admin/settings", settingsGetController(mp));
|
|
397
|
-
router.post("/admin/settings", settingsPostController(mp));
|
|
398
|
-
|
|
399
|
-
return router;
|
|
86
|
+
return buildAdminRoutes(this);
|
|
400
87
|
}
|
|
401
88
|
|
|
402
89
|
/**
|
|
@@ -405,95 +92,7 @@ export default class ActivityPubEndpoint {
|
|
|
405
92
|
* at /nodeinfo/2.1 (delegated to Fedify).
|
|
406
93
|
*/
|
|
407
94
|
get contentNegotiationRoutes() {
|
|
408
|
-
|
|
409
|
-
const self = this;
|
|
410
|
-
|
|
411
|
-
// Let Fedify handle NodeInfo data (/nodeinfo/2.1)
|
|
412
|
-
// Only pass GET/HEAD requests — POST/PUT/DELETE must not go through
|
|
413
|
-
// Fedify here, because fromExpressRequest() consumes the body stream,
|
|
414
|
-
// breaking Express body-parsed routes downstream (e.g. admin forms).
|
|
415
|
-
router.use((req, res, next) => {
|
|
416
|
-
if (!self._fedifyMiddleware) return next();
|
|
417
|
-
if (req.method !== "GET" && req.method !== "HEAD") return next();
|
|
418
|
-
// Only delegate to Fedify for NodeInfo data endpoint (/nodeinfo/2.1).
|
|
419
|
-
// All other paths in this root-mounted router are handled by the
|
|
420
|
-
// content negotiation catch-all below. Passing arbitrary paths like
|
|
421
|
-
// /notes/... to Fedify causes harmless but noisy 404 warnings.
|
|
422
|
-
if (!req.path.startsWith("/nodeinfo/")) return next();
|
|
423
|
-
return self._fedifyMiddleware(req, res, next);
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
// Content negotiation for AP clients on regular URLs
|
|
427
|
-
router.get("{*path}", async (req, res, next) => {
|
|
428
|
-
const accept = req.headers.accept || "";
|
|
429
|
-
const isActivityPub =
|
|
430
|
-
accept.includes("application/activity+json") ||
|
|
431
|
-
accept.includes("application/ld+json");
|
|
432
|
-
|
|
433
|
-
if (!isActivityPub) {
|
|
434
|
-
return next();
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
try {
|
|
438
|
-
// Root URL — redirect to Fedify actor
|
|
439
|
-
if (req.path === "/") {
|
|
440
|
-
const actorPath = `${self.options.mountPath}/users/${self.options.actor.handle}`;
|
|
441
|
-
return res.redirect(actorPath);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Post URLs — look up in database and convert to AS2
|
|
445
|
-
const { application } = req.app.locals;
|
|
446
|
-
const postsCollection = application?.collections?.get("posts");
|
|
447
|
-
if (!postsCollection) {
|
|
448
|
-
return next();
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const requestUrl = `${self._publicationUrl}${req.path.slice(1)}`;
|
|
452
|
-
const post = await postsCollection.findOne({
|
|
453
|
-
"properties.url": requestUrl,
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
if (!post || post.properties?.deleted) {
|
|
457
|
-
// FEP-4f05: Serve Tombstone for deleted posts
|
|
458
|
-
const { getTombstone } = await import("./lib/storage/tombstones.js");
|
|
459
|
-
const tombstone = await getTombstone(self._collections, requestUrl);
|
|
460
|
-
if (tombstone) {
|
|
461
|
-
res.status(410).set("Content-Type", "application/activity+json").json({
|
|
462
|
-
"@context": "https://www.w3.org/ns/activitystreams",
|
|
463
|
-
type: "Tombstone",
|
|
464
|
-
id: requestUrl,
|
|
465
|
-
formerType: tombstone.formerType,
|
|
466
|
-
published: tombstone.published || undefined,
|
|
467
|
-
deleted: tombstone.deleted,
|
|
468
|
-
});
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
return next();
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const actorUrl = self._getActorUrl();
|
|
475
|
-
const activity = jf2ToActivityStreams(
|
|
476
|
-
post.properties,
|
|
477
|
-
actorUrl,
|
|
478
|
-
self._publicationUrl,
|
|
479
|
-
{ visibility: self.options.defaultVisibility },
|
|
480
|
-
);
|
|
481
|
-
|
|
482
|
-
const object = activity.object || activity;
|
|
483
|
-
res.set("Content-Type", "application/activity+json");
|
|
484
|
-
return res.json({
|
|
485
|
-
"@context": [
|
|
486
|
-
"https://www.w3.org/ns/activitystreams",
|
|
487
|
-
"https://w3id.org/security/v1",
|
|
488
|
-
],
|
|
489
|
-
...object,
|
|
490
|
-
});
|
|
491
|
-
} catch {
|
|
492
|
-
return next();
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
return router;
|
|
95
|
+
return buildContentNegotiationRoutes(this);
|
|
497
96
|
}
|
|
498
97
|
|
|
499
98
|
/**
|
|
@@ -517,141 +116,11 @@ export default class ActivityPubEndpoint {
|
|
|
517
116
|
* @returns {Promise<CryptoKey|null>}
|
|
518
117
|
*/
|
|
519
118
|
async _loadRsaPrivateKey() {
|
|
520
|
-
|
|
521
|
-
const keyDoc = await this._collections.ap_keys.findOne({
|
|
522
|
-
privateKeyPem: { $exists: true },
|
|
523
|
-
});
|
|
524
|
-
if (!keyDoc?.privateKeyPem) return null;
|
|
525
|
-
const pemBody = keyDoc.privateKeyPem
|
|
526
|
-
.replace(/-----[^-]+-----/g, "")
|
|
527
|
-
.replace(/\s/g, "");
|
|
528
|
-
return await crypto.subtle.importKey(
|
|
529
|
-
"pkcs8",
|
|
530
|
-
Buffer.from(pemBody, "base64"),
|
|
531
|
-
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
532
|
-
true,
|
|
533
|
-
["sign"],
|
|
534
|
-
);
|
|
535
|
-
} catch (error) {
|
|
536
|
-
console.error("[ActivityPub] Failed to load RSA key:", error.message);
|
|
537
|
-
return null;
|
|
538
|
-
}
|
|
119
|
+
return loadRsaPrivateKey(this);
|
|
539
120
|
}
|
|
540
121
|
|
|
541
122
|
async followActor(actorUrl, actorInfo = {}) {
|
|
542
|
-
|
|
543
|
-
return { ok: false, error: "Federation not initialized" };
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
try {
|
|
547
|
-
const { Follow } = await import("@fedify/fedify/vocab");
|
|
548
|
-
const handle = this.options.actor.handle;
|
|
549
|
-
const ctx = this._federation.createContext(
|
|
550
|
-
new URL(this._publicationUrl),
|
|
551
|
-
{ handle, publicationUrl: this._publicationUrl },
|
|
552
|
-
);
|
|
553
|
-
|
|
554
|
-
// Resolve the remote actor to get their inbox
|
|
555
|
-
// lookupWithSecurity handles signed→unsigned fallback automatically
|
|
556
|
-
const documentLoader = await ctx.getDocumentLoader({
|
|
557
|
-
identifier: handle,
|
|
558
|
-
});
|
|
559
|
-
const remoteActor = await lookupWithSecurity(ctx, actorUrl, {
|
|
560
|
-
documentLoader,
|
|
561
|
-
});
|
|
562
|
-
if (!remoteActor) {
|
|
563
|
-
return { ok: false, error: "Could not resolve remote actor" };
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// Send Follow activity
|
|
567
|
-
if (needsDirectFollow(actorUrl)) {
|
|
568
|
-
// tags.pub rejects Fedify's LD Signature context (identity/v1).
|
|
569
|
-
// Send a minimal signed Follow directly, bypassing the outbox pipeline.
|
|
570
|
-
// See: https://github.com/social-web-foundation/tags.pub/issues/10
|
|
571
|
-
const rsaKey = await this._loadRsaPrivateKey();
|
|
572
|
-
if (!rsaKey) {
|
|
573
|
-
return { ok: false, error: "No RSA key available for direct follow" };
|
|
574
|
-
}
|
|
575
|
-
const result = await sendDirectFollow({
|
|
576
|
-
actorUri: ctx.getActorUri(handle).href,
|
|
577
|
-
targetActorUrl: actorUrl,
|
|
578
|
-
inboxUrl: remoteActor.inboxId?.href,
|
|
579
|
-
keyId: `${ctx.getActorUri(handle).href}#main-key`,
|
|
580
|
-
privateKey: rsaKey,
|
|
581
|
-
});
|
|
582
|
-
if (!result.ok) {
|
|
583
|
-
return { ok: false, error: result.error };
|
|
584
|
-
}
|
|
585
|
-
} else {
|
|
586
|
-
const follow = new Follow({
|
|
587
|
-
actor: ctx.getActorUri(handle),
|
|
588
|
-
object: new URL(actorUrl),
|
|
589
|
-
});
|
|
590
|
-
await ctx.sendActivity({ identifier: handle }, remoteActor, follow, {
|
|
591
|
-
orderingKey: actorUrl,
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Store in ap_following
|
|
596
|
-
const name =
|
|
597
|
-
actorInfo.name ||
|
|
598
|
-
remoteActor.name?.toString() ||
|
|
599
|
-
remoteActor.preferredUsername?.toString() ||
|
|
600
|
-
actorUrl;
|
|
601
|
-
const actorHandle =
|
|
602
|
-
actorInfo.handle ||
|
|
603
|
-
remoteActor.preferredUsername?.toString() ||
|
|
604
|
-
"";
|
|
605
|
-
const avatar =
|
|
606
|
-
actorInfo.photo ||
|
|
607
|
-
(remoteActor.icon
|
|
608
|
-
? (await remoteActor.icon)?.url?.href || ""
|
|
609
|
-
: "");
|
|
610
|
-
const inbox = remoteActor.inboxId?.href || "";
|
|
611
|
-
const sharedInbox = remoteActor.endpoints?.sharedInbox?.href || "";
|
|
612
|
-
|
|
613
|
-
await this._collections.ap_following.updateOne(
|
|
614
|
-
{ actorUrl },
|
|
615
|
-
{
|
|
616
|
-
$set: {
|
|
617
|
-
actorUrl,
|
|
618
|
-
handle: actorHandle,
|
|
619
|
-
name,
|
|
620
|
-
avatar,
|
|
621
|
-
inbox,
|
|
622
|
-
sharedInbox,
|
|
623
|
-
followedAt: new Date().toISOString(),
|
|
624
|
-
source: "reader",
|
|
625
|
-
},
|
|
626
|
-
},
|
|
627
|
-
{ upsert: true },
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
console.info(`[ActivityPub] Sent Follow to ${actorUrl}`);
|
|
631
|
-
|
|
632
|
-
await logActivity(this._collections.ap_activities, {
|
|
633
|
-
direction: "outbound",
|
|
634
|
-
type: "Follow",
|
|
635
|
-
actorUrl: this._publicationUrl,
|
|
636
|
-
objectUrl: actorUrl,
|
|
637
|
-
actorName: name,
|
|
638
|
-
summary: `Sent Follow to ${name} (${actorUrl})`,
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
return { ok: true };
|
|
642
|
-
} catch (error) {
|
|
643
|
-
console.error(`[ActivityPub] Follow failed for ${actorUrl}:`, error.message);
|
|
644
|
-
|
|
645
|
-
await logActivity(this._collections.ap_activities, {
|
|
646
|
-
direction: "outbound",
|
|
647
|
-
type: "Follow",
|
|
648
|
-
actorUrl: this._publicationUrl,
|
|
649
|
-
objectUrl: actorUrl,
|
|
650
|
-
summary: `Follow failed for ${actorUrl}: ${error.message}`,
|
|
651
|
-
}).catch(() => {});
|
|
652
|
-
|
|
653
|
-
return { ok: false, error: error.message };
|
|
654
|
-
}
|
|
123
|
+
return followActor(this, actorUrl, actorInfo);
|
|
655
124
|
}
|
|
656
125
|
|
|
657
126
|
/**
|
|
@@ -660,277 +129,31 @@ export default class ActivityPubEndpoint {
|
|
|
660
129
|
* @returns {Promise<{ok: boolean, error?: string}>}
|
|
661
130
|
*/
|
|
662
131
|
async unfollowActor(actorUrl) {
|
|
663
|
-
|
|
664
|
-
return { ok: false, error: "Federation not initialized" };
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
try {
|
|
668
|
-
const { Follow, Undo } = await import("@fedify/fedify/vocab");
|
|
669
|
-
const handle = this.options.actor.handle;
|
|
670
|
-
const ctx = this._federation.createContext(
|
|
671
|
-
new URL(this._publicationUrl),
|
|
672
|
-
{ handle, publicationUrl: this._publicationUrl },
|
|
673
|
-
);
|
|
674
|
-
|
|
675
|
-
// Use authenticated document loader for servers requiring Authorized Fetch
|
|
676
|
-
const documentLoader = await ctx.getDocumentLoader({
|
|
677
|
-
identifier: handle,
|
|
678
|
-
});
|
|
679
|
-
const remoteActor = await lookupWithSecurity(ctx,actorUrl, {
|
|
680
|
-
documentLoader,
|
|
681
|
-
});
|
|
682
|
-
if (!remoteActor) {
|
|
683
|
-
// Even if we can't resolve, remove locally
|
|
684
|
-
await this._collections.ap_following.deleteOne({ actorUrl });
|
|
685
|
-
|
|
686
|
-
await logActivity(this._collections.ap_activities, {
|
|
687
|
-
direction: "outbound",
|
|
688
|
-
type: "Undo(Follow)",
|
|
689
|
-
actorUrl: this._publicationUrl,
|
|
690
|
-
objectUrl: actorUrl,
|
|
691
|
-
summary: `Removed ${actorUrl} locally (could not resolve remote actor)`,
|
|
692
|
-
}).catch(() => {});
|
|
693
|
-
|
|
694
|
-
return { ok: true };
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
if (needsDirectFollow(actorUrl)) {
|
|
698
|
-
// tags.pub rejects Fedify's LD Signature context (identity/v1).
|
|
699
|
-
// See: https://github.com/social-web-foundation/tags.pub/issues/10
|
|
700
|
-
const rsaKey = await this._loadRsaPrivateKey();
|
|
701
|
-
if (rsaKey) {
|
|
702
|
-
const result = await sendDirectUnfollow({
|
|
703
|
-
actorUri: ctx.getActorUri(handle).href,
|
|
704
|
-
targetActorUrl: actorUrl,
|
|
705
|
-
inboxUrl: remoteActor.inboxId?.href,
|
|
706
|
-
keyId: `${ctx.getActorUri(handle).href}#main-key`,
|
|
707
|
-
privateKey: rsaKey,
|
|
708
|
-
});
|
|
709
|
-
if (!result.ok) {
|
|
710
|
-
console.warn(`[ActivityPub] Direct unfollow failed for ${actorUrl}: ${result.error}`);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
} else {
|
|
714
|
-
const follow = new Follow({
|
|
715
|
-
actor: ctx.getActorUri(handle),
|
|
716
|
-
object: new URL(actorUrl),
|
|
717
|
-
});
|
|
718
|
-
const undo = new Undo({
|
|
719
|
-
actor: ctx.getActorUri(handle),
|
|
720
|
-
object: follow,
|
|
721
|
-
});
|
|
722
|
-
await ctx.sendActivity({ identifier: handle }, remoteActor, undo, {
|
|
723
|
-
orderingKey: actorUrl,
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
await this._collections.ap_following.deleteOne({ actorUrl });
|
|
727
|
-
|
|
728
|
-
console.info(`[ActivityPub] Sent Undo(Follow) to ${actorUrl}`);
|
|
729
|
-
|
|
730
|
-
await logActivity(this._collections.ap_activities, {
|
|
731
|
-
direction: "outbound",
|
|
732
|
-
type: "Undo(Follow)",
|
|
733
|
-
actorUrl: this._publicationUrl,
|
|
734
|
-
objectUrl: actorUrl,
|
|
735
|
-
summary: `Sent Undo(Follow) to ${actorUrl}`,
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
return { ok: true };
|
|
739
|
-
} catch (error) {
|
|
740
|
-
console.error(`[ActivityPub] Unfollow failed for ${actorUrl}:`, error.message);
|
|
741
|
-
|
|
742
|
-
await logActivity(this._collections.ap_activities, {
|
|
743
|
-
direction: "outbound",
|
|
744
|
-
type: "Undo(Follow)",
|
|
745
|
-
actorUrl: this._publicationUrl,
|
|
746
|
-
objectUrl: actorUrl,
|
|
747
|
-
summary: `Unfollow failed for ${actorUrl}: ${error.message}`,
|
|
748
|
-
}).catch(() => {});
|
|
749
|
-
|
|
750
|
-
// Remove locally even if remote delivery fails
|
|
751
|
-
await this._collections.ap_following.deleteOne({ actorUrl }).catch(() => {});
|
|
752
|
-
return { ok: false, error: error.message };
|
|
753
|
-
}
|
|
132
|
+
return unfollowActor(this, actorUrl);
|
|
754
133
|
}
|
|
755
134
|
|
|
756
|
-
/**
|
|
757
|
-
* Send an Update(Person) activity to all followers so remote servers
|
|
758
|
-
* re-fetch the actor object (picking up profile changes, new featured
|
|
759
|
-
* collections, attachments, etc.).
|
|
760
|
-
*
|
|
761
|
-
* Delivery is batched to avoid a thundering herd: hundreds of remote
|
|
762
|
-
* servers simultaneously re-fetching the actor, featured posts, and
|
|
763
|
-
* featured tags after receiving the Update all at once.
|
|
764
|
-
*/
|
|
765
135
|
async broadcastActorUpdate() {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
try {
|
|
769
|
-
const { Update } = await import("@fedify/fedify/vocab");
|
|
770
|
-
const handle = this.options.actor.handle;
|
|
771
|
-
const ctx = this._federation.createContext(
|
|
772
|
-
new URL(this._publicationUrl),
|
|
773
|
-
{ handle, publicationUrl: this._publicationUrl },
|
|
774
|
-
);
|
|
775
|
-
|
|
776
|
-
const actor = await buildPersonActor(
|
|
777
|
-
ctx,
|
|
778
|
-
handle,
|
|
779
|
-
this._collections,
|
|
780
|
-
this.options.actorType,
|
|
781
|
-
);
|
|
782
|
-
if (!actor) {
|
|
783
|
-
console.warn("[ActivityPub] broadcastActorUpdate: could not build actor");
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
const update = new Update({
|
|
788
|
-
actor: ctx.getActorUri(handle),
|
|
789
|
-
object: actor,
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
await batchBroadcast({
|
|
793
|
-
federation: this._federation,
|
|
794
|
-
collections: this._collections,
|
|
795
|
-
publicationUrl: this._publicationUrl,
|
|
796
|
-
handle,
|
|
797
|
-
activity: update,
|
|
798
|
-
label: "Update(Person)",
|
|
799
|
-
objectUrl: this._getActorUrl(),
|
|
800
|
-
});
|
|
801
|
-
} catch (error) {
|
|
802
|
-
console.error("[ActivityPub] broadcastActorUpdate failed:", error.message);
|
|
803
|
-
}
|
|
136
|
+
return broadcastActorUpdate(this);
|
|
804
137
|
}
|
|
805
138
|
|
|
806
|
-
/**
|
|
807
|
-
* Send Delete activity to all followers for a removed post.
|
|
808
|
-
* Mirrors broadcastActorUpdate() pattern: batch delivery with shared inbox dedup.
|
|
809
|
-
* @param {string} postUrl - Full URL of the deleted post
|
|
810
|
-
*/
|
|
811
139
|
async broadcastDelete(postUrl) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
try {
|
|
815
|
-
const { Delete } = await import("@fedify/fedify/vocab");
|
|
816
|
-
const handle = this.options.actor.handle;
|
|
817
|
-
const ctx = this._federation.createContext(
|
|
818
|
-
new URL(this._publicationUrl),
|
|
819
|
-
{ handle, publicationUrl: this._publicationUrl },
|
|
820
|
-
);
|
|
821
|
-
|
|
822
|
-
const del = new Delete({
|
|
823
|
-
actor: ctx.getActorUri(handle),
|
|
824
|
-
object: new URL(postUrl),
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
await batchBroadcast({
|
|
828
|
-
federation: this._federation,
|
|
829
|
-
collections: this._collections,
|
|
830
|
-
publicationUrl: this._publicationUrl,
|
|
831
|
-
handle,
|
|
832
|
-
activity: del,
|
|
833
|
-
label: "Delete",
|
|
834
|
-
objectUrl: postUrl,
|
|
835
|
-
});
|
|
836
|
-
} catch (error) {
|
|
837
|
-
console.warn("[ActivityPub] broadcastDelete failed:", error.message);
|
|
838
|
-
}
|
|
140
|
+
return broadcastDelete(this, postUrl);
|
|
839
141
|
}
|
|
840
142
|
|
|
841
|
-
/**
|
|
842
|
-
* Called by post-content.js when a Micropub delete succeeds.
|
|
843
|
-
* Broadcasts an ActivityPub Delete activity to all followers.
|
|
844
|
-
* @param {string} url - Full URL of the deleted post
|
|
845
|
-
*/
|
|
846
143
|
async delete(url) {
|
|
847
|
-
|
|
848
|
-
try {
|
|
849
|
-
const { addTombstone } = await import("./lib/storage/tombstones.js");
|
|
850
|
-
const postsCol = this._collections.posts;
|
|
851
|
-
const post = postsCol ? await postsCol.findOne({ "properties.url": url }) : null;
|
|
852
|
-
await addTombstone(this._collections, {
|
|
853
|
-
url,
|
|
854
|
-
formerType: post?.properties?.["post-type"] === "article" ? "Article" : "Note",
|
|
855
|
-
published: post?.properties?.published || null,
|
|
856
|
-
deleted: new Date().toISOString(),
|
|
857
|
-
});
|
|
858
|
-
} catch (error) {
|
|
859
|
-
console.warn(`[ActivityPub] Tombstone creation failed for ${url}: ${error.message}`);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
await this.broadcastDelete(url).catch((err) =>
|
|
863
|
-
console.warn(`[ActivityPub] broadcastDelete failed for ${url}: ${err.message}`)
|
|
864
|
-
);
|
|
144
|
+
return deletePost(this, url);
|
|
865
145
|
}
|
|
866
146
|
|
|
867
|
-
/**
|
|
868
|
-
* Called by post-content.js when a Micropub update succeeds.
|
|
869
|
-
* Broadcasts an ActivityPub Update activity for the post to all followers.
|
|
870
|
-
* @param {object} properties - JF2 post properties (must include url)
|
|
871
|
-
*/
|
|
872
147
|
async update(properties) {
|
|
873
|
-
|
|
874
|
-
console.warn(`[ActivityPub] broadcastPostUpdate failed for ${properties?.url}: ${err.message}`)
|
|
875
|
-
);
|
|
148
|
+
return updatePost(this, properties);
|
|
876
149
|
}
|
|
877
150
|
|
|
878
|
-
/**
|
|
879
|
-
* Send an Update activity to all followers for a modified post.
|
|
880
|
-
* Mirrors broadcastDelete() pattern: batch delivery with shared inbox dedup.
|
|
881
|
-
* @param {object} properties - JF2 post properties
|
|
882
|
-
*/
|
|
883
151
|
async broadcastPostUpdate(properties) {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
try {
|
|
887
|
-
const { Update } = await import("@fedify/fedify/vocab");
|
|
888
|
-
const actorUrl = this._getActorUrl();
|
|
889
|
-
const handle = this.options.actor.handle;
|
|
890
|
-
const ctx = this._federation.createContext(
|
|
891
|
-
new URL(this._publicationUrl),
|
|
892
|
-
{ handle, publicationUrl: this._publicationUrl },
|
|
893
|
-
);
|
|
894
|
-
|
|
895
|
-
const createActivity = jf2ToAS2Activity(
|
|
896
|
-
properties,
|
|
897
|
-
actorUrl,
|
|
898
|
-
this._publicationUrl,
|
|
899
|
-
{ visibility: this.options.defaultVisibility },
|
|
900
|
-
);
|
|
901
|
-
|
|
902
|
-
if (!createActivity) {
|
|
903
|
-
console.warn(`[ActivityPub] broadcastPostUpdate: could not convert post to AS2 for ${properties?.url}`);
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
const noteObject = await createActivity.getObject();
|
|
908
|
-
const activity = new Update({
|
|
909
|
-
actor: ctx.getActorUri(handle),
|
|
910
|
-
object: noteObject,
|
|
911
|
-
});
|
|
912
|
-
|
|
913
|
-
await batchBroadcast({
|
|
914
|
-
federation: this._federation,
|
|
915
|
-
collections: this._collections,
|
|
916
|
-
publicationUrl: this._publicationUrl,
|
|
917
|
-
handle,
|
|
918
|
-
activity,
|
|
919
|
-
label: "Update(Note)",
|
|
920
|
-
objectUrl: properties.url,
|
|
921
|
-
});
|
|
922
|
-
} catch (error) {
|
|
923
|
-
console.warn("[ActivityPub] broadcastPostUpdate failed:", error.message);
|
|
924
|
-
}
|
|
152
|
+
return broadcastPostUpdate(this, properties);
|
|
925
153
|
}
|
|
926
154
|
|
|
927
|
-
/**
|
|
928
|
-
* Build the full actor URL from config.
|
|
929
|
-
* @returns {string}
|
|
930
|
-
*/
|
|
931
155
|
_getActorUrl() {
|
|
932
|
-
|
|
933
|
-
return `${base}${this.options.mountPath}/users/${this.options.actor.handle}`;
|
|
156
|
+
return getActorUrl(this);
|
|
934
157
|
}
|
|
935
158
|
|
|
936
159
|
init(Indiekit) {
|