@rmdes/indiekit-endpoint-activitypub 3.13.9 → 3.13.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/index.js +10 -414
- package/lib/defaults.js +37 -0
- package/lib/inbox-handlers.js +4 -0
- package/lib/item-processing.js +3 -1
- 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/lib/emoji-utils.js +0 -38
package/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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
7
|
import { setupFederation, buildPersonActor } from "./lib/federation-setup.js";
|
|
6
8
|
import { createMastodonRouter } from "./lib/mastodon/router.js";
|
|
@@ -16,106 +18,10 @@ import {
|
|
|
16
18
|
import {
|
|
17
19
|
createFedifyMiddleware,
|
|
18
20
|
} from "./lib/federation-bridge.js";
|
|
19
|
-
import {
|
|
20
|
-
jf2ToActivityStreams,
|
|
21
|
-
jf2ToAS2Activity,
|
|
22
|
-
} from "./lib/jf2-to-as2.js";
|
|
21
|
+
import { jf2ToAS2Activity } from "./lib/jf2-to-as2.js";
|
|
23
22
|
import { createSyndicator } from "./lib/syndicator.js";
|
|
24
|
-
import {
|
|
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
|
-
import {
|
|
84
|
-
exploreController,
|
|
85
|
-
exploreApiController,
|
|
86
|
-
instanceSearchApiController,
|
|
87
|
-
instanceCheckApiController,
|
|
88
|
-
popularAccountsApiController,
|
|
89
|
-
} from "./lib/controllers/explore.js";
|
|
90
|
-
import {
|
|
91
|
-
followTagController,
|
|
92
|
-
unfollowTagController,
|
|
93
|
-
followTagGloballyController,
|
|
94
|
-
unfollowTagGloballyController,
|
|
95
|
-
} from "./lib/controllers/follow-tag.js";
|
|
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";
|
|
23
|
+
import { buildAdminRoutes } from "./lib/routes/admin-routes.js";
|
|
24
|
+
import { buildRoutesPublic, buildContentNegotiationRoutes } from "./lib/routes/public-routes.js";
|
|
119
25
|
import { startBatchRefollow } from "./lib/batch-refollow.js";
|
|
120
26
|
import { logActivity } from "./lib/activity-log.js";
|
|
121
27
|
import { batchBroadcast } from "./lib/batch-broadcast.js";
|
|
@@ -124,48 +30,12 @@ import { runSeparateMentionsMigration } from "./lib/migrations/separate-mentions
|
|
|
124
30
|
import { loadBlockedServersToRedis } from "./lib/storage/server-blocks.js";
|
|
125
31
|
import { scheduleKeyRefresh } from "./lib/key-refresh.js";
|
|
126
32
|
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
33
|
|
|
163
34
|
export default class ActivityPubEndpoint {
|
|
164
35
|
name = "ActivityPub endpoint";
|
|
165
36
|
|
|
166
37
|
constructor(options = {}) {
|
|
167
|
-
this.options =
|
|
168
|
-
this.options.actor = { ...defaults.actor, ...options.actor };
|
|
38
|
+
this.options = resolveOptions(options);
|
|
169
39
|
this.mountPath = this.options.mountPath;
|
|
170
40
|
|
|
171
41
|
this._publicationUrl = "";
|
|
@@ -179,48 +49,7 @@ export default class ActivityPubEndpoint {
|
|
|
179
49
|
}
|
|
180
50
|
|
|
181
51
|
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
|
-
];
|
|
52
|
+
return buildNavigationItems(this.options.mountPath);
|
|
224
53
|
}
|
|
225
54
|
|
|
226
55
|
/**
|
|
@@ -244,159 +73,14 @@ export default class ActivityPubEndpoint {
|
|
|
244
73
|
* Fedify handles actor, inbox, outbox, followers, following.
|
|
245
74
|
*/
|
|
246
75
|
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;
|
|
76
|
+
return buildRoutesPublic(this);
|
|
307
77
|
}
|
|
308
78
|
|
|
309
79
|
/**
|
|
310
80
|
* Authenticated admin routes — mounted at mountPath, behind IndieAuth.
|
|
311
81
|
*/
|
|
312
82
|
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;
|
|
83
|
+
return buildAdminRoutes(this);
|
|
400
84
|
}
|
|
401
85
|
|
|
402
86
|
/**
|
|
@@ -405,95 +89,7 @@ export default class ActivityPubEndpoint {
|
|
|
405
89
|
* at /nodeinfo/2.1 (delegated to Fedify).
|
|
406
90
|
*/
|
|
407
91
|
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;
|
|
92
|
+
return buildContentNegotiationRoutes(this);
|
|
497
93
|
}
|
|
498
94
|
|
|
499
95
|
/**
|
package/lib/defaults.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default plugin options for @rmdes/indiekit-endpoint-activitypub.
|
|
3
|
+
* Merged over user options in the endpoint constructor.
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULTS = {
|
|
6
|
+
mountPath: "/activitypub",
|
|
7
|
+
actor: {
|
|
8
|
+
handle: "rick",
|
|
9
|
+
name: "",
|
|
10
|
+
summary: "",
|
|
11
|
+
icon: "",
|
|
12
|
+
},
|
|
13
|
+
checked: true,
|
|
14
|
+
alsoKnownAs: "",
|
|
15
|
+
activityRetentionDays: 90,
|
|
16
|
+
storeRawActivities: false,
|
|
17
|
+
redisUrl: "",
|
|
18
|
+
parallelWorkers: 5,
|
|
19
|
+
actorType: "Person",
|
|
20
|
+
logLevel: "warning",
|
|
21
|
+
timelineRetention: 1000,
|
|
22
|
+
notificationRetentionDays: 30,
|
|
23
|
+
debugDashboard: false,
|
|
24
|
+
debugPassword: "",
|
|
25
|
+
defaultVisibility: "public", // "public" | "unlisted" | "followers"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Merge user options over defaults (deep-merges the nested `actor` object).
|
|
30
|
+
* @param {object} [options]
|
|
31
|
+
* @returns {object} resolved options
|
|
32
|
+
*/
|
|
33
|
+
export function resolveOptions(options = {}) {
|
|
34
|
+
const merged = { ...DEFAULTS, ...options };
|
|
35
|
+
merged.actor = { ...DEFAULTS.actor, ...options.actor };
|
|
36
|
+
return merged;
|
|
37
|
+
}
|
package/lib/inbox-handlers.js
CHANGED
|
@@ -43,6 +43,10 @@ import { getSettings } from "./settings.js";
|
|
|
43
43
|
/** @type {string} ActivityStreams Public Collection constant */
|
|
44
44
|
const PUBLIC = "https://www.w3.org/ns/activitystreams#Public";
|
|
45
45
|
|
|
46
|
+
// Pure addressing/visibility helpers are exported for unit testing (see
|
|
47
|
+
// tests/inbox-visibility.test.js). They are not part of the handler API.
|
|
48
|
+
export { isDirectMessage as _isDirectMessage, computeVisibility as _computeVisibility };
|
|
49
|
+
|
|
46
50
|
// ---------------------------------------------------------------------------
|
|
47
51
|
// Router
|
|
48
52
|
// ---------------------------------------------------------------------------
|
package/lib/item-processing.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { stripQuoteReferenceHtml } from "./og-unfurl.js";
|
|
10
|
-
|
|
10
|
+
// Use the hardened replaceCustomEmoji (validates http(s) URL schemes + escapes
|
|
11
|
+
// attributes) — the render pipeline processes attacker-controlled remote emoji.
|
|
12
|
+
import { replaceCustomEmoji } from "./timeline-store.js";
|
|
11
13
|
import { shortenDisplayUrls, collapseHashtagStuffing } from "./content-utils.js";
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -68,6 +68,15 @@ function parseScopes(value) {
|
|
|
68
68
|
.filter(Boolean);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// Pure helpers exported for unit testing (see tests/oauth-helpers.test.js).
|
|
72
|
+
// Not part of the router API.
|
|
73
|
+
export {
|
|
74
|
+
escapeHtml as _escapeHtml,
|
|
75
|
+
hashSecret as _hashSecret,
|
|
76
|
+
parseRedirectUris as _parseRedirectUris,
|
|
77
|
+
parseScopes as _parseScopes,
|
|
78
|
+
};
|
|
79
|
+
|
|
71
80
|
// ─── POST /api/v1/apps — Register client application ────────────────────────
|
|
72
81
|
|
|
73
82
|
router.post("/api/v1/apps", async (req, res, next) => {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin navigation items for the ActivityPub endpoint.
|
|
3
|
+
* Extracted from index.js so the nav structure is unit-testable.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build the plugin's admin navigation items.
|
|
8
|
+
* @param {string} mountPath - The plugin mount path (e.g. "/activitypub")
|
|
9
|
+
* @returns {Array<{href: string, text: string, requiresDatabase: boolean}>}
|
|
10
|
+
*/
|
|
11
|
+
export function buildNavigationItems(mountPath) {
|
|
12
|
+
return [
|
|
13
|
+
{ href: mountPath, text: "activitypub.title", requiresDatabase: true },
|
|
14
|
+
{ href: `${mountPath}/admin/reader`, text: "activitypub.reader.title", requiresDatabase: true },
|
|
15
|
+
{ href: `${mountPath}/admin/reader/notifications`, text: "activitypub.notifications.title", requiresDatabase: true },
|
|
16
|
+
{ href: `${mountPath}/admin/reader/messages`, text: "activitypub.messages.title", requiresDatabase: true },
|
|
17
|
+
{ href: `${mountPath}/admin/reader/moderation`, text: "activitypub.moderation.title", requiresDatabase: true },
|
|
18
|
+
{ href: `${mountPath}/admin/my-profile`, text: "activitypub.myProfile.title", requiresDatabase: true },
|
|
19
|
+
{ href: `${mountPath}/admin/federation`, text: "activitypub.federationMgmt.title", requiresDatabase: true },
|
|
20
|
+
{ href: `${mountPath}/admin/settings`, text: "activitypub.settings.title", requiresDatabase: true },
|
|
21
|
+
];
|
|
22
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authenticated admin UI routes for the ActivityPub endpoint.
|
|
3
|
+
* Extracted from index.js's `get routes()` getter (Phase 2 god-entry split).
|
|
4
|
+
* `self` is the ActivityPubEndpoint instance (passed through to controllers).
|
|
5
|
+
*/
|
|
6
|
+
import express from "express";
|
|
7
|
+
|
|
8
|
+
import { dashboardController } from "../controllers/dashboard.js";
|
|
9
|
+
import {
|
|
10
|
+
readerController,
|
|
11
|
+
notificationsController,
|
|
12
|
+
markAllNotificationsReadController,
|
|
13
|
+
clearAllNotificationsController,
|
|
14
|
+
deleteNotificationController,
|
|
15
|
+
composeController,
|
|
16
|
+
submitComposeController,
|
|
17
|
+
remoteProfileController,
|
|
18
|
+
followController,
|
|
19
|
+
unfollowController,
|
|
20
|
+
postDetailController,
|
|
21
|
+
} from "../controllers/reader.js";
|
|
22
|
+
import {
|
|
23
|
+
likeController,
|
|
24
|
+
unlikeController,
|
|
25
|
+
boostController,
|
|
26
|
+
unboostController,
|
|
27
|
+
} from "../controllers/interactions.js";
|
|
28
|
+
import {
|
|
29
|
+
muteController,
|
|
30
|
+
unmuteController,
|
|
31
|
+
blockController,
|
|
32
|
+
unblockController,
|
|
33
|
+
blockServerController,
|
|
34
|
+
unblockServerController,
|
|
35
|
+
moderationController,
|
|
36
|
+
filterModeController,
|
|
37
|
+
} from "../controllers/moderation.js";
|
|
38
|
+
import { followersController } from "../controllers/followers.js";
|
|
39
|
+
import {
|
|
40
|
+
approveFollowController,
|
|
41
|
+
rejectFollowController,
|
|
42
|
+
} from "../controllers/follow-requests.js";
|
|
43
|
+
import { followingController } from "../controllers/following.js";
|
|
44
|
+
import { activitiesController } from "../controllers/activities.js";
|
|
45
|
+
import {
|
|
46
|
+
migrateGetController,
|
|
47
|
+
migratePostController,
|
|
48
|
+
migrateImportController,
|
|
49
|
+
} from "../controllers/migrate.js";
|
|
50
|
+
import {
|
|
51
|
+
profileGetController,
|
|
52
|
+
profilePostController,
|
|
53
|
+
} from "../controllers/profile.js";
|
|
54
|
+
import {
|
|
55
|
+
featuredGetController,
|
|
56
|
+
featuredPinController,
|
|
57
|
+
featuredUnpinController,
|
|
58
|
+
} from "../controllers/featured.js";
|
|
59
|
+
import {
|
|
60
|
+
featuredTagsGetController,
|
|
61
|
+
featuredTagsAddController,
|
|
62
|
+
featuredTagsRemoveController,
|
|
63
|
+
} from "../controllers/featured-tags.js";
|
|
64
|
+
import { resolveController } from "../controllers/resolve.js";
|
|
65
|
+
import { tagTimelineController } from "../controllers/tag-timeline.js";
|
|
66
|
+
import { apiTimelineController, countNewController, markReadController } from "../controllers/api-timeline.js";
|
|
67
|
+
import {
|
|
68
|
+
exploreController,
|
|
69
|
+
exploreApiController,
|
|
70
|
+
instanceSearchApiController,
|
|
71
|
+
instanceCheckApiController,
|
|
72
|
+
popularAccountsApiController,
|
|
73
|
+
} from "../controllers/explore.js";
|
|
74
|
+
import {
|
|
75
|
+
followTagController,
|
|
76
|
+
unfollowTagController,
|
|
77
|
+
followTagGloballyController,
|
|
78
|
+
unfollowTagGloballyController,
|
|
79
|
+
} from "../controllers/follow-tag.js";
|
|
80
|
+
import {
|
|
81
|
+
listTabsController,
|
|
82
|
+
addTabController,
|
|
83
|
+
removeTabController,
|
|
84
|
+
reorderTabsController,
|
|
85
|
+
} from "../controllers/tabs.js";
|
|
86
|
+
import { hashtagExploreApiController } from "../controllers/hashtag-explore.js";
|
|
87
|
+
import {
|
|
88
|
+
messagesController,
|
|
89
|
+
messageComposeController,
|
|
90
|
+
submitMessageController,
|
|
91
|
+
markAllMessagesReadController,
|
|
92
|
+
clearAllMessagesController,
|
|
93
|
+
deleteMessageController,
|
|
94
|
+
} from "../controllers/messages.js";
|
|
95
|
+
import { myProfileController } from "../controllers/my-profile.js";
|
|
96
|
+
import {
|
|
97
|
+
refollowPauseController,
|
|
98
|
+
refollowResumeController,
|
|
99
|
+
refollowStatusController,
|
|
100
|
+
} from "../controllers/refollow.js";
|
|
101
|
+
import { deleteFederationController } from "../controllers/federation-delete.js";
|
|
102
|
+
import {
|
|
103
|
+
federationMgmtController,
|
|
104
|
+
rebroadcastController,
|
|
105
|
+
viewApJsonController,
|
|
106
|
+
broadcastActorUpdateController,
|
|
107
|
+
lookupObjectController,
|
|
108
|
+
} from "../controllers/federation-mgmt.js";
|
|
109
|
+
import {
|
|
110
|
+
settingsGetController,
|
|
111
|
+
settingsPostController,
|
|
112
|
+
} from "../controllers/settings.js";
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build the authenticated admin router.
|
|
116
|
+
* @param {object} self - the ActivityPubEndpoint instance
|
|
117
|
+
* @returns {import("express").Router}
|
|
118
|
+
*/
|
|
119
|
+
export function buildAdminRoutes(self) {
|
|
120
|
+
const router = express.Router(); // eslint-disable-line new-cap
|
|
121
|
+
const mp = self.options.mountPath;
|
|
122
|
+
|
|
123
|
+
router.get("/", dashboardController(mp));
|
|
124
|
+
router.get("/admin/reader", readerController(mp));
|
|
125
|
+
router.get("/admin/reader/tag", tagTimelineController(mp));
|
|
126
|
+
router.get("/admin/reader/api/timeline", apiTimelineController(mp));
|
|
127
|
+
router.get("/admin/reader/api/timeline/count-new", countNewController());
|
|
128
|
+
router.post("/admin/reader/api/timeline/mark-read", markReadController());
|
|
129
|
+
router.get("/admin/reader/explore", exploreController(mp));
|
|
130
|
+
router.get("/admin/reader/api/explore", exploreApiController(mp));
|
|
131
|
+
router.get("/admin/reader/api/explore/hashtag", hashtagExploreApiController(mp));
|
|
132
|
+
router.get("/admin/reader/api/instances", instanceSearchApiController(mp));
|
|
133
|
+
router.get("/admin/reader/api/instance-check", instanceCheckApiController(mp));
|
|
134
|
+
router.get("/admin/reader/api/popular-accounts", popularAccountsApiController(mp));
|
|
135
|
+
router.get("/admin/reader/api/tabs", listTabsController(mp));
|
|
136
|
+
router.post("/admin/reader/api/tabs", addTabController(mp));
|
|
137
|
+
router.post("/admin/reader/api/tabs/remove", removeTabController(mp));
|
|
138
|
+
router.patch("/admin/reader/api/tabs/reorder", reorderTabsController(mp));
|
|
139
|
+
router.post("/admin/reader/follow-tag", followTagController(mp));
|
|
140
|
+
router.post("/admin/reader/unfollow-tag", unfollowTagController(mp));
|
|
141
|
+
router.post("/admin/reader/follow-tag-global", followTagGloballyController(mp, self));
|
|
142
|
+
router.post("/admin/reader/unfollow-tag-global", unfollowTagGloballyController(mp, self));
|
|
143
|
+
router.get("/admin/reader/notifications", notificationsController(mp));
|
|
144
|
+
router.post("/admin/reader/notifications/mark-read", markAllNotificationsReadController(mp));
|
|
145
|
+
router.post("/admin/reader/notifications/clear", clearAllNotificationsController(mp));
|
|
146
|
+
router.post("/admin/reader/notifications/delete", deleteNotificationController(mp));
|
|
147
|
+
router.get("/admin/reader/messages", messagesController(mp));
|
|
148
|
+
router.get("/admin/reader/messages/compose", messageComposeController(mp, self));
|
|
149
|
+
router.post("/admin/reader/messages/compose", submitMessageController(mp, self));
|
|
150
|
+
router.post("/admin/reader/messages/mark-read", markAllMessagesReadController(mp));
|
|
151
|
+
router.post("/admin/reader/messages/clear", clearAllMessagesController(mp));
|
|
152
|
+
router.post("/admin/reader/messages/delete", deleteMessageController(mp));
|
|
153
|
+
router.get("/admin/reader/compose", composeController(mp, self));
|
|
154
|
+
router.post("/admin/reader/compose", submitComposeController(mp, self));
|
|
155
|
+
router.post("/admin/reader/like", likeController(mp, self));
|
|
156
|
+
router.post("/admin/reader/unlike", unlikeController(mp, self));
|
|
157
|
+
router.post("/admin/reader/boost", boostController(mp, self));
|
|
158
|
+
router.post("/admin/reader/unboost", unboostController(mp, self));
|
|
159
|
+
router.get("/admin/reader/resolve", resolveController(mp, self));
|
|
160
|
+
router.get("/admin/reader/profile", remoteProfileController(mp, self));
|
|
161
|
+
router.get("/admin/reader/post", postDetailController(mp, self));
|
|
162
|
+
router.post("/admin/reader/follow", followController(mp, self));
|
|
163
|
+
router.post("/admin/reader/unfollow", unfollowController(mp, self));
|
|
164
|
+
router.get("/admin/reader/moderation", moderationController(mp));
|
|
165
|
+
router.post("/admin/reader/moderation/filter-mode", filterModeController(mp));
|
|
166
|
+
router.post("/admin/reader/mute", muteController(mp, self));
|
|
167
|
+
router.post("/admin/reader/unmute", unmuteController(mp, self));
|
|
168
|
+
router.post("/admin/reader/block", blockController(mp, self));
|
|
169
|
+
router.post("/admin/reader/unblock", unblockController(mp, self));
|
|
170
|
+
router.post("/admin/reader/block-server", blockServerController(mp));
|
|
171
|
+
router.post("/admin/reader/unblock-server", unblockServerController(mp));
|
|
172
|
+
router.get("/admin/followers", followersController(mp));
|
|
173
|
+
router.post("/admin/followers/approve", approveFollowController(mp, self));
|
|
174
|
+
router.post("/admin/followers/reject", rejectFollowController(mp, self));
|
|
175
|
+
router.get("/admin/following", followingController(mp));
|
|
176
|
+
router.get("/admin/activities", activitiesController(mp));
|
|
177
|
+
router.get("/admin/featured", featuredGetController(mp));
|
|
178
|
+
router.post("/admin/featured/pin", featuredPinController(mp, self));
|
|
179
|
+
router.post("/admin/featured/unpin", featuredUnpinController(mp, self));
|
|
180
|
+
router.get("/admin/tags", featuredTagsGetController(mp));
|
|
181
|
+
router.post("/admin/tags/add", featuredTagsAddController(mp, self));
|
|
182
|
+
router.post("/admin/tags/remove", featuredTagsRemoveController(mp, self));
|
|
183
|
+
router.get("/admin/profile", profileGetController(mp));
|
|
184
|
+
router.post("/admin/profile", profilePostController(mp, self));
|
|
185
|
+
router.get("/admin/my-profile", myProfileController(self));
|
|
186
|
+
router.get("/admin/migrate", migrateGetController(mp, self.options));
|
|
187
|
+
router.post("/admin/migrate", migratePostController(mp, self.options));
|
|
188
|
+
router.post("/admin/migrate/import", migrateImportController(mp, self.options));
|
|
189
|
+
router.post("/admin/refollow/pause", refollowPauseController(mp, self));
|
|
190
|
+
router.post("/admin/refollow/resume", refollowResumeController(mp, self));
|
|
191
|
+
router.get("/admin/refollow/status", refollowStatusController(mp));
|
|
192
|
+
router.post("/admin/federation/delete", deleteFederationController(mp, self));
|
|
193
|
+
router.get("/admin/federation", federationMgmtController(mp, self));
|
|
194
|
+
router.post("/admin/federation/rebroadcast", rebroadcastController(mp, self));
|
|
195
|
+
router.get("/admin/federation/ap-json", viewApJsonController(mp, self));
|
|
196
|
+
router.post("/admin/federation/broadcast-actor", broadcastActorUpdateController(mp, self));
|
|
197
|
+
router.get("/admin/federation/lookup", lookupObjectController(mp, self));
|
|
198
|
+
|
|
199
|
+
// Settings
|
|
200
|
+
router.get("/admin/settings", settingsGetController(mp));
|
|
201
|
+
router.post("/admin/settings", settingsPostController(mp));
|
|
202
|
+
|
|
203
|
+
return router;
|
|
204
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public (federation-facing) route getters, extracted from index.js's
|
|
3
|
+
* `get routesPublic()` + `get contentNegotiationRoutes()` (Phase 2 god-entry split).
|
|
4
|
+
* `self` is the ActivityPubEndpoint instance.
|
|
5
|
+
*/
|
|
6
|
+
import express from "express";
|
|
7
|
+
|
|
8
|
+
import { authorizeInteractionController } from "../controllers/authorize-interaction.js";
|
|
9
|
+
import { publicProfileController } from "../controllers/public-profile.js";
|
|
10
|
+
import { jf2ToActivityStreams } from "../jf2-to-as2.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Public routes — Fedify bridge for actor/inbox/collections, plus HTML
|
|
14
|
+
* fallbacks. Mounted at mountPath, in front of the authenticated admin routes.
|
|
15
|
+
* @param {object} self - the ActivityPubEndpoint instance
|
|
16
|
+
* @returns {import("express").Router}
|
|
17
|
+
*/
|
|
18
|
+
export function buildRoutesPublic(self) {
|
|
19
|
+
const router = express.Router(); // eslint-disable-line new-cap
|
|
20
|
+
|
|
21
|
+
router.use((req, res, next) => {
|
|
22
|
+
if (!self._fedifyMiddleware) return next();
|
|
23
|
+
// Skip Fedify for admin UI routes — they're handled by the
|
|
24
|
+
// authenticated `routes` getter, not the federation layer.
|
|
25
|
+
if (req.path.startsWith("/admin")) return next();
|
|
26
|
+
|
|
27
|
+
// Fedify's acceptsJsonLd() treats Accept: */* as NOT accepting JSON-LD
|
|
28
|
+
// (it only returns true for explicit application/activity+json etc.).
|
|
29
|
+
// Remote servers fetching actor URLs for HTTP Signature verification
|
|
30
|
+
// (e.g. tags.pub) often omit Accept or use */* — they get HTML back
|
|
31
|
+
// instead of the actor JSON, causing "public key not found" errors.
|
|
32
|
+
// Fix: for GET requests to actor paths, upgrade ambiguous Accept headers
|
|
33
|
+
// to application/activity+json so Fedify serves JSON-LD. Explicit
|
|
34
|
+
// text/html requests (browsers) are unaffected.
|
|
35
|
+
if (req.method === "GET" && /^\/users\/[^/]+\/?$/.test(req.path)) {
|
|
36
|
+
const accept = req.get("accept") || "";
|
|
37
|
+
if (!accept.includes("text/html") && !accept.includes("application/xhtml+xml")) {
|
|
38
|
+
req.headers["accept"] = "application/activity+json";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return self._fedifyMiddleware(req, res, next);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Authorize interaction — remote follow / subscribe endpoint.
|
|
46
|
+
// Remote servers redirect users here via the WebFinger subscribe template.
|
|
47
|
+
router.get("/authorize_interaction", authorizeInteractionController(self));
|
|
48
|
+
|
|
49
|
+
// HTML fallback for actor URL — serve a public profile page.
|
|
50
|
+
// Fedify only serves JSON-LD; browsers get 406 and fall through here.
|
|
51
|
+
router.get("/users/:identifier", publicProfileController(self));
|
|
52
|
+
|
|
53
|
+
// Catch-all for federation paths that Fedify didn't handle (e.g. GET
|
|
54
|
+
// on inbox). Without this, they fall through to Indiekit's auth
|
|
55
|
+
// middleware and redirect to the login page.
|
|
56
|
+
router.all("/users/:identifier/inbox", (req, res) => {
|
|
57
|
+
res
|
|
58
|
+
.status(405)
|
|
59
|
+
.set("Allow", "POST")
|
|
60
|
+
.type("application/activity+json")
|
|
61
|
+
.json({
|
|
62
|
+
error: "Method Not Allowed",
|
|
63
|
+
message: "The inbox only accepts POST requests",
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
router.all("/inbox", (req, res) => {
|
|
67
|
+
res
|
|
68
|
+
.status(405)
|
|
69
|
+
.set("Allow", "POST")
|
|
70
|
+
.type("application/activity+json")
|
|
71
|
+
.json({
|
|
72
|
+
error: "Method Not Allowed",
|
|
73
|
+
message: "The shared inbox only accepts POST requests",
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return router;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Content negotiation — serves AS2 JSON for ActivityPub clients requesting
|
|
82
|
+
* individual post URLs; delegates /nodeinfo/2.1 to Fedify.
|
|
83
|
+
* @param {object} self - the ActivityPubEndpoint instance
|
|
84
|
+
* @returns {import("express").Router}
|
|
85
|
+
*/
|
|
86
|
+
export function buildContentNegotiationRoutes(self) {
|
|
87
|
+
const router = express.Router(); // eslint-disable-line new-cap
|
|
88
|
+
|
|
89
|
+
// Let Fedify handle NodeInfo data (/nodeinfo/2.1)
|
|
90
|
+
// Only pass GET/HEAD requests — POST/PUT/DELETE must not go through
|
|
91
|
+
// Fedify here, because fromExpressRequest() consumes the body stream,
|
|
92
|
+
// breaking Express body-parsed routes downstream (e.g. admin forms).
|
|
93
|
+
router.use((req, res, next) => {
|
|
94
|
+
if (!self._fedifyMiddleware) return next();
|
|
95
|
+
if (req.method !== "GET" && req.method !== "HEAD") return next();
|
|
96
|
+
// Only delegate to Fedify for NodeInfo data endpoint (/nodeinfo/2.1).
|
|
97
|
+
// All other paths in this root-mounted router are handled by the
|
|
98
|
+
// content negotiation catch-all below. Passing arbitrary paths like
|
|
99
|
+
// /notes/... to Fedify causes harmless but noisy 404 warnings.
|
|
100
|
+
if (!req.path.startsWith("/nodeinfo/")) return next();
|
|
101
|
+
return self._fedifyMiddleware(req, res, next);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Content negotiation for AP clients on regular URLs
|
|
105
|
+
router.get("{*path}", async (req, res, next) => {
|
|
106
|
+
const accept = req.headers.accept || "";
|
|
107
|
+
const isActivityPub =
|
|
108
|
+
accept.includes("application/activity+json") ||
|
|
109
|
+
accept.includes("application/ld+json");
|
|
110
|
+
|
|
111
|
+
if (!isActivityPub) {
|
|
112
|
+
return next();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Root URL — redirect to Fedify actor
|
|
117
|
+
if (req.path === "/") {
|
|
118
|
+
const actorPath = `${self.options.mountPath}/users/${self.options.actor.handle}`;
|
|
119
|
+
return res.redirect(actorPath);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Post URLs — look up in database and convert to AS2
|
|
123
|
+
const { application } = req.app.locals;
|
|
124
|
+
const postsCollection = application?.collections?.get("posts");
|
|
125
|
+
if (!postsCollection) {
|
|
126
|
+
return next();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const requestUrl = `${self._publicationUrl}${req.path.slice(1)}`;
|
|
130
|
+
const post = await postsCollection.findOne({
|
|
131
|
+
"properties.url": requestUrl,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!post || post.properties?.deleted) {
|
|
135
|
+
// FEP-4f05: Serve Tombstone for deleted posts
|
|
136
|
+
const { getTombstone } = await import("../storage/tombstones.js");
|
|
137
|
+
const tombstone = await getTombstone(self._collections, requestUrl);
|
|
138
|
+
if (tombstone) {
|
|
139
|
+
res.status(410).set("Content-Type", "application/activity+json").json({
|
|
140
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
141
|
+
type: "Tombstone",
|
|
142
|
+
id: requestUrl,
|
|
143
|
+
formerType: tombstone.formerType,
|
|
144
|
+
published: tombstone.published || undefined,
|
|
145
|
+
deleted: tombstone.deleted,
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
return next();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const actorUrl = self._getActorUrl();
|
|
153
|
+
const activity = jf2ToActivityStreams(
|
|
154
|
+
post.properties,
|
|
155
|
+
actorUrl,
|
|
156
|
+
self._publicationUrl,
|
|
157
|
+
{ visibility: self.options.defaultVisibility },
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const object = activity.object || activity;
|
|
161
|
+
res.set("Content-Type", "application/activity+json");
|
|
162
|
+
return res.json({
|
|
163
|
+
"@context": [
|
|
164
|
+
"https://www.w3.org/ns/activitystreams",
|
|
165
|
+
"https://w3id.org/security/v1",
|
|
166
|
+
],
|
|
167
|
+
...object,
|
|
168
|
+
});
|
|
169
|
+
} catch {
|
|
170
|
+
return next();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return router;
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "3.13.
|
|
3
|
+
"version": "3.13.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",
|
package/lib/emoji-utils.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom emoji replacement for fediverse content.
|
|
3
|
-
*
|
|
4
|
-
* Replaces :shortcode: patterns with <img> tags for custom emoji.
|
|
5
|
-
* Must be called AFTER sanitizeContent() — the inserted <img> tags
|
|
6
|
-
* would be stripped if run through the sanitizer.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Escape special regex characters in a string.
|
|
11
|
-
* @param {string} str
|
|
12
|
-
* @returns {string}
|
|
13
|
-
*/
|
|
14
|
-
function escapeRegex(str) {
|
|
15
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Replace :shortcode: patterns in HTML with custom emoji <img> tags.
|
|
20
|
-
*
|
|
21
|
-
* @param {string} html - HTML string (already sanitized)
|
|
22
|
-
* @param {Array<{shortcode: string, url: string}>} emojis - Custom emoji list
|
|
23
|
-
* @returns {string} HTML with emoji shortcodes replaced by img tags
|
|
24
|
-
*/
|
|
25
|
-
export function replaceCustomEmoji(html, emojis) {
|
|
26
|
-
if (!html || !emojis?.length) return html;
|
|
27
|
-
|
|
28
|
-
for (const emoji of emojis) {
|
|
29
|
-
if (!emoji.shortcode || !emoji.url) continue;
|
|
30
|
-
const pattern = new RegExp(`:${escapeRegex(emoji.shortcode)}:`, "g");
|
|
31
|
-
html = html.replace(
|
|
32
|
-
pattern,
|
|
33
|
-
`<img src="${emoji.url}" alt=":${emoji.shortcode}:" title=":${emoji.shortcode}:" class="ap-custom-emoji" loading="lazy">`,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return html;
|
|
38
|
-
}
|