@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/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
|
+
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation send-path actions, extracted from the index.js god-entry
|
|
3
|
+
* (Phase 2). Each takes `self` (the ActivityPubEndpoint instance); the class
|
|
4
|
+
* keeps thin delegating methods so the public interface + the init() facade
|
|
5
|
+
* are preserved. Internal cross-calls go directly to the module functions.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
needsDirectFollow,
|
|
9
|
+
sendDirectFollow,
|
|
10
|
+
sendDirectUnfollow,
|
|
11
|
+
} from "./direct-follow.js";
|
|
12
|
+
import { lookupWithSecurity } from "./lookup-helpers.js";
|
|
13
|
+
import { logActivity } from "./activity-log.js";
|
|
14
|
+
import { batchBroadcast } from "./batch-broadcast.js";
|
|
15
|
+
import { buildPersonActor } from "./federation-setup.js";
|
|
16
|
+
import { jf2ToAS2Activity } from "./jf2-to-as2.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load the RSA private key from ap_keys for direct HTTP Signature signing.
|
|
20
|
+
* @returns {Promise<CryptoKey|null>}
|
|
21
|
+
*/
|
|
22
|
+
export async function loadRsaPrivateKey(self) {
|
|
23
|
+
try {
|
|
24
|
+
const keyDoc = await self._collections.ap_keys.findOne({
|
|
25
|
+
privateKeyPem: { $exists: true },
|
|
26
|
+
});
|
|
27
|
+
if (!keyDoc?.privateKeyPem) return null;
|
|
28
|
+
const pemBody = keyDoc.privateKeyPem
|
|
29
|
+
.replace(/-----[^-]+-----/g, "")
|
|
30
|
+
.replace(/\s/g, "");
|
|
31
|
+
return await crypto.subtle.importKey(
|
|
32
|
+
"pkcs8",
|
|
33
|
+
Buffer.from(pemBody, "base64"),
|
|
34
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
35
|
+
true,
|
|
36
|
+
["sign"],
|
|
37
|
+
);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error("[ActivityPub] Failed to load RSA key:", error.message);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Send a Follow activity to a remote actor and store in ap_following. */
|
|
45
|
+
export async function followActor(self, actorUrl, actorInfo = {}) {
|
|
46
|
+
if (!self._federation) {
|
|
47
|
+
return { ok: false, error: "Federation not initialized" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const { Follow } = await import("@fedify/fedify/vocab");
|
|
52
|
+
const handle = self.options.actor.handle;
|
|
53
|
+
const ctx = self._federation.createContext(
|
|
54
|
+
new URL(self._publicationUrl),
|
|
55
|
+
{ handle, publicationUrl: self._publicationUrl },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Resolve the remote actor to get their inbox
|
|
59
|
+
// lookupWithSecurity handles signed→unsigned fallback automatically
|
|
60
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
61
|
+
identifier: handle,
|
|
62
|
+
});
|
|
63
|
+
const remoteActor = await lookupWithSecurity(ctx, actorUrl, {
|
|
64
|
+
documentLoader,
|
|
65
|
+
});
|
|
66
|
+
if (!remoteActor) {
|
|
67
|
+
return { ok: false, error: "Could not resolve remote actor" };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Send Follow activity
|
|
71
|
+
if (needsDirectFollow(actorUrl)) {
|
|
72
|
+
// tags.pub rejects Fedify's LD Signature context (identity/v1).
|
|
73
|
+
// Send a minimal signed Follow directly, bypassing the outbox pipeline.
|
|
74
|
+
// See: https://github.com/social-web-foundation/tags.pub/issues/10
|
|
75
|
+
const rsaKey = await loadRsaPrivateKey(self);
|
|
76
|
+
if (!rsaKey) {
|
|
77
|
+
return { ok: false, error: "No RSA key available for direct follow" };
|
|
78
|
+
}
|
|
79
|
+
const result = await sendDirectFollow({
|
|
80
|
+
actorUri: ctx.getActorUri(handle).href,
|
|
81
|
+
targetActorUrl: actorUrl,
|
|
82
|
+
inboxUrl: remoteActor.inboxId?.href,
|
|
83
|
+
keyId: `${ctx.getActorUri(handle).href}#main-key`,
|
|
84
|
+
privateKey: rsaKey,
|
|
85
|
+
});
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
return { ok: false, error: result.error };
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
const follow = new Follow({
|
|
91
|
+
actor: ctx.getActorUri(handle),
|
|
92
|
+
object: new URL(actorUrl),
|
|
93
|
+
});
|
|
94
|
+
await ctx.sendActivity({ identifier: handle }, remoteActor, follow, {
|
|
95
|
+
orderingKey: actorUrl,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Store in ap_following
|
|
100
|
+
const name =
|
|
101
|
+
actorInfo.name ||
|
|
102
|
+
remoteActor.name?.toString() ||
|
|
103
|
+
remoteActor.preferredUsername?.toString() ||
|
|
104
|
+
actorUrl;
|
|
105
|
+
const actorHandle =
|
|
106
|
+
actorInfo.handle ||
|
|
107
|
+
remoteActor.preferredUsername?.toString() ||
|
|
108
|
+
"";
|
|
109
|
+
const avatar =
|
|
110
|
+
actorInfo.photo ||
|
|
111
|
+
(remoteActor.icon
|
|
112
|
+
? (await remoteActor.icon)?.url?.href || ""
|
|
113
|
+
: "");
|
|
114
|
+
const inbox = remoteActor.inboxId?.href || "";
|
|
115
|
+
const sharedInbox = remoteActor.endpoints?.sharedInbox?.href || "";
|
|
116
|
+
|
|
117
|
+
await self._collections.ap_following.updateOne(
|
|
118
|
+
{ actorUrl },
|
|
119
|
+
{
|
|
120
|
+
$set: {
|
|
121
|
+
actorUrl,
|
|
122
|
+
handle: actorHandle,
|
|
123
|
+
name,
|
|
124
|
+
avatar,
|
|
125
|
+
inbox,
|
|
126
|
+
sharedInbox,
|
|
127
|
+
followedAt: new Date().toISOString(),
|
|
128
|
+
source: "reader",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{ upsert: true },
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
console.info(`[ActivityPub] Sent Follow to ${actorUrl}`);
|
|
135
|
+
|
|
136
|
+
await logActivity(self._collections.ap_activities, {
|
|
137
|
+
direction: "outbound",
|
|
138
|
+
type: "Follow",
|
|
139
|
+
actorUrl: self._publicationUrl,
|
|
140
|
+
objectUrl: actorUrl,
|
|
141
|
+
actorName: name,
|
|
142
|
+
summary: `Sent Follow to ${name} (${actorUrl})`,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return { ok: true };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(`[ActivityPub] Follow failed for ${actorUrl}:`, error.message);
|
|
148
|
+
|
|
149
|
+
await logActivity(self._collections.ap_activities, {
|
|
150
|
+
direction: "outbound",
|
|
151
|
+
type: "Follow",
|
|
152
|
+
actorUrl: self._publicationUrl,
|
|
153
|
+
objectUrl: actorUrl,
|
|
154
|
+
summary: `Follow failed for ${actorUrl}: ${error.message}`,
|
|
155
|
+
}).catch(() => {});
|
|
156
|
+
|
|
157
|
+
return { ok: false, error: error.message };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Send an Undo(Follow) activity and remove from ap_following. */
|
|
162
|
+
export async function unfollowActor(self, actorUrl) {
|
|
163
|
+
if (!self._federation) {
|
|
164
|
+
return { ok: false, error: "Federation not initialized" };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const { Follow, Undo } = await import("@fedify/fedify/vocab");
|
|
169
|
+
const handle = self.options.actor.handle;
|
|
170
|
+
const ctx = self._federation.createContext(
|
|
171
|
+
new URL(self._publicationUrl),
|
|
172
|
+
{ handle, publicationUrl: self._publicationUrl },
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Use authenticated document loader for servers requiring Authorized Fetch
|
|
176
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
177
|
+
identifier: handle,
|
|
178
|
+
});
|
|
179
|
+
const remoteActor = await lookupWithSecurity(ctx, actorUrl, {
|
|
180
|
+
documentLoader,
|
|
181
|
+
});
|
|
182
|
+
if (!remoteActor) {
|
|
183
|
+
// Even if we can't resolve, remove locally
|
|
184
|
+
await self._collections.ap_following.deleteOne({ actorUrl });
|
|
185
|
+
|
|
186
|
+
await logActivity(self._collections.ap_activities, {
|
|
187
|
+
direction: "outbound",
|
|
188
|
+
type: "Undo(Follow)",
|
|
189
|
+
actorUrl: self._publicationUrl,
|
|
190
|
+
objectUrl: actorUrl,
|
|
191
|
+
summary: `Removed ${actorUrl} locally (could not resolve remote actor)`,
|
|
192
|
+
}).catch(() => {});
|
|
193
|
+
|
|
194
|
+
return { ok: true };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (needsDirectFollow(actorUrl)) {
|
|
198
|
+
// tags.pub rejects Fedify's LD Signature context (identity/v1).
|
|
199
|
+
// See: https://github.com/social-web-foundation/tags.pub/issues/10
|
|
200
|
+
const rsaKey = await loadRsaPrivateKey(self);
|
|
201
|
+
if (rsaKey) {
|
|
202
|
+
const result = await sendDirectUnfollow({
|
|
203
|
+
actorUri: ctx.getActorUri(handle).href,
|
|
204
|
+
targetActorUrl: actorUrl,
|
|
205
|
+
inboxUrl: remoteActor.inboxId?.href,
|
|
206
|
+
keyId: `${ctx.getActorUri(handle).href}#main-key`,
|
|
207
|
+
privateKey: rsaKey,
|
|
208
|
+
});
|
|
209
|
+
if (!result.ok) {
|
|
210
|
+
console.warn(`[ActivityPub] Direct unfollow failed for ${actorUrl}: ${result.error}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
const follow = new Follow({
|
|
215
|
+
actor: ctx.getActorUri(handle),
|
|
216
|
+
object: new URL(actorUrl),
|
|
217
|
+
});
|
|
218
|
+
const undo = new Undo({
|
|
219
|
+
actor: ctx.getActorUri(handle),
|
|
220
|
+
object: follow,
|
|
221
|
+
});
|
|
222
|
+
await ctx.sendActivity({ identifier: handle }, remoteActor, undo, {
|
|
223
|
+
orderingKey: actorUrl,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
await self._collections.ap_following.deleteOne({ actorUrl });
|
|
227
|
+
|
|
228
|
+
console.info(`[ActivityPub] Sent Undo(Follow) to ${actorUrl}`);
|
|
229
|
+
|
|
230
|
+
await logActivity(self._collections.ap_activities, {
|
|
231
|
+
direction: "outbound",
|
|
232
|
+
type: "Undo(Follow)",
|
|
233
|
+
actorUrl: self._publicationUrl,
|
|
234
|
+
objectUrl: actorUrl,
|
|
235
|
+
summary: `Sent Undo(Follow) to ${actorUrl}`,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return { ok: true };
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(`[ActivityPub] Unfollow failed for ${actorUrl}:`, error.message);
|
|
241
|
+
|
|
242
|
+
await logActivity(self._collections.ap_activities, {
|
|
243
|
+
direction: "outbound",
|
|
244
|
+
type: "Undo(Follow)",
|
|
245
|
+
actorUrl: self._publicationUrl,
|
|
246
|
+
objectUrl: actorUrl,
|
|
247
|
+
summary: `Unfollow failed for ${actorUrl}: ${error.message}`,
|
|
248
|
+
}).catch(() => {});
|
|
249
|
+
|
|
250
|
+
// Remove locally even if remote delivery fails
|
|
251
|
+
await self._collections.ap_following.deleteOne({ actorUrl }).catch(() => {});
|
|
252
|
+
return { ok: false, error: error.message };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Send an Update(Person) to all followers so they re-fetch the actor. */
|
|
257
|
+
export async function broadcastActorUpdate(self) {
|
|
258
|
+
if (!self._federation) return;
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const { Update } = await import("@fedify/fedify/vocab");
|
|
262
|
+
const handle = self.options.actor.handle;
|
|
263
|
+
const ctx = self._federation.createContext(
|
|
264
|
+
new URL(self._publicationUrl),
|
|
265
|
+
{ handle, publicationUrl: self._publicationUrl },
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const actor = await buildPersonActor(
|
|
269
|
+
ctx,
|
|
270
|
+
handle,
|
|
271
|
+
self._collections,
|
|
272
|
+
self.options.actorType,
|
|
273
|
+
);
|
|
274
|
+
if (!actor) {
|
|
275
|
+
console.warn("[ActivityPub] broadcastActorUpdate: could not build actor");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const update = new Update({
|
|
280
|
+
actor: ctx.getActorUri(handle),
|
|
281
|
+
object: actor,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await batchBroadcast({
|
|
285
|
+
federation: self._federation,
|
|
286
|
+
collections: self._collections,
|
|
287
|
+
publicationUrl: self._publicationUrl,
|
|
288
|
+
handle,
|
|
289
|
+
activity: update,
|
|
290
|
+
label: "Update(Person)",
|
|
291
|
+
objectUrl: getActorUrl(self),
|
|
292
|
+
});
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error("[ActivityPub] broadcastActorUpdate failed:", error.message);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Send a Delete activity to all followers for a removed post. */
|
|
299
|
+
export async function broadcastDelete(self, postUrl) {
|
|
300
|
+
if (!self._federation) return;
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const { Delete } = await import("@fedify/fedify/vocab");
|
|
304
|
+
const handle = self.options.actor.handle;
|
|
305
|
+
const ctx = self._federation.createContext(
|
|
306
|
+
new URL(self._publicationUrl),
|
|
307
|
+
{ handle, publicationUrl: self._publicationUrl },
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const del = new Delete({
|
|
311
|
+
actor: ctx.getActorUri(handle),
|
|
312
|
+
object: new URL(postUrl),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await batchBroadcast({
|
|
316
|
+
federation: self._federation,
|
|
317
|
+
collections: self._collections,
|
|
318
|
+
publicationUrl: self._publicationUrl,
|
|
319
|
+
handle,
|
|
320
|
+
activity: del,
|
|
321
|
+
label: "Delete",
|
|
322
|
+
objectUrl: postUrl,
|
|
323
|
+
});
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.warn("[ActivityPub] broadcastDelete failed:", error.message);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Micropub delete hook: record a tombstone (FEP-4f05) + broadcast Delete. */
|
|
330
|
+
export async function deletePost(self, url) {
|
|
331
|
+
// Record tombstone for FEP-4f05
|
|
332
|
+
try {
|
|
333
|
+
const { addTombstone } = await import("./storage/tombstones.js");
|
|
334
|
+
const postsCol = self._collections.posts;
|
|
335
|
+
const post = postsCol ? await postsCol.findOne({ "properties.url": url }) : null;
|
|
336
|
+
await addTombstone(self._collections, {
|
|
337
|
+
url,
|
|
338
|
+
formerType: post?.properties?.["post-type"] === "article" ? "Article" : "Note",
|
|
339
|
+
published: post?.properties?.published || null,
|
|
340
|
+
deleted: new Date().toISOString(),
|
|
341
|
+
});
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.warn(`[ActivityPub] Tombstone creation failed for ${url}: ${error.message}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
await broadcastDelete(self, url).catch((err) =>
|
|
347
|
+
console.warn(`[ActivityPub] broadcastDelete failed for ${url}: ${err.message}`)
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Micropub update hook: broadcast an Update for the modified post. */
|
|
352
|
+
export async function updatePost(self, properties) {
|
|
353
|
+
await broadcastPostUpdate(self, properties).catch((err) =>
|
|
354
|
+
console.warn(`[ActivityPub] broadcastPostUpdate failed for ${properties?.url}: ${err.message}`)
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Send an Update activity to all followers for a modified post. */
|
|
359
|
+
export async function broadcastPostUpdate(self, properties) {
|
|
360
|
+
if (!self._federation) return;
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const { Update } = await import("@fedify/fedify/vocab");
|
|
364
|
+
const actorUrl = getActorUrl(self);
|
|
365
|
+
const handle = self.options.actor.handle;
|
|
366
|
+
const ctx = self._federation.createContext(
|
|
367
|
+
new URL(self._publicationUrl),
|
|
368
|
+
{ handle, publicationUrl: self._publicationUrl },
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const createActivity = jf2ToAS2Activity(
|
|
372
|
+
properties,
|
|
373
|
+
actorUrl,
|
|
374
|
+
self._publicationUrl,
|
|
375
|
+
{ visibility: self.options.defaultVisibility },
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
if (!createActivity) {
|
|
379
|
+
console.warn(`[ActivityPub] broadcastPostUpdate: could not convert post to AS2 for ${properties?.url}`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const noteObject = await createActivity.getObject();
|
|
384
|
+
const activity = new Update({
|
|
385
|
+
actor: ctx.getActorUri(handle),
|
|
386
|
+
object: noteObject,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
await batchBroadcast({
|
|
390
|
+
federation: self._federation,
|
|
391
|
+
collections: self._collections,
|
|
392
|
+
publicationUrl: self._publicationUrl,
|
|
393
|
+
handle,
|
|
394
|
+
activity,
|
|
395
|
+
label: "Update(Note)",
|
|
396
|
+
objectUrl: properties.url,
|
|
397
|
+
});
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.warn("[ActivityPub] broadcastPostUpdate failed:", error.message);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Build the full actor URL from config. */
|
|
404
|
+
export function getActorUrl(self) {
|
|
405
|
+
const base = self._publicationUrl.replace(/\/$/, "");
|
|
406
|
+
return `${base}${self.options.mountPath}/users/${self.options.actor.handle}`;
|
|
407
|
+
}
|
|
@@ -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
|
+
}
|