@rmdes/indiekit-endpoint-activitypub 1.0.28 → 1.1.1
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/assets/reader.css +884 -0
- package/index.js +172 -15
- package/lib/controllers/compose.js +323 -0
- package/lib/controllers/featured-tags.js +12 -2
- package/lib/controllers/featured.js +12 -2
- package/lib/controllers/interactions-boost.js +208 -0
- package/lib/controllers/interactions-like.js +231 -0
- package/lib/controllers/interactions.js +7 -0
- package/lib/controllers/moderation.js +294 -0
- package/lib/controllers/profile.js +27 -1
- package/lib/controllers/profile.remote.js +218 -0
- package/lib/controllers/reader.js +187 -0
- package/lib/csrf.js +49 -0
- package/lib/federation-setup.js +33 -2
- package/lib/inbox-listeners.js +217 -213
- package/lib/storage/moderation.js +180 -0
- package/lib/storage/notifications.js +132 -0
- package/lib/storage/timeline.js +210 -0
- package/lib/timeline-cleanup.js +88 -0
- package/lib/timeline-store.js +207 -0
- package/locales/en.json +92 -1
- package/package.json +3 -2
- package/views/activitypub-compose.njk +94 -0
- package/views/activitypub-moderation.njk +118 -0
- package/views/activitypub-notifications.njk +31 -0
- package/views/activitypub-profile.njk +98 -0
- package/views/activitypub-reader.njk +61 -0
- package/views/activitypub-remote-profile.njk +117 -0
- package/views/layouts/reader.njk +9 -0
- package/views/partials/ap-item-card.njk +157 -0
- package/views/partials/ap-item-media.njk +37 -0
- package/views/partials/ap-notification-card.njk +58 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moderation controllers — Mute, Unmute, Block, Unblock.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { validateToken, getToken } from "../csrf.js";
|
|
6
|
+
import {
|
|
7
|
+
addMuted,
|
|
8
|
+
removeMuted,
|
|
9
|
+
addBlocked,
|
|
10
|
+
removeBlocked,
|
|
11
|
+
getAllMuted,
|
|
12
|
+
getAllBlocked,
|
|
13
|
+
} from "../storage/moderation.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Helper to get moderation collections from request.
|
|
17
|
+
*/
|
|
18
|
+
function getModerationCollections(request) {
|
|
19
|
+
const { application } = request.app.locals;
|
|
20
|
+
return {
|
|
21
|
+
ap_muted: application?.collections?.get("ap_muted"),
|
|
22
|
+
ap_blocked: application?.collections?.get("ap_blocked"),
|
|
23
|
+
ap_timeline: application?.collections?.get("ap_timeline"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* POST /admin/reader/mute — Mute an actor or keyword.
|
|
29
|
+
*/
|
|
30
|
+
export function muteController(mountPath, plugin) {
|
|
31
|
+
return async (request, response, next) => {
|
|
32
|
+
try {
|
|
33
|
+
if (!validateToken(request)) {
|
|
34
|
+
return response.status(403).json({
|
|
35
|
+
success: false,
|
|
36
|
+
error: "Invalid CSRF token",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { url, keyword } = request.body;
|
|
41
|
+
|
|
42
|
+
if (!url && !keyword) {
|
|
43
|
+
return response.status(400).json({
|
|
44
|
+
success: false,
|
|
45
|
+
error: "Provide url or keyword to mute",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const collections = getModerationCollections(request);
|
|
50
|
+
await addMuted(collections, { url: url || undefined, keyword: keyword || undefined });
|
|
51
|
+
|
|
52
|
+
console.info(
|
|
53
|
+
`[ActivityPub] Muted ${url ? `actor: ${url}` : `keyword: ${keyword}`}`,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return response.json({
|
|
57
|
+
success: true,
|
|
58
|
+
type: "mute",
|
|
59
|
+
url: url || undefined,
|
|
60
|
+
keyword: keyword || undefined,
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error("[ActivityPub] Mute failed:", error.message);
|
|
64
|
+
return response.status(500).json({
|
|
65
|
+
success: false,
|
|
66
|
+
error: "Operation failed. Please try again later.",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* POST /admin/reader/unmute — Unmute an actor or keyword.
|
|
74
|
+
*/
|
|
75
|
+
export function unmuteController(mountPath, plugin) {
|
|
76
|
+
return async (request, response, next) => {
|
|
77
|
+
try {
|
|
78
|
+
if (!validateToken(request)) {
|
|
79
|
+
return response.status(403).json({
|
|
80
|
+
success: false,
|
|
81
|
+
error: "Invalid CSRF token",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { url, keyword } = request.body;
|
|
86
|
+
|
|
87
|
+
if (!url && !keyword) {
|
|
88
|
+
return response.status(400).json({
|
|
89
|
+
success: false,
|
|
90
|
+
error: "Provide url or keyword to unmute",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const collections = getModerationCollections(request);
|
|
95
|
+
await removeMuted(collections, { url: url || undefined, keyword: keyword || undefined });
|
|
96
|
+
|
|
97
|
+
return response.json({
|
|
98
|
+
success: true,
|
|
99
|
+
type: "unmute",
|
|
100
|
+
url: url || undefined,
|
|
101
|
+
keyword: keyword || undefined,
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return response.status(500).json({
|
|
105
|
+
success: false,
|
|
106
|
+
error: "Operation failed. Please try again later.",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* POST /admin/reader/block — Block an actor (sends Block activity + removes timeline items).
|
|
114
|
+
*/
|
|
115
|
+
export function blockController(mountPath, plugin) {
|
|
116
|
+
return async (request, response, next) => {
|
|
117
|
+
try {
|
|
118
|
+
if (!validateToken(request)) {
|
|
119
|
+
return response.status(403).json({
|
|
120
|
+
success: false,
|
|
121
|
+
error: "Invalid CSRF token",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { url } = request.body;
|
|
126
|
+
|
|
127
|
+
if (!url) {
|
|
128
|
+
return response.status(400).json({
|
|
129
|
+
success: false,
|
|
130
|
+
error: "Missing actor URL",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const collections = getModerationCollections(request);
|
|
135
|
+
|
|
136
|
+
// Store the block
|
|
137
|
+
await addBlocked(collections, url);
|
|
138
|
+
|
|
139
|
+
// Remove timeline items from this actor
|
|
140
|
+
if (collections.ap_timeline) {
|
|
141
|
+
await collections.ap_timeline.deleteMany({ "author.url": url });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Send Block activity via federation
|
|
145
|
+
if (plugin._federation) {
|
|
146
|
+
try {
|
|
147
|
+
const { Block } = await import("@fedify/fedify");
|
|
148
|
+
const handle = plugin.options.actor.handle;
|
|
149
|
+
const ctx = plugin._federation.createContext(
|
|
150
|
+
new URL(plugin._publicationUrl),
|
|
151
|
+
{ handle, publicationUrl: plugin._publicationUrl },
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const remoteActor = await ctx.lookupObject(new URL(url));
|
|
155
|
+
|
|
156
|
+
if (remoteActor) {
|
|
157
|
+
const block = new Block({
|
|
158
|
+
actor: ctx.getActorUri(handle),
|
|
159
|
+
object: new URL(url),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await ctx.sendActivity(
|
|
163
|
+
{ identifier: handle },
|
|
164
|
+
remoteActor,
|
|
165
|
+
block,
|
|
166
|
+
{ orderingKey: url },
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.warn(
|
|
171
|
+
`[ActivityPub] Could not send Block to ${url}: ${error.message}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.info(`[ActivityPub] Blocked actor: ${url}`);
|
|
177
|
+
|
|
178
|
+
return response.json({
|
|
179
|
+
success: true,
|
|
180
|
+
type: "block",
|
|
181
|
+
url,
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("[ActivityPub] Block failed:", error.message);
|
|
185
|
+
return response.status(500).json({
|
|
186
|
+
success: false,
|
|
187
|
+
error: "Operation failed. Please try again later.",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* POST /admin/reader/unblock — Unblock an actor (sends Undo(Block)).
|
|
195
|
+
*/
|
|
196
|
+
export function unblockController(mountPath, plugin) {
|
|
197
|
+
return async (request, response, next) => {
|
|
198
|
+
try {
|
|
199
|
+
if (!validateToken(request)) {
|
|
200
|
+
return response.status(403).json({
|
|
201
|
+
success: false,
|
|
202
|
+
error: "Invalid CSRF token",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const { url } = request.body;
|
|
207
|
+
|
|
208
|
+
if (!url) {
|
|
209
|
+
return response.status(400).json({
|
|
210
|
+
success: false,
|
|
211
|
+
error: "Missing actor URL",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const collections = getModerationCollections(request);
|
|
216
|
+
await removeBlocked(collections, url);
|
|
217
|
+
|
|
218
|
+
// Send Undo(Block) via federation
|
|
219
|
+
if (plugin._federation) {
|
|
220
|
+
try {
|
|
221
|
+
const { Block, Undo } = await import("@fedify/fedify");
|
|
222
|
+
const handle = plugin.options.actor.handle;
|
|
223
|
+
const ctx = plugin._federation.createContext(
|
|
224
|
+
new URL(plugin._publicationUrl),
|
|
225
|
+
{ handle, publicationUrl: plugin._publicationUrl },
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const remoteActor = await ctx.lookupObject(new URL(url));
|
|
229
|
+
|
|
230
|
+
if (remoteActor) {
|
|
231
|
+
const block = new Block({
|
|
232
|
+
actor: ctx.getActorUri(handle),
|
|
233
|
+
object: new URL(url),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const undo = new Undo({
|
|
237
|
+
actor: ctx.getActorUri(handle),
|
|
238
|
+
object: block,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await ctx.sendActivity(
|
|
242
|
+
{ identifier: handle },
|
|
243
|
+
remoteActor,
|
|
244
|
+
undo,
|
|
245
|
+
{ orderingKey: url },
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.warn(
|
|
250
|
+
`[ActivityPub] Could not send Undo(Block) to ${url}: ${error.message}`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
console.info(`[ActivityPub] Unblocked actor: ${url}`);
|
|
256
|
+
|
|
257
|
+
return response.json({
|
|
258
|
+
success: true,
|
|
259
|
+
type: "unblock",
|
|
260
|
+
url,
|
|
261
|
+
});
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return response.status(500).json({
|
|
264
|
+
success: false,
|
|
265
|
+
error: "Operation failed. Please try again later.",
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* GET /admin/reader/moderation — View muted/blocked lists.
|
|
273
|
+
*/
|
|
274
|
+
export function moderationController(mountPath) {
|
|
275
|
+
return async (request, response, next) => {
|
|
276
|
+
try {
|
|
277
|
+
const collections = getModerationCollections(request);
|
|
278
|
+
const csrfToken = getToken(request.session);
|
|
279
|
+
|
|
280
|
+
const muted = await getAllMuted(collections);
|
|
281
|
+
const blocked = await getAllBlocked(collections);
|
|
282
|
+
|
|
283
|
+
response.render("activitypub-moderation", {
|
|
284
|
+
title: response.locals.__("activitypub.moderation.title"),
|
|
285
|
+
muted,
|
|
286
|
+
blocked,
|
|
287
|
+
csrfToken,
|
|
288
|
+
mountPath,
|
|
289
|
+
});
|
|
290
|
+
} catch (error) {
|
|
291
|
+
next(error);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* POST: saves updated profile fields back to ap_profile
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const ACTOR_TYPES = ["Person", "Service", "Organization"];
|
|
9
|
+
|
|
8
10
|
export function profileGetController(mountPath) {
|
|
9
11
|
return async (request, response, next) => {
|
|
10
12
|
try {
|
|
@@ -18,6 +20,7 @@ export function profileGetController(mountPath) {
|
|
|
18
20
|
title: response.locals.__("activitypub.profile.title"),
|
|
19
21
|
mountPath,
|
|
20
22
|
profile,
|
|
23
|
+
actorTypes: ACTOR_TYPES,
|
|
21
24
|
result: null,
|
|
22
25
|
});
|
|
23
26
|
} catch (error) {
|
|
@@ -26,7 +29,7 @@ export function profileGetController(mountPath) {
|
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
export function profilePostController(mountPath) {
|
|
32
|
+
export function profilePostController(mountPath, plugin) {
|
|
30
33
|
return async (request, response, next) => {
|
|
31
34
|
try {
|
|
32
35
|
const { application } = request.app.locals;
|
|
@@ -42,10 +45,23 @@ export function profilePostController(mountPath) {
|
|
|
42
45
|
url,
|
|
43
46
|
icon,
|
|
44
47
|
image,
|
|
48
|
+
actorType,
|
|
45
49
|
manuallyApprovesFollowers,
|
|
46
50
|
authorizedFetch,
|
|
47
51
|
} = request.body;
|
|
48
52
|
|
|
53
|
+
// Parse profile links (attachments) from form arrays
|
|
54
|
+
const linkNames = [].concat(request.body["link_name[]"] || []);
|
|
55
|
+
const linkValues = [].concat(request.body["link_value[]"] || []);
|
|
56
|
+
const attachments = [];
|
|
57
|
+
for (let i = 0; i < linkNames.length; i++) {
|
|
58
|
+
const n = linkNames[i]?.trim();
|
|
59
|
+
const v = linkValues[i]?.trim();
|
|
60
|
+
if (n && v) {
|
|
61
|
+
attachments.push({ name: n, value: v });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
const update = {
|
|
50
66
|
$set: {
|
|
51
67
|
name: name?.trim() || "",
|
|
@@ -53,20 +69,30 @@ export function profilePostController(mountPath) {
|
|
|
53
69
|
url: url?.trim() || "",
|
|
54
70
|
icon: icon?.trim() || "",
|
|
55
71
|
image: image?.trim() || "",
|
|
72
|
+
actorType: ACTOR_TYPES.includes(actorType) ? actorType : "Person",
|
|
56
73
|
manuallyApprovesFollowers: manuallyApprovesFollowers === "true",
|
|
57
74
|
authorizedFetch: authorizedFetch === "true",
|
|
75
|
+
attachments,
|
|
58
76
|
updatedAt: new Date().toISOString(),
|
|
59
77
|
},
|
|
60
78
|
};
|
|
61
79
|
|
|
62
80
|
await profileCollection.updateOne({}, update, { upsert: true });
|
|
63
81
|
|
|
82
|
+
// Send Update(Person) to followers so remote servers re-fetch the actor
|
|
83
|
+
if (plugin?.broadcastActorUpdate) {
|
|
84
|
+
plugin.broadcastActorUpdate().catch((error) => {
|
|
85
|
+
console.warn("[ActivityPub] Profile update broadcast failed:", error.message);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
64
89
|
const profile = await profileCollection.findOne({});
|
|
65
90
|
|
|
66
91
|
response.render("activitypub-profile", {
|
|
67
92
|
title: response.locals.__("activitypub.profile.title"),
|
|
68
93
|
mountPath,
|
|
69
94
|
profile,
|
|
95
|
+
actorTypes: ACTOR_TYPES,
|
|
70
96
|
result: {
|
|
71
97
|
type: "success",
|
|
72
98
|
text: response.locals.__("activitypub.profile.saved"),
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote profile controllers — view remote actors and follow/unfollow.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getToken, validateToken } from "../csrf.js";
|
|
6
|
+
import { sanitizeContent } from "../timeline-store.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GET /admin/reader/profile — Show remote actor profile.
|
|
10
|
+
* @param {string} mountPath - Plugin mount path
|
|
11
|
+
* @param {object} plugin - ActivityPub plugin instance
|
|
12
|
+
*/
|
|
13
|
+
export function remoteProfileController(mountPath, plugin) {
|
|
14
|
+
return async (request, response, next) => {
|
|
15
|
+
try {
|
|
16
|
+
const { application } = request.app.locals;
|
|
17
|
+
const actorUrl = request.query.url || request.query.handle;
|
|
18
|
+
|
|
19
|
+
if (!actorUrl) {
|
|
20
|
+
return response.status(400).render("error", {
|
|
21
|
+
title: "Error",
|
|
22
|
+
content: "Missing actor URL or handle",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!plugin._federation) {
|
|
27
|
+
return response.status(503).render("error", {
|
|
28
|
+
title: "Error",
|
|
29
|
+
content: "Federation not initialized",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const handle = plugin.options.actor.handle;
|
|
34
|
+
const ctx = plugin._federation.createContext(
|
|
35
|
+
new URL(plugin._publicationUrl),
|
|
36
|
+
{ handle, publicationUrl: plugin._publicationUrl },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Look up the remote actor
|
|
40
|
+
let actor;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
actor = await ctx.lookupObject(new URL(actorUrl));
|
|
44
|
+
} catch {
|
|
45
|
+
return response.status(404).render("error", {
|
|
46
|
+
title: "Error",
|
|
47
|
+
content: response.locals.__("activitypub.profile.remote.notFound"),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!actor) {
|
|
52
|
+
return response.status(404).render("error", {
|
|
53
|
+
title: "Error",
|
|
54
|
+
content: response.locals.__("activitypub.profile.remote.notFound"),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Extract actor info
|
|
59
|
+
const name =
|
|
60
|
+
actor.name?.toString() ||
|
|
61
|
+
actor.preferredUsername?.toString() ||
|
|
62
|
+
actorUrl;
|
|
63
|
+
const actorHandle = actor.preferredUsername?.toString() || "";
|
|
64
|
+
const summary = sanitizeContent(actor.summary?.toString() || "");
|
|
65
|
+
let icon = "";
|
|
66
|
+
let image = "";
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const iconObj = await actor.getIcon();
|
|
70
|
+
icon = iconObj?.url?.href || "";
|
|
71
|
+
} catch {
|
|
72
|
+
// No icon
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const imageObj = await actor.getImage();
|
|
77
|
+
image = imageObj?.url?.href || "";
|
|
78
|
+
} catch {
|
|
79
|
+
// No header image
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Extract host for "View on {instance}"
|
|
83
|
+
let instanceHost = "";
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
instanceHost = new URL(actorUrl).hostname;
|
|
87
|
+
} catch {
|
|
88
|
+
// Invalid URL
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if we're following this actor
|
|
92
|
+
const followingCol = application?.collections?.get("ap_following");
|
|
93
|
+
const isFollowing = followingCol
|
|
94
|
+
? !!(await followingCol.findOne({ actorUrl }))
|
|
95
|
+
: false;
|
|
96
|
+
|
|
97
|
+
// Get their posts from our timeline (only if following)
|
|
98
|
+
let posts = [];
|
|
99
|
+
|
|
100
|
+
if (isFollowing) {
|
|
101
|
+
const timelineCol = application?.collections?.get("ap_timeline");
|
|
102
|
+
|
|
103
|
+
if (timelineCol) {
|
|
104
|
+
posts = await timelineCol
|
|
105
|
+
.find({ "author.url": actorUrl })
|
|
106
|
+
.sort({ published: -1 })
|
|
107
|
+
.limit(20)
|
|
108
|
+
.toArray();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check mute/block state
|
|
113
|
+
const mutedCol = application?.collections?.get("ap_muted");
|
|
114
|
+
const blockedCol = application?.collections?.get("ap_blocked");
|
|
115
|
+
const isMuted = mutedCol
|
|
116
|
+
? !!(await mutedCol.findOne({ url: actorUrl }))
|
|
117
|
+
: false;
|
|
118
|
+
const isBlocked = blockedCol
|
|
119
|
+
? !!(await blockedCol.findOne({ url: actorUrl }))
|
|
120
|
+
: false;
|
|
121
|
+
|
|
122
|
+
const csrfToken = getToken(request.session);
|
|
123
|
+
|
|
124
|
+
response.render("activitypub-remote-profile", {
|
|
125
|
+
title: name,
|
|
126
|
+
actorUrl,
|
|
127
|
+
name,
|
|
128
|
+
actorHandle,
|
|
129
|
+
summary,
|
|
130
|
+
icon,
|
|
131
|
+
image,
|
|
132
|
+
instanceHost,
|
|
133
|
+
isFollowing,
|
|
134
|
+
isMuted,
|
|
135
|
+
isBlocked,
|
|
136
|
+
posts,
|
|
137
|
+
csrfToken,
|
|
138
|
+
mountPath,
|
|
139
|
+
});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
next(error);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* POST /admin/reader/follow — Follow a remote actor.
|
|
148
|
+
*/
|
|
149
|
+
export function followController(mountPath, plugin) {
|
|
150
|
+
return async (request, response, next) => {
|
|
151
|
+
try {
|
|
152
|
+
if (!validateToken(request)) {
|
|
153
|
+
return response.status(403).json({
|
|
154
|
+
success: false,
|
|
155
|
+
error: "Invalid CSRF token",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const { url } = request.body;
|
|
160
|
+
|
|
161
|
+
if (!url) {
|
|
162
|
+
return response.status(400).json({
|
|
163
|
+
success: false,
|
|
164
|
+
error: "Missing actor URL",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const result = await plugin.followActor(url);
|
|
169
|
+
|
|
170
|
+
return response.json({
|
|
171
|
+
success: result.ok,
|
|
172
|
+
error: result.error || undefined,
|
|
173
|
+
});
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return response.status(500).json({
|
|
176
|
+
success: false,
|
|
177
|
+
error: "Operation failed. Please try again later.",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* POST /admin/reader/unfollow — Unfollow a remote actor.
|
|
185
|
+
*/
|
|
186
|
+
export function unfollowController(mountPath, plugin) {
|
|
187
|
+
return async (request, response, next) => {
|
|
188
|
+
try {
|
|
189
|
+
if (!validateToken(request)) {
|
|
190
|
+
return response.status(403).json({
|
|
191
|
+
success: false,
|
|
192
|
+
error: "Invalid CSRF token",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { url } = request.body;
|
|
197
|
+
|
|
198
|
+
if (!url) {
|
|
199
|
+
return response.status(400).json({
|
|
200
|
+
success: false,
|
|
201
|
+
error: "Missing actor URL",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const result = await plugin.unfollowActor(url);
|
|
206
|
+
|
|
207
|
+
return response.json({
|
|
208
|
+
success: result.ok,
|
|
209
|
+
error: result.error || undefined,
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return response.status(500).json({
|
|
213
|
+
success: false,
|
|
214
|
+
error: "Operation failed. Please try again later.",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|