@rmdes/indiekit-endpoint-activitypub 0.1.10 → 1.0.0
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 +155 -174
- package/lib/controllers/migrate.js +23 -5
- package/lib/controllers/profile.js +71 -0
- package/lib/federation-bridge.js +119 -0
- package/lib/federation-setup.js +321 -0
- package/lib/inbox-listeners.js +215 -0
- package/lib/jf2-to-as2.js +262 -63
- package/lib/kv-store.js +55 -0
- package/locales/en.json +18 -0
- package/package.json +2 -1
- package/views/activitypub-dashboard.njk +4 -0
- package/views/activitypub-profile.njk +74 -0
- package/lib/actor.js +0 -75
- package/lib/federation.js +0 -410
- package/lib/inbox.js +0 -291
- package/lib/keys.js +0 -39
- package/lib/webfinger.js +0 -43
package/index.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import {
|
|
3
|
+
import { setupFederation } from "./lib/federation-setup.js";
|
|
4
|
+
import {
|
|
5
|
+
createFedifyMiddleware,
|
|
6
|
+
} from "./lib/federation-bridge.js";
|
|
7
|
+
import {
|
|
8
|
+
jf2ToActivityStreams,
|
|
9
|
+
jf2ToAS2Activity,
|
|
10
|
+
} from "./lib/jf2-to-as2.js";
|
|
8
11
|
import { dashboardController } from "./lib/controllers/dashboard.js";
|
|
9
12
|
import { followersController } from "./lib/controllers/followers.js";
|
|
10
13
|
import { followingController } from "./lib/controllers/following.js";
|
|
11
14
|
import { activitiesController } from "./lib/controllers/activities.js";
|
|
12
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
migrateGetController,
|
|
17
|
+
migratePostController,
|
|
18
|
+
migrateImportController,
|
|
19
|
+
} from "./lib/controllers/migrate.js";
|
|
20
|
+
import {
|
|
21
|
+
profileGetController,
|
|
22
|
+
profilePostController,
|
|
23
|
+
} from "./lib/controllers/profile.js";
|
|
13
24
|
|
|
14
25
|
const defaults = {
|
|
15
26
|
mountPath: "/activitypub",
|
|
@@ -21,8 +32,8 @@ const defaults = {
|
|
|
21
32
|
},
|
|
22
33
|
checked: true,
|
|
23
34
|
alsoKnownAs: "",
|
|
24
|
-
activityRetentionDays: 90,
|
|
25
|
-
storeRawActivities: false,
|
|
35
|
+
activityRetentionDays: 90,
|
|
36
|
+
storeRawActivities: false,
|
|
26
37
|
};
|
|
27
38
|
|
|
28
39
|
export default class ActivityPubEndpoint {
|
|
@@ -33,11 +44,10 @@ export default class ActivityPubEndpoint {
|
|
|
33
44
|
this.options.actor = { ...defaults.actor, ...options.actor };
|
|
34
45
|
this.mountPath = this.options.mountPath;
|
|
35
46
|
|
|
36
|
-
// Set at init time when we have access to Indiekit
|
|
37
47
|
this._publicationUrl = "";
|
|
38
|
-
this._actorUrl = "";
|
|
39
48
|
this._collections = {};
|
|
40
|
-
this.
|
|
49
|
+
this._federation = null;
|
|
50
|
+
this._fedifyMiddleware = null;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
get navigationItems() {
|
|
@@ -48,127 +58,40 @@ export default class ActivityPubEndpoint {
|
|
|
48
58
|
};
|
|
49
59
|
}
|
|
50
60
|
|
|
51
|
-
// filePath is set by Indiekit's plugin loader via require.resolve()
|
|
52
|
-
|
|
53
61
|
/**
|
|
54
|
-
* WebFinger
|
|
62
|
+
* WebFinger + NodeInfo discovery — mounted at /.well-known/
|
|
63
|
+
* Fedify handles these automatically via federation.fetch().
|
|
55
64
|
*/
|
|
56
65
|
get routesWellKnown() {
|
|
57
66
|
const router = express.Router(); // eslint-disable-line new-cap
|
|
58
|
-
const options = this.options;
|
|
59
67
|
const self = this;
|
|
60
68
|
|
|
61
|
-
router.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return response.status(400).json({ error: "Missing resource parameter" });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const result = handleWebFinger(resource, {
|
|
68
|
-
handle: options.actor.handle,
|
|
69
|
-
hostname: new URL(self._publicationUrl).hostname,
|
|
70
|
-
actorUrl: self._actorUrl,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
if (!result) {
|
|
74
|
-
return response.status(404).json({ error: "Resource not found" });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
response.set("Content-Type", "application/jrd+json");
|
|
78
|
-
return response.json(result);
|
|
69
|
+
router.use((req, res, next) => {
|
|
70
|
+
if (!self._fedifyMiddleware) return next();
|
|
71
|
+
return self._fedifyMiddleware(req, res, next);
|
|
79
72
|
});
|
|
80
73
|
|
|
81
74
|
return router;
|
|
82
75
|
}
|
|
83
76
|
|
|
84
77
|
/**
|
|
85
|
-
* Public federation routes — mounted at mountPath
|
|
78
|
+
* Public federation routes — mounted at mountPath.
|
|
79
|
+
* Fedify handles actor, inbox, outbox, followers, following.
|
|
86
80
|
*/
|
|
87
81
|
get routesPublic() {
|
|
88
82
|
const router = express.Router(); // eslint-disable-line new-cap
|
|
89
83
|
const self = this;
|
|
90
84
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (!actor) {
|
|
95
|
-
return response.status(500).json({ error: "Actor not configured" });
|
|
96
|
-
}
|
|
97
|
-
response.set("Content-Type", "application/activity+json");
|
|
98
|
-
return response.json(actor);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// Inbox — receive incoming activities
|
|
102
|
-
router.post("/inbox", express.raw({ type: ["application/activity+json", "application/ld+json", "application/json"] }), async (request, response, next) => {
|
|
103
|
-
try {
|
|
104
|
-
if (self._federationHandler) {
|
|
105
|
-
return await self._federationHandler.handleInbox(request, response);
|
|
106
|
-
}
|
|
107
|
-
return response.status(202).json({ status: "accepted" });
|
|
108
|
-
} catch (error) {
|
|
109
|
-
next(error);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// Outbox — serve published posts as ActivityStreams
|
|
114
|
-
router.get("/outbox", async (request, response, next) => {
|
|
115
|
-
try {
|
|
116
|
-
if (self._federationHandler) {
|
|
117
|
-
return await self._federationHandler.handleOutbox(request, response);
|
|
118
|
-
}
|
|
119
|
-
response.set("Content-Type", "application/activity+json");
|
|
120
|
-
return response.json({
|
|
121
|
-
"@context": "https://www.w3.org/ns/activitystreams",
|
|
122
|
-
type: "OrderedCollection",
|
|
123
|
-
totalItems: 0,
|
|
124
|
-
orderedItems: [],
|
|
125
|
-
});
|
|
126
|
-
} catch (error) {
|
|
127
|
-
next(error);
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Followers collection
|
|
132
|
-
router.get("/followers", async (request, response, next) => {
|
|
133
|
-
try {
|
|
134
|
-
if (self._federationHandler) {
|
|
135
|
-
return await self._federationHandler.handleFollowers(request, response);
|
|
136
|
-
}
|
|
137
|
-
response.set("Content-Type", "application/activity+json");
|
|
138
|
-
return response.json({
|
|
139
|
-
"@context": "https://www.w3.org/ns/activitystreams",
|
|
140
|
-
type: "OrderedCollection",
|
|
141
|
-
totalItems: 0,
|
|
142
|
-
orderedItems: [],
|
|
143
|
-
});
|
|
144
|
-
} catch (error) {
|
|
145
|
-
next(error);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Following collection
|
|
150
|
-
router.get("/following", async (request, response, next) => {
|
|
151
|
-
try {
|
|
152
|
-
if (self._federationHandler) {
|
|
153
|
-
return await self._federationHandler.handleFollowing(request, response);
|
|
154
|
-
}
|
|
155
|
-
response.set("Content-Type", "application/activity+json");
|
|
156
|
-
return response.json({
|
|
157
|
-
"@context": "https://www.w3.org/ns/activitystreams",
|
|
158
|
-
type: "OrderedCollection",
|
|
159
|
-
totalItems: 0,
|
|
160
|
-
orderedItems: [],
|
|
161
|
-
});
|
|
162
|
-
} catch (error) {
|
|
163
|
-
next(error);
|
|
164
|
-
}
|
|
85
|
+
router.use((req, res, next) => {
|
|
86
|
+
if (!self._fedifyMiddleware) return next();
|
|
87
|
+
return self._fedifyMiddleware(req, res, next);
|
|
165
88
|
});
|
|
166
89
|
|
|
167
90
|
return router;
|
|
168
91
|
}
|
|
169
92
|
|
|
170
93
|
/**
|
|
171
|
-
* Authenticated admin routes — mounted at mountPath, behind IndieAuth
|
|
94
|
+
* Authenticated admin routes — mounted at mountPath, behind IndieAuth.
|
|
172
95
|
*/
|
|
173
96
|
get routes() {
|
|
174
97
|
const router = express.Router(); // eslint-disable-line new-cap
|
|
@@ -178,23 +101,36 @@ export default class ActivityPubEndpoint {
|
|
|
178
101
|
router.get("/admin/followers", followersController(mp));
|
|
179
102
|
router.get("/admin/following", followingController(mp));
|
|
180
103
|
router.get("/admin/activities", activitiesController(mp));
|
|
104
|
+
router.get("/admin/profile", profileGetController(mp));
|
|
105
|
+
router.post("/admin/profile", profilePostController(mp));
|
|
181
106
|
router.get("/admin/migrate", migrateGetController(mp, this.options));
|
|
182
107
|
router.post("/admin/migrate", migratePostController(mp, this.options));
|
|
183
|
-
router.post(
|
|
108
|
+
router.post(
|
|
109
|
+
"/admin/migrate/import",
|
|
110
|
+
migrateImportController(mp, this.options),
|
|
111
|
+
);
|
|
184
112
|
|
|
185
113
|
return router;
|
|
186
114
|
}
|
|
187
115
|
|
|
188
116
|
/**
|
|
189
|
-
* Content negotiation
|
|
190
|
-
*
|
|
117
|
+
* Content negotiation — serves AS2 JSON for ActivityPub clients
|
|
118
|
+
* requesting individual post URLs. Also handles NodeInfo data
|
|
119
|
+
* at /nodeinfo/2.1 (delegated to Fedify).
|
|
191
120
|
*/
|
|
192
121
|
get contentNegotiationRoutes() {
|
|
193
122
|
const router = express.Router(); // eslint-disable-line new-cap
|
|
194
123
|
const self = this;
|
|
195
124
|
|
|
196
|
-
|
|
197
|
-
|
|
125
|
+
// Let Fedify handle NodeInfo data (/nodeinfo/2.1)
|
|
126
|
+
router.use((req, res, next) => {
|
|
127
|
+
if (!self._fedifyMiddleware) return next();
|
|
128
|
+
return self._fedifyMiddleware(req, res, next);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Content negotiation for AP clients on regular URLs
|
|
132
|
+
router.get("{*path}", async (req, res, next) => {
|
|
133
|
+
const accept = req.headers.accept || "";
|
|
198
134
|
const isActivityPub =
|
|
199
135
|
accept.includes("application/activity+json") ||
|
|
200
136
|
accept.includes("application/ld+json");
|
|
@@ -204,25 +140,20 @@ export default class ActivityPubEndpoint {
|
|
|
204
140
|
}
|
|
205
141
|
|
|
206
142
|
try {
|
|
207
|
-
// Root URL —
|
|
208
|
-
if (
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
return next();
|
|
212
|
-
}
|
|
213
|
-
response.set("Content-Type", "application/activity+json");
|
|
214
|
-
return response.json(actor);
|
|
143
|
+
// Root URL — redirect to Fedify actor
|
|
144
|
+
if (req.path === "/") {
|
|
145
|
+
const actorPath = `${self.options.mountPath}/users/${self.options.actor.handle}`;
|
|
146
|
+
return res.redirect(actorPath);
|
|
215
147
|
}
|
|
216
148
|
|
|
217
149
|
// Post URLs — look up in database and convert to AS2
|
|
218
|
-
const { application } =
|
|
150
|
+
const { application } = req.app.locals;
|
|
219
151
|
const postsCollection = application?.collections?.get("posts");
|
|
220
152
|
if (!postsCollection) {
|
|
221
153
|
return next();
|
|
222
154
|
}
|
|
223
155
|
|
|
224
|
-
|
|
225
|
-
const requestUrl = `${self._publicationUrl}${request.path.slice(1)}`;
|
|
156
|
+
const requestUrl = `${self._publicationUrl}${req.path.slice(1)}`;
|
|
226
157
|
const post = await postsCollection.findOne({
|
|
227
158
|
"properties.url": requestUrl,
|
|
228
159
|
});
|
|
@@ -231,16 +162,16 @@ export default class ActivityPubEndpoint {
|
|
|
231
162
|
return next();
|
|
232
163
|
}
|
|
233
164
|
|
|
165
|
+
const actorUrl = self._getActorUrl();
|
|
234
166
|
const activity = jf2ToActivityStreams(
|
|
235
167
|
post.properties,
|
|
236
|
-
|
|
168
|
+
actorUrl,
|
|
237
169
|
self._publicationUrl,
|
|
238
170
|
);
|
|
239
171
|
|
|
240
|
-
// Return the object, not the wrapping Create activity
|
|
241
172
|
const object = activity.object || activity;
|
|
242
|
-
|
|
243
|
-
return
|
|
173
|
+
res.set("Content-Type", "application/activity+json");
|
|
174
|
+
return res.json({
|
|
244
175
|
"@context": [
|
|
245
176
|
"https://www.w3.org/ns/activitystreams",
|
|
246
177
|
"https://w3id.org/security/v1",
|
|
@@ -256,30 +187,7 @@ export default class ActivityPubEndpoint {
|
|
|
256
187
|
}
|
|
257
188
|
|
|
258
189
|
/**
|
|
259
|
-
*
|
|
260
|
-
*/
|
|
261
|
-
async _getActorDocument() {
|
|
262
|
-
const keysCollection = this._collections.ap_keys;
|
|
263
|
-
if (!keysCollection) {
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const keyPair = await getOrCreateKeyPair(keysCollection, this._actorUrl);
|
|
268
|
-
return buildActorDocument({
|
|
269
|
-
actorUrl: this._actorUrl,
|
|
270
|
-
publicationUrl: this._publicationUrl,
|
|
271
|
-
mountPath: this.options.mountPath,
|
|
272
|
-
handle: this.options.actor.handle,
|
|
273
|
-
name: this.options.actor.name,
|
|
274
|
-
summary: this.options.actor.summary,
|
|
275
|
-
icon: this.options.actor.icon,
|
|
276
|
-
alsoKnownAs: this.options.alsoKnownAs,
|
|
277
|
-
publicKeyPem: keyPair.publicKeyPem,
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Syndicator — delivers posts to ActivityPub followers
|
|
190
|
+
* Syndicator — delivers posts to ActivityPub followers via Fedify.
|
|
283
191
|
*/
|
|
284
192
|
get syndicator() {
|
|
285
193
|
const self = this;
|
|
@@ -303,15 +211,35 @@ export default class ActivityPubEndpoint {
|
|
|
303
211
|
};
|
|
304
212
|
},
|
|
305
213
|
|
|
306
|
-
async syndicate(properties
|
|
307
|
-
if (!self.
|
|
214
|
+
async syndicate(properties) {
|
|
215
|
+
if (!self._federation) {
|
|
308
216
|
return undefined;
|
|
309
217
|
}
|
|
218
|
+
|
|
310
219
|
try {
|
|
311
|
-
|
|
220
|
+
const actorUrl = self._getActorUrl();
|
|
221
|
+
const activity = jf2ToAS2Activity(
|
|
312
222
|
properties,
|
|
313
|
-
|
|
223
|
+
actorUrl,
|
|
224
|
+
self._publicationUrl,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (!activity) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const ctx = self._federation.createContext(
|
|
232
|
+
new URL(self._publicationUrl),
|
|
233
|
+
{},
|
|
314
234
|
);
|
|
235
|
+
|
|
236
|
+
await ctx.sendActivity(
|
|
237
|
+
{ identifier: self.options.actor.handle },
|
|
238
|
+
"followers",
|
|
239
|
+
activity,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return properties.url || undefined;
|
|
315
243
|
} catch (error) {
|
|
316
244
|
console.error("[ActivityPub] Syndication failed:", error.message);
|
|
317
245
|
return undefined;
|
|
@@ -320,6 +248,15 @@ export default class ActivityPubEndpoint {
|
|
|
320
248
|
};
|
|
321
249
|
}
|
|
322
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Build the full actor URL from config.
|
|
253
|
+
* @returns {string}
|
|
254
|
+
*/
|
|
255
|
+
_getActorUrl() {
|
|
256
|
+
const base = this._publicationUrl.replace(/\/$/, "");
|
|
257
|
+
return `${base}${this.options.mountPath}/users/${this.options.actor.handle}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
323
260
|
init(Indiekit) {
|
|
324
261
|
// Store publication URL for later use
|
|
325
262
|
this._publicationUrl = Indiekit.publication?.me
|
|
@@ -327,23 +264,31 @@ export default class ActivityPubEndpoint {
|
|
|
327
264
|
? Indiekit.publication.me
|
|
328
265
|
: `${Indiekit.publication.me}/`
|
|
329
266
|
: "";
|
|
330
|
-
this._actorUrl = this._publicationUrl;
|
|
331
267
|
|
|
332
268
|
// Register MongoDB collections
|
|
333
269
|
Indiekit.addCollection("ap_followers");
|
|
334
270
|
Indiekit.addCollection("ap_following");
|
|
335
271
|
Indiekit.addCollection("ap_activities");
|
|
336
272
|
Indiekit.addCollection("ap_keys");
|
|
273
|
+
Indiekit.addCollection("ap_kv");
|
|
274
|
+
Indiekit.addCollection("ap_profile");
|
|
337
275
|
|
|
338
|
-
// Store collection references
|
|
276
|
+
// Store collection references (posts resolved lazily)
|
|
277
|
+
const indiekitCollections = Indiekit.collections;
|
|
339
278
|
this._collections = {
|
|
340
|
-
ap_followers:
|
|
341
|
-
ap_following:
|
|
342
|
-
ap_activities:
|
|
343
|
-
ap_keys:
|
|
279
|
+
ap_followers: indiekitCollections.get("ap_followers"),
|
|
280
|
+
ap_following: indiekitCollections.get("ap_following"),
|
|
281
|
+
ap_activities: indiekitCollections.get("ap_activities"),
|
|
282
|
+
ap_keys: indiekitCollections.get("ap_keys"),
|
|
283
|
+
ap_kv: indiekitCollections.get("ap_kv"),
|
|
284
|
+
ap_profile: indiekitCollections.get("ap_profile"),
|
|
285
|
+
get posts() {
|
|
286
|
+
return indiekitCollections.get("posts");
|
|
287
|
+
},
|
|
288
|
+
_publicationUrl: this._publicationUrl,
|
|
344
289
|
};
|
|
345
290
|
|
|
346
|
-
//
|
|
291
|
+
// TTL index for activity cleanup (MongoDB handles expiry automatically)
|
|
347
292
|
const retentionDays = this.options.activityRetentionDays;
|
|
348
293
|
if (retentionDays > 0) {
|
|
349
294
|
this._collections.ap_activities.createIndex(
|
|
@@ -352,28 +297,64 @@ export default class ActivityPubEndpoint {
|
|
|
352
297
|
);
|
|
353
298
|
}
|
|
354
299
|
|
|
355
|
-
//
|
|
356
|
-
this.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
300
|
+
// Seed actor profile from config on first run
|
|
301
|
+
this._seedProfile().catch((error) => {
|
|
302
|
+
console.warn("[ActivityPub] Profile seed failed:", error.message);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Set up Fedify Federation instance
|
|
306
|
+
const { federation } = setupFederation({
|
|
362
307
|
collections: this._collections,
|
|
308
|
+
mountPath: this.options.mountPath,
|
|
309
|
+
handle: this.options.actor.handle,
|
|
363
310
|
storeRawActivities: this.options.storeRawActivities,
|
|
364
311
|
});
|
|
365
312
|
|
|
366
|
-
|
|
313
|
+
this._federation = federation;
|
|
314
|
+
this._fedifyMiddleware = createFedifyMiddleware(federation, () => ({}));
|
|
315
|
+
|
|
316
|
+
// Register as endpoint (mounts routesPublic, routesWellKnown, routes)
|
|
367
317
|
Indiekit.addEndpoint(this);
|
|
368
318
|
|
|
369
|
-
//
|
|
319
|
+
// Content negotiation + NodeInfo — virtual endpoint at root
|
|
370
320
|
Indiekit.addEndpoint({
|
|
371
321
|
name: "ActivityPub content negotiation",
|
|
372
322
|
mountPath: "/",
|
|
373
323
|
routesPublic: this.contentNegotiationRoutes,
|
|
374
324
|
});
|
|
375
325
|
|
|
376
|
-
// Register
|
|
326
|
+
// Register syndicator (appears in post editing UI)
|
|
377
327
|
Indiekit.addSyndicator(this.syndicator);
|
|
378
328
|
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Seed the ap_profile collection from config options on first run.
|
|
332
|
+
* Only creates a profile if none exists — preserves UI edits.
|
|
333
|
+
*/
|
|
334
|
+
async _seedProfile() {
|
|
335
|
+
const { ap_profile } = this._collections;
|
|
336
|
+
const existing = await ap_profile.findOne({});
|
|
337
|
+
|
|
338
|
+
if (existing) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const profile = {
|
|
343
|
+
name: this.options.actor.name || this.options.actor.handle,
|
|
344
|
+
summary: this.options.actor.summary || "",
|
|
345
|
+
url: this._publicationUrl,
|
|
346
|
+
icon: this.options.actor.icon || "",
|
|
347
|
+
manuallyApprovesFollowers: false,
|
|
348
|
+
createdAt: new Date().toISOString(),
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Only include alsoKnownAs if explicitly configured
|
|
352
|
+
if (this.options.alsoKnownAs) {
|
|
353
|
+
profile.alsoKnownAs = Array.isArray(this.options.alsoKnownAs)
|
|
354
|
+
? this.options.alsoKnownAs
|
|
355
|
+
: [this.options.alsoKnownAs];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
await ap_profile.insertOne(profile);
|
|
359
|
+
}
|
|
379
360
|
}
|
|
@@ -14,10 +14,18 @@ import {
|
|
|
14
14
|
export function migrateGetController(mountPath, pluginOptions) {
|
|
15
15
|
return async (request, response, next) => {
|
|
16
16
|
try {
|
|
17
|
+
const { application } = request.app.locals;
|
|
18
|
+
const profileCollection = application?.collections?.get("ap_profile");
|
|
19
|
+
const profile = profileCollection
|
|
20
|
+
? (await profileCollection.findOne({})) || {}
|
|
21
|
+
: {};
|
|
22
|
+
|
|
23
|
+
const currentAlias = profile.alsoKnownAs?.[0] || "";
|
|
24
|
+
|
|
17
25
|
response.render("activitypub-migrate", {
|
|
18
26
|
title: response.locals.__("activitypub.migrate.title"),
|
|
19
27
|
mountPath,
|
|
20
|
-
currentAlias
|
|
28
|
+
currentAlias,
|
|
21
29
|
result: null,
|
|
22
30
|
});
|
|
23
31
|
} catch (error) {
|
|
@@ -29,22 +37,32 @@ export function migrateGetController(mountPath, pluginOptions) {
|
|
|
29
37
|
export function migratePostController(mountPath, pluginOptions) {
|
|
30
38
|
return async (request, response, next) => {
|
|
31
39
|
try {
|
|
40
|
+
const { application } = request.app.locals;
|
|
41
|
+
const profileCollection = application?.collections?.get("ap_profile");
|
|
32
42
|
let result = null;
|
|
33
43
|
|
|
34
|
-
// Only handles alias updates (small payload, regular form POST)
|
|
35
44
|
const aliasUrl = request.body.aliasUrl?.trim();
|
|
36
|
-
if (aliasUrl) {
|
|
37
|
-
|
|
45
|
+
if (aliasUrl && profileCollection) {
|
|
46
|
+
await profileCollection.updateOne(
|
|
47
|
+
{},
|
|
48
|
+
{ $set: { alsoKnownAs: [aliasUrl] } },
|
|
49
|
+
{ upsert: true },
|
|
50
|
+
);
|
|
38
51
|
result = {
|
|
39
52
|
type: "success",
|
|
40
53
|
text: response.locals.__("activitypub.migrate.aliasSuccess"),
|
|
41
54
|
};
|
|
42
55
|
}
|
|
43
56
|
|
|
57
|
+
const profile = profileCollection
|
|
58
|
+
? (await profileCollection.findOne({})) || {}
|
|
59
|
+
: {};
|
|
60
|
+
const currentAlias = profile.alsoKnownAs?.[0] || "";
|
|
61
|
+
|
|
44
62
|
response.render("activitypub-migrate", {
|
|
45
63
|
title: response.locals.__("activitypub.migrate.title"),
|
|
46
64
|
mountPath,
|
|
47
|
-
currentAlias
|
|
65
|
+
currentAlias,
|
|
48
66
|
result,
|
|
49
67
|
});
|
|
50
68
|
} catch (error) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile controller — edit the ActivityPub actor profile.
|
|
3
|
+
*
|
|
4
|
+
* GET: loads profile from ap_profile collection, renders form
|
|
5
|
+
* POST: saves updated profile fields back to ap_profile
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function profileGetController(mountPath) {
|
|
9
|
+
return async (request, response, next) => {
|
|
10
|
+
try {
|
|
11
|
+
const { application } = request.app.locals;
|
|
12
|
+
const profileCollection = application?.collections?.get("ap_profile");
|
|
13
|
+
const profile = profileCollection
|
|
14
|
+
? (await profileCollection.findOne({})) || {}
|
|
15
|
+
: {};
|
|
16
|
+
|
|
17
|
+
response.render("activitypub-profile", {
|
|
18
|
+
title: response.locals.__("activitypub.profile.title"),
|
|
19
|
+
mountPath,
|
|
20
|
+
profile,
|
|
21
|
+
result: null,
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
next(error);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function profilePostController(mountPath) {
|
|
30
|
+
return async (request, response, next) => {
|
|
31
|
+
try {
|
|
32
|
+
const { application } = request.app.locals;
|
|
33
|
+
const profileCollection = application?.collections?.get("ap_profile");
|
|
34
|
+
|
|
35
|
+
if (!profileCollection) {
|
|
36
|
+
return next(new Error("ap_profile collection not available"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { name, summary, url, icon, image, manuallyApprovesFollowers } =
|
|
40
|
+
request.body;
|
|
41
|
+
|
|
42
|
+
const update = {
|
|
43
|
+
$set: {
|
|
44
|
+
name: name?.trim() || "",
|
|
45
|
+
summary: summary?.trim() || "",
|
|
46
|
+
url: url?.trim() || "",
|
|
47
|
+
icon: icon?.trim() || "",
|
|
48
|
+
image: image?.trim() || "",
|
|
49
|
+
manuallyApprovesFollowers: manuallyApprovesFollowers === "true",
|
|
50
|
+
updatedAt: new Date().toISOString(),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
await profileCollection.updateOne({}, update, { upsert: true });
|
|
55
|
+
|
|
56
|
+
const profile = await profileCollection.findOne({});
|
|
57
|
+
|
|
58
|
+
response.render("activitypub-profile", {
|
|
59
|
+
title: response.locals.__("activitypub.profile.title"),
|
|
60
|
+
mountPath,
|
|
61
|
+
profile,
|
|
62
|
+
result: {
|
|
63
|
+
type: "success",
|
|
64
|
+
text: response.locals.__("activitypub.profile.saved"),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
next(error);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|