@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,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boost/Unboost interaction controllers.
|
|
3
|
+
* Sends Announce and Undo(Announce) activities via Fedify.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { validateToken } from "../csrf.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST /admin/reader/boost — send an Announce activity to followers.
|
|
10
|
+
*/
|
|
11
|
+
export function boostController(mountPath, plugin) {
|
|
12
|
+
return async (request, response, next) => {
|
|
13
|
+
try {
|
|
14
|
+
if (!validateToken(request)) {
|
|
15
|
+
return response.status(403).json({
|
|
16
|
+
success: false,
|
|
17
|
+
error: "Invalid CSRF token",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { url } = request.body;
|
|
22
|
+
|
|
23
|
+
if (!url) {
|
|
24
|
+
return response.status(400).json({
|
|
25
|
+
success: false,
|
|
26
|
+
error: "Missing post URL",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!plugin._federation) {
|
|
31
|
+
return response.status(503).json({
|
|
32
|
+
success: false,
|
|
33
|
+
error: "Federation not initialized",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { Announce } = await import("@fedify/fedify");
|
|
38
|
+
const handle = plugin.options.actor.handle;
|
|
39
|
+
const ctx = plugin._federation.createContext(
|
|
40
|
+
new URL(plugin._publicationUrl),
|
|
41
|
+
{ handle, publicationUrl: plugin._publicationUrl },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const activityId = `urn:uuid:${crypto.randomUUID()}`;
|
|
45
|
+
|
|
46
|
+
// Construct Announce activity
|
|
47
|
+
const announce = new Announce({
|
|
48
|
+
id: new URL(activityId),
|
|
49
|
+
actor: ctx.getActorUri(handle),
|
|
50
|
+
object: new URL(url),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Send to followers via shared inbox
|
|
54
|
+
await ctx.sendActivity({ identifier: handle }, "followers", announce, {
|
|
55
|
+
preferSharedInbox: true,
|
|
56
|
+
syncCollection: true,
|
|
57
|
+
orderingKey: url,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Also send to the original post author
|
|
61
|
+
try {
|
|
62
|
+
const remoteObject = await ctx.lookupObject(new URL(url));
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
remoteObject &&
|
|
66
|
+
typeof remoteObject.getAttributedTo === "function"
|
|
67
|
+
) {
|
|
68
|
+
const author = await remoteObject.getAttributedTo();
|
|
69
|
+
const recipient = Array.isArray(author) ? author[0] : author;
|
|
70
|
+
|
|
71
|
+
if (recipient) {
|
|
72
|
+
await ctx.sendActivity(
|
|
73
|
+
{ identifier: handle },
|
|
74
|
+
recipient,
|
|
75
|
+
announce,
|
|
76
|
+
{ orderingKey: url },
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Non-critical — followers still received the boost
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Track the interaction
|
|
85
|
+
const { application } = request.app.locals;
|
|
86
|
+
const interactions = application?.collections?.get("ap_interactions");
|
|
87
|
+
|
|
88
|
+
if (interactions) {
|
|
89
|
+
await interactions.updateOne(
|
|
90
|
+
{ objectUrl: url, type: "boost" },
|
|
91
|
+
{
|
|
92
|
+
$set: {
|
|
93
|
+
objectUrl: url,
|
|
94
|
+
type: "boost",
|
|
95
|
+
activityId,
|
|
96
|
+
createdAt: new Date().toISOString(),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{ upsert: true },
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.info(`[ActivityPub] Sent Announce (boost) for ${url}`);
|
|
104
|
+
|
|
105
|
+
return response.json({
|
|
106
|
+
success: true,
|
|
107
|
+
type: "boost",
|
|
108
|
+
objectUrl: url,
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error("[ActivityPub] Boost failed:", error.message);
|
|
112
|
+
return response.status(500).json({
|
|
113
|
+
success: false,
|
|
114
|
+
error: "Boost failed. Please try again later.",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* POST /admin/reader/unboost — send an Undo(Announce) to followers.
|
|
122
|
+
*/
|
|
123
|
+
export function unboostController(mountPath, plugin) {
|
|
124
|
+
return async (request, response, next) => {
|
|
125
|
+
try {
|
|
126
|
+
if (!validateToken(request)) {
|
|
127
|
+
return response.status(403).json({
|
|
128
|
+
success: false,
|
|
129
|
+
error: "Invalid CSRF token",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { url } = request.body;
|
|
134
|
+
|
|
135
|
+
if (!url) {
|
|
136
|
+
return response.status(400).json({
|
|
137
|
+
success: false,
|
|
138
|
+
error: "Missing post URL",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!plugin._federation) {
|
|
143
|
+
return response.status(503).json({
|
|
144
|
+
success: false,
|
|
145
|
+
error: "Federation not initialized",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { application } = request.app.locals;
|
|
150
|
+
const interactions = application?.collections?.get("ap_interactions");
|
|
151
|
+
|
|
152
|
+
const existing = interactions
|
|
153
|
+
? await interactions.findOne({ objectUrl: url, type: "boost" })
|
|
154
|
+
: null;
|
|
155
|
+
|
|
156
|
+
if (!existing) {
|
|
157
|
+
return response.status(404).json({
|
|
158
|
+
success: false,
|
|
159
|
+
error: "No boost found for this post",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { Announce, Undo } = await import("@fedify/fedify");
|
|
164
|
+
const handle = plugin.options.actor.handle;
|
|
165
|
+
const ctx = plugin._federation.createContext(
|
|
166
|
+
new URL(plugin._publicationUrl),
|
|
167
|
+
{ handle, publicationUrl: plugin._publicationUrl },
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Construct Undo(Announce)
|
|
171
|
+
const announce = new Announce({
|
|
172
|
+
id: existing.activityId ? new URL(existing.activityId) : undefined,
|
|
173
|
+
actor: ctx.getActorUri(handle),
|
|
174
|
+
object: new URL(url),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const undo = new Undo({
|
|
178
|
+
actor: ctx.getActorUri(handle),
|
|
179
|
+
object: announce,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Send to followers
|
|
183
|
+
await ctx.sendActivity({ identifier: handle }, "followers", undo, {
|
|
184
|
+
preferSharedInbox: true,
|
|
185
|
+
orderingKey: url,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Remove the interaction record
|
|
189
|
+
if (interactions) {
|
|
190
|
+
await interactions.deleteOne({ objectUrl: url, type: "boost" });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.info(`[ActivityPub] Sent Undo(Announce) for ${url}`);
|
|
194
|
+
|
|
195
|
+
return response.json({
|
|
196
|
+
success: true,
|
|
197
|
+
type: "unboost",
|
|
198
|
+
objectUrl: url,
|
|
199
|
+
});
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error("[ActivityPub] Unboost failed:", error.message);
|
|
202
|
+
return response.status(500).json({
|
|
203
|
+
success: false,
|
|
204
|
+
error: "Unboost failed. Please try again later.",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Like/Unlike interaction controllers.
|
|
3
|
+
* Sends Like and Undo(Like) activities via Fedify.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { validateToken } from "../csrf.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST /admin/reader/like — send a Like activity to the post author.
|
|
10
|
+
* @param {string} mountPath - Plugin mount path
|
|
11
|
+
* @param {object} plugin - ActivityPub plugin instance (for federation access)
|
|
12
|
+
*/
|
|
13
|
+
export function likeController(mountPath, plugin) {
|
|
14
|
+
return async (request, response, next) => {
|
|
15
|
+
try {
|
|
16
|
+
if (!validateToken(request)) {
|
|
17
|
+
return response.status(403).json({
|
|
18
|
+
success: false,
|
|
19
|
+
error: "Invalid CSRF token",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { url } = request.body;
|
|
24
|
+
|
|
25
|
+
if (!url) {
|
|
26
|
+
return response.status(400).json({
|
|
27
|
+
success: false,
|
|
28
|
+
error: "Missing post URL",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!plugin._federation) {
|
|
33
|
+
return response.status(503).json({
|
|
34
|
+
success: false,
|
|
35
|
+
error: "Federation not initialized",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { Like } = await import("@fedify/fedify");
|
|
40
|
+
const handle = plugin.options.actor.handle;
|
|
41
|
+
const ctx = plugin._federation.createContext(
|
|
42
|
+
new URL(plugin._publicationUrl),
|
|
43
|
+
{ handle, publicationUrl: plugin._publicationUrl },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Look up the remote post to find its author
|
|
47
|
+
const remoteObject = await ctx.lookupObject(new URL(url));
|
|
48
|
+
|
|
49
|
+
if (!remoteObject) {
|
|
50
|
+
return response.status(404).json({
|
|
51
|
+
success: false,
|
|
52
|
+
error: "Could not resolve remote post",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Get the post author for delivery
|
|
57
|
+
let recipient = null;
|
|
58
|
+
|
|
59
|
+
if (typeof remoteObject.getAttributedTo === "function") {
|
|
60
|
+
const author = await remoteObject.getAttributedTo();
|
|
61
|
+
recipient = Array.isArray(author) ? author[0] : author;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!recipient) {
|
|
65
|
+
return response.status(404).json({
|
|
66
|
+
success: false,
|
|
67
|
+
error: "Could not resolve post author",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Generate a unique activity ID
|
|
72
|
+
const activityId = `urn:uuid:${crypto.randomUUID()}`;
|
|
73
|
+
|
|
74
|
+
// Construct and send Like activity
|
|
75
|
+
const like = new Like({
|
|
76
|
+
id: new URL(activityId),
|
|
77
|
+
actor: ctx.getActorUri(handle),
|
|
78
|
+
object: new URL(url),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await ctx.sendActivity({ identifier: handle }, recipient, like, {
|
|
82
|
+
orderingKey: url,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Track the interaction for undo
|
|
86
|
+
const { application } = request.app.locals;
|
|
87
|
+
const interactions = application?.collections?.get("ap_interactions");
|
|
88
|
+
|
|
89
|
+
if (interactions) {
|
|
90
|
+
await interactions.updateOne(
|
|
91
|
+
{ objectUrl: url, type: "like" },
|
|
92
|
+
{
|
|
93
|
+
$set: {
|
|
94
|
+
objectUrl: url,
|
|
95
|
+
type: "like",
|
|
96
|
+
activityId,
|
|
97
|
+
recipientUrl: recipient.id?.href || "",
|
|
98
|
+
createdAt: new Date().toISOString(),
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{ upsert: true },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.info(`[ActivityPub] Sent Like for ${url}`);
|
|
106
|
+
|
|
107
|
+
return response.json({
|
|
108
|
+
success: true,
|
|
109
|
+
type: "like",
|
|
110
|
+
objectUrl: url,
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error("[ActivityPub] Like failed:", error.message);
|
|
114
|
+
return response.status(500).json({
|
|
115
|
+
success: false,
|
|
116
|
+
error: "Like failed. Please try again later.",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* POST /admin/reader/unlike — send an Undo(Like) activity.
|
|
124
|
+
*/
|
|
125
|
+
export function unlikeController(mountPath, plugin) {
|
|
126
|
+
return async (request, response, next) => {
|
|
127
|
+
try {
|
|
128
|
+
if (!validateToken(request)) {
|
|
129
|
+
return response.status(403).json({
|
|
130
|
+
success: false,
|
|
131
|
+
error: "Invalid CSRF token",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { url } = request.body;
|
|
136
|
+
|
|
137
|
+
if (!url) {
|
|
138
|
+
return response.status(400).json({
|
|
139
|
+
success: false,
|
|
140
|
+
error: "Missing post URL",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!plugin._federation) {
|
|
145
|
+
return response.status(503).json({
|
|
146
|
+
success: false,
|
|
147
|
+
error: "Federation not initialized",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { application } = request.app.locals;
|
|
152
|
+
const interactions = application?.collections?.get("ap_interactions");
|
|
153
|
+
|
|
154
|
+
// Look up the original interaction to get the activity ID
|
|
155
|
+
const existing = interactions
|
|
156
|
+
? await interactions.findOne({ objectUrl: url, type: "like" })
|
|
157
|
+
: null;
|
|
158
|
+
|
|
159
|
+
if (!existing) {
|
|
160
|
+
return response.status(404).json({
|
|
161
|
+
success: false,
|
|
162
|
+
error: "No like found for this post",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { Like, Undo } = await import("@fedify/fedify");
|
|
167
|
+
const handle = plugin.options.actor.handle;
|
|
168
|
+
const ctx = plugin._federation.createContext(
|
|
169
|
+
new URL(plugin._publicationUrl),
|
|
170
|
+
{ handle, publicationUrl: plugin._publicationUrl },
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Resolve the recipient
|
|
174
|
+
const remoteObject = await ctx.lookupObject(new URL(url));
|
|
175
|
+
let recipient = null;
|
|
176
|
+
|
|
177
|
+
if (remoteObject && typeof remoteObject.getAttributedTo === "function") {
|
|
178
|
+
const author = await remoteObject.getAttributedTo();
|
|
179
|
+
recipient = Array.isArray(author) ? author[0] : author;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!recipient) {
|
|
183
|
+
// Clean up the local record even if we can't send Undo
|
|
184
|
+
if (interactions) {
|
|
185
|
+
await interactions.deleteOne({ objectUrl: url, type: "like" });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return response.json({
|
|
189
|
+
success: true,
|
|
190
|
+
type: "unlike",
|
|
191
|
+
objectUrl: url,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Construct Undo(Like)
|
|
196
|
+
const like = new Like({
|
|
197
|
+
id: existing.activityId ? new URL(existing.activityId) : undefined,
|
|
198
|
+
actor: ctx.getActorUri(handle),
|
|
199
|
+
object: new URL(url),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const undo = new Undo({
|
|
203
|
+
actor: ctx.getActorUri(handle),
|
|
204
|
+
object: like,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await ctx.sendActivity({ identifier: handle }, recipient, undo, {
|
|
208
|
+
orderingKey: url,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Remove the interaction record
|
|
212
|
+
if (interactions) {
|
|
213
|
+
await interactions.deleteOne({ objectUrl: url, type: "like" });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.info(`[ActivityPub] Sent Undo(Like) for ${url}`);
|
|
217
|
+
|
|
218
|
+
return response.json({
|
|
219
|
+
success: true,
|
|
220
|
+
type: "unlike",
|
|
221
|
+
objectUrl: url,
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error("[ActivityPub] Unlike failed:", error.message);
|
|
225
|
+
return response.status(500).json({
|
|
226
|
+
success: false,
|
|
227
|
+
error: "Unlike failed. Please try again later.",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction controllers — Like, Unlike, Boost, Unboost.
|
|
3
|
+
* Re-exports from split modules for backward compatibility with index.js imports.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { likeController, unlikeController } from "./interactions-like.js";
|
|
7
|
+
export { boostController, unboostController } from "./interactions-boost.js";
|