@rmdes/indiekit-endpoint-activitypub 3.10.6 → 3.11.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 +18 -0
- package/lib/controllers/compose.js +17 -1
- package/lib/init-indexes.js +32 -0
- package/lib/mastodon/entities/status.js +1 -1
- package/lib/mastodon/router.js +2 -0
- package/lib/mastodon/routes/accounts.js +95 -0
- package/lib/mastodon/routes/filters.js +216 -0
- package/lib/mastodon/routes/media.js +235 -21
- package/lib/mastodon/routes/statuses.js +351 -9
- package/lib/mastodon/routes/stubs.js +104 -10
- package/package.json +3 -2
- package/views/activitypub-compose.njk +34 -6
package/index.js
CHANGED
|
@@ -959,6 +959,15 @@ export default class ActivityPubEndpoint {
|
|
|
959
959
|
Indiekit.addCollection("ap_markers");
|
|
960
960
|
// Tombstones for soft-deleted posts (FEP-4f05)
|
|
961
961
|
Indiekit.addCollection("ap_tombstones");
|
|
962
|
+
// Media attachments (Mastodon API upload)
|
|
963
|
+
Indiekit.addCollection("ap_media");
|
|
964
|
+
// Status edit history
|
|
965
|
+
Indiekit.addCollection("ap_status_edits");
|
|
966
|
+
// Idempotency keys for Mastodon API
|
|
967
|
+
Indiekit.addCollection("ap_idempotency");
|
|
968
|
+
// Filters and filter keywords
|
|
969
|
+
Indiekit.addCollection("ap_filters");
|
|
970
|
+
Indiekit.addCollection("ap_filter_keywords");
|
|
962
971
|
|
|
963
972
|
// Store collection references (posts resolved lazily)
|
|
964
973
|
const indiekitCollections = Indiekit.collections;
|
|
@@ -997,6 +1006,15 @@ export default class ActivityPubEndpoint {
|
|
|
997
1006
|
ap_oauth_tokens: indiekitCollections.get("ap_oauth_tokens"),
|
|
998
1007
|
ap_markers: indiekitCollections.get("ap_markers"),
|
|
999
1008
|
ap_tombstones: indiekitCollections.get("ap_tombstones"),
|
|
1009
|
+
// Media attachments (Mastodon API upload)
|
|
1010
|
+
ap_media: indiekitCollections.get("ap_media"),
|
|
1011
|
+
// Status edit history
|
|
1012
|
+
ap_status_edits: indiekitCollections.get("ap_status_edits"),
|
|
1013
|
+
// Idempotency keys for Mastodon API
|
|
1014
|
+
ap_idempotency: indiekitCollections.get("ap_idempotency"),
|
|
1015
|
+
// Filters and filter keywords
|
|
1016
|
+
ap_filters: indiekitCollections.get("ap_filters"),
|
|
1017
|
+
ap_filter_keywords: indiekitCollections.get("ap_filter_keywords"),
|
|
1000
1018
|
get posts() {
|
|
1001
1019
|
return indiekitCollections.get("posts");
|
|
1002
1020
|
},
|
|
@@ -144,6 +144,7 @@ export function composeController(mountPath, plugin) {
|
|
|
144
144
|
syndicationTargets,
|
|
145
145
|
csrfToken,
|
|
146
146
|
mountPath,
|
|
147
|
+
mediaEndpoint: application.mediaEndpoint || "",
|
|
147
148
|
});
|
|
148
149
|
} catch (error) {
|
|
149
150
|
next(error);
|
|
@@ -167,7 +168,7 @@ export function submitComposeController(mountPath, plugin) {
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
const { application } = request.app.locals;
|
|
170
|
-
const { content, visibility, summary } = request.body;
|
|
171
|
+
const { content, visibility, summary, photo, category } = request.body;
|
|
171
172
|
const cwEnabled = request.body["cw-enabled"];
|
|
172
173
|
const inReplyTo = request.body["in-reply-to"];
|
|
173
174
|
const syndicateTo = request.body["mp-syndicate-to"];
|
|
@@ -228,6 +229,21 @@ export function submitComposeController(mountPath, plugin) {
|
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
// Photo (from file-input component — already a URL from media endpoint)
|
|
233
|
+
if (photo && photo.trim()) {
|
|
234
|
+
micropubData.append("photo", photo.trim());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Tags / categories
|
|
238
|
+
if (category) {
|
|
239
|
+
const tags = Array.isArray(category)
|
|
240
|
+
? category
|
|
241
|
+
: category.split(",").map((t) => t.trim()).filter(Boolean);
|
|
242
|
+
for (const tag of tags) {
|
|
243
|
+
micropubData.append("category[]", tag);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
231
247
|
console.info(
|
|
232
248
|
`[ActivityPub] Compose Micropub submission:`,
|
|
233
249
|
JSON.stringify({
|
package/lib/init-indexes.js
CHANGED
|
@@ -250,6 +250,38 @@ export function createIndexes(collections, options) {
|
|
|
250
250
|
{ url: 1 },
|
|
251
251
|
{ unique: true, background: true },
|
|
252
252
|
);
|
|
253
|
+
|
|
254
|
+
// Media attachments (Mastodon API upload)
|
|
255
|
+
collections.ap_media?.createIndex(
|
|
256
|
+
{ createdAt: 1 },
|
|
257
|
+
{ expireAfterSeconds: 86400, background: true },
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Status edit history
|
|
261
|
+
collections.ap_status_edits?.createIndex(
|
|
262
|
+
{ statusId: 1, editedAt: 1 },
|
|
263
|
+
{ background: true },
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Idempotency keys (auto-expire after 1 hour)
|
|
267
|
+
collections.ap_idempotency?.createIndex(
|
|
268
|
+
{ key: 1 },
|
|
269
|
+
{ unique: true, background: true },
|
|
270
|
+
);
|
|
271
|
+
collections.ap_idempotency?.createIndex(
|
|
272
|
+
{ createdAt: 1 },
|
|
273
|
+
{ expireAfterSeconds: 3600, background: true },
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Filters
|
|
277
|
+
collections.ap_filters?.createIndex(
|
|
278
|
+
{ createdAt: 1 },
|
|
279
|
+
{ background: true },
|
|
280
|
+
);
|
|
281
|
+
collections.ap_filter_keywords?.createIndex(
|
|
282
|
+
{ filterId: 1 },
|
|
283
|
+
{ background: true },
|
|
284
|
+
);
|
|
253
285
|
} catch {
|
|
254
286
|
// Index creation failed — collections not yet available.
|
|
255
287
|
// Indexes already exist from previous startups; non-fatal.
|
|
@@ -246,7 +246,7 @@ export function serializeStatus(item, { baseUrl, favouritedIds, rebloggedIds, bo
|
|
|
246
246
|
/**
|
|
247
247
|
* Serialize a linkPreview object as a Mastodon PreviewCard.
|
|
248
248
|
*/
|
|
249
|
-
function serializeCard(preview) {
|
|
249
|
+
export function serializeCard(preview) {
|
|
250
250
|
if (!preview) return null;
|
|
251
251
|
|
|
252
252
|
return {
|
package/lib/mastodon/router.js
CHANGED
|
@@ -20,6 +20,7 @@ import timelinesRouter from "./routes/timelines.js";
|
|
|
20
20
|
import notificationsRouter from "./routes/notifications.js";
|
|
21
21
|
import searchRouter from "./routes/search.js";
|
|
22
22
|
import mediaRouter from "./routes/media.js";
|
|
23
|
+
import filtersRouter from "./routes/filters.js";
|
|
23
24
|
import stubsRouter from "./routes/stubs.js";
|
|
24
25
|
|
|
25
26
|
// Rate limiters for different endpoint categories.
|
|
@@ -118,6 +119,7 @@ export function createMastodonRouter({ collections, pluginOptions = {} }) {
|
|
|
118
119
|
router.use(notificationsRouter);
|
|
119
120
|
router.use(searchRouter);
|
|
120
121
|
router.use(mediaRouter);
|
|
122
|
+
router.use(filtersRouter);
|
|
121
123
|
router.use(stubsRouter);
|
|
122
124
|
|
|
123
125
|
// ─── Catch-all for unimplemented endpoints ──────────────────────────────
|
|
@@ -153,6 +153,61 @@ router.get("/api/v1/accounts/lookup", async (req, res, next) => {
|
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
// ─── GET /api/v1/accounts/search ────────────────────────────────────────────
|
|
157
|
+
// Used by clients for @mention autocomplete in compose box.
|
|
158
|
+
|
|
159
|
+
router.get("/api/v1/accounts/search", tokenRequired, scopeRequired("read", "read:accounts"), async (req, res, next) => {
|
|
160
|
+
try {
|
|
161
|
+
const collections = req.app.locals.mastodonCollections;
|
|
162
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
163
|
+
const query = req.query.q?.trim();
|
|
164
|
+
const limit = Math.min(Number.parseInt(req.query.limit, 10) || 10, 40);
|
|
165
|
+
|
|
166
|
+
if (!query) {
|
|
167
|
+
return res.json([]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Escape regex special characters
|
|
171
|
+
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
172
|
+
const regex = new RegExp(escaped, "i");
|
|
173
|
+
|
|
174
|
+
const results = new Map(); // dedupe by URL
|
|
175
|
+
|
|
176
|
+
// Search followers
|
|
177
|
+
if (collections.ap_followers) {
|
|
178
|
+
const followers = await collections.ap_followers
|
|
179
|
+
.find({
|
|
180
|
+
$or: [{ name: regex }, { handle: regex }, { actorUrl: regex }],
|
|
181
|
+
})
|
|
182
|
+
.limit(limit)
|
|
183
|
+
.toArray();
|
|
184
|
+
for (const f of followers) results.set(f.actorUrl, f);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Search following
|
|
188
|
+
if (results.size < limit && collections.ap_following) {
|
|
189
|
+
const following = await collections.ap_following
|
|
190
|
+
.find({
|
|
191
|
+
$or: [{ name: regex }, { handle: regex }, { actorUrl: regex }],
|
|
192
|
+
})
|
|
193
|
+
.limit(limit - results.size)
|
|
194
|
+
.toArray();
|
|
195
|
+
for (const f of following) results.set(f.actorUrl, f);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { serializeAccount } = await import("../entities/account.js");
|
|
199
|
+
const accounts = [...results.values()]
|
|
200
|
+
.slice(0, limit)
|
|
201
|
+
.map((actor) =>
|
|
202
|
+
serializeAccount(actor, { baseUrl, isLocal: false }),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
res.json(accounts);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
next(error);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
156
211
|
// ─── GET /api/v1/accounts/relationships ──────────────────────────────────────
|
|
157
212
|
// MUST be before /accounts/:id to prevent Express matching "relationships" as :id
|
|
158
213
|
|
|
@@ -228,6 +283,46 @@ router.get("/api/v1/accounts/familiar_followers", tokenRequired, scopeRequired("
|
|
|
228
283
|
res.json(ids.map((id) => ({ id, accounts: [] })));
|
|
229
284
|
});
|
|
230
285
|
|
|
286
|
+
// ─── PATCH /api/v1/accounts/update_credentials ──────────────────────────────
|
|
287
|
+
|
|
288
|
+
router.patch("/api/v1/accounts/update_credentials", tokenRequired, scopeRequired("write", "write:accounts"), async (req, res, next) => {
|
|
289
|
+
try {
|
|
290
|
+
const collections = req.app.locals.mastodonCollections;
|
|
291
|
+
const pluginOptions = req.app.locals.mastodonPluginOptions || {};
|
|
292
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
293
|
+
|
|
294
|
+
const update = {};
|
|
295
|
+
if (req.body.display_name !== undefined) update.name = req.body.display_name;
|
|
296
|
+
if (req.body.note !== undefined) update.summary = req.body.note;
|
|
297
|
+
if (req.body.fields_attributes) {
|
|
298
|
+
update.attachments = Object.values(req.body.fields_attributes).map(
|
|
299
|
+
(f) => ({
|
|
300
|
+
name: f.name,
|
|
301
|
+
value: f.value,
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (Object.keys(update).length > 0 && collections.ap_profile) {
|
|
307
|
+
await collections.ap_profile.updateOne({}, { $set: update });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Return updated credential account
|
|
311
|
+
const profile = collections.ap_profile
|
|
312
|
+
? await collections.ap_profile.findOne({})
|
|
313
|
+
: {};
|
|
314
|
+
|
|
315
|
+
const { serializeCredentialAccount } = await import(
|
|
316
|
+
"../entities/account.js"
|
|
317
|
+
);
|
|
318
|
+
res.json(
|
|
319
|
+
await serializeCredentialAccount(profile, { baseUrl, collections }),
|
|
320
|
+
);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
next(error);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
231
326
|
// ─── GET /api/v1/accounts/:id ────────────────────────────────────────────────
|
|
232
327
|
|
|
233
328
|
router.get("/api/v1/accounts/:id", tokenRequired, scopeRequired("read", "read:accounts"), async (req, res, next) => {
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter endpoints for Mastodon Client API v2.
|
|
3
|
+
*/
|
|
4
|
+
import express from "express";
|
|
5
|
+
import { ObjectId } from "mongodb";
|
|
6
|
+
import { tokenRequired } from "../middleware/token-required.js";
|
|
7
|
+
import { scopeRequired } from "../middleware/scope-required.js";
|
|
8
|
+
|
|
9
|
+
const router = express.Router(); // eslint-disable-line new-cap
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Serialize a filter document with its keywords.
|
|
13
|
+
*/
|
|
14
|
+
function serializeFilter(filter, keywords = []) {
|
|
15
|
+
return {
|
|
16
|
+
id: filter._id.toString(),
|
|
17
|
+
title: filter.title || "",
|
|
18
|
+
context: filter.context || [],
|
|
19
|
+
filter_action: filter.filterAction || "warn",
|
|
20
|
+
expires_at: filter.expiresAt || null,
|
|
21
|
+
keywords: keywords.map((kw) => ({
|
|
22
|
+
id: kw._id.toString(),
|
|
23
|
+
keyword: kw.keyword,
|
|
24
|
+
whole_word: kw.wholeWord ?? true,
|
|
25
|
+
})),
|
|
26
|
+
statuses: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── GET /api/v2/filters ────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
router.get("/api/v2/filters", tokenRequired, scopeRequired("read", "read:filters"), async (req, res, next) => {
|
|
33
|
+
try {
|
|
34
|
+
const collections = req.app.locals.mastodonCollections;
|
|
35
|
+
if (!collections.ap_filters) return res.json([]);
|
|
36
|
+
|
|
37
|
+
const filters = await collections.ap_filters.find({}).toArray();
|
|
38
|
+
const result = [];
|
|
39
|
+
|
|
40
|
+
for (const filter of filters) {
|
|
41
|
+
const keywords = collections.ap_filter_keywords
|
|
42
|
+
? await collections.ap_filter_keywords
|
|
43
|
+
.find({ filterId: filter._id })
|
|
44
|
+
.toArray()
|
|
45
|
+
: [];
|
|
46
|
+
result.push(serializeFilter(filter, keywords));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
res.json(result);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
next(error);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ─── POST /api/v2/filters ───────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
router.post("/api/v2/filters", tokenRequired, scopeRequired("write", "write:filters"), async (req, res, next) => {
|
|
58
|
+
try {
|
|
59
|
+
const collections = req.app.locals.mastodonCollections;
|
|
60
|
+
if (!collections.ap_filters) {
|
|
61
|
+
return res.status(500).json({ error: "Filters not available" });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const {
|
|
65
|
+
title,
|
|
66
|
+
context,
|
|
67
|
+
filter_action: filterAction = "warn",
|
|
68
|
+
expires_in: expiresIn,
|
|
69
|
+
keywords_attributes: keywordsAttributes,
|
|
70
|
+
} = req.body;
|
|
71
|
+
|
|
72
|
+
if (!title) {
|
|
73
|
+
return res.status(422).json({ error: "title is required" });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const expiresAt = expiresIn
|
|
77
|
+
? new Date(Date.now() + Number.parseInt(expiresIn, 10) * 1000).toISOString()
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
const filterDoc = {
|
|
81
|
+
title,
|
|
82
|
+
context: Array.isArray(context) ? context : [context].filter(Boolean),
|
|
83
|
+
filterAction,
|
|
84
|
+
expiresAt,
|
|
85
|
+
createdAt: new Date().toISOString(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const result = await collections.ap_filters.insertOne(filterDoc);
|
|
89
|
+
filterDoc._id = result.insertedId;
|
|
90
|
+
|
|
91
|
+
// Insert keywords if provided
|
|
92
|
+
const keywords = [];
|
|
93
|
+
if (keywordsAttributes && collections.ap_filter_keywords) {
|
|
94
|
+
const attrs = Array.isArray(keywordsAttributes)
|
|
95
|
+
? keywordsAttributes
|
|
96
|
+
: Object.values(keywordsAttributes);
|
|
97
|
+
for (const attr of attrs) {
|
|
98
|
+
if (attr.keyword) {
|
|
99
|
+
const kwDoc = {
|
|
100
|
+
filterId: filterDoc._id,
|
|
101
|
+
keyword: attr.keyword,
|
|
102
|
+
wholeWord: attr.whole_word !== "false" && attr.whole_word !== false,
|
|
103
|
+
};
|
|
104
|
+
const kwResult = await collections.ap_filter_keywords.insertOne(kwDoc);
|
|
105
|
+
kwDoc._id = kwResult.insertedId;
|
|
106
|
+
keywords.push(kwDoc);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
res.json(serializeFilter(filterDoc, keywords));
|
|
112
|
+
} catch (error) {
|
|
113
|
+
next(error);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ─── GET /api/v2/filters/:id ────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
router.get("/api/v2/filters/:id", tokenRequired, scopeRequired("read", "read:filters"), async (req, res, next) => {
|
|
120
|
+
try {
|
|
121
|
+
const collections = req.app.locals.mastodonCollections;
|
|
122
|
+
let filter;
|
|
123
|
+
try {
|
|
124
|
+
filter = await collections.ap_filters?.findOne({
|
|
125
|
+
_id: new ObjectId(req.params.id),
|
|
126
|
+
});
|
|
127
|
+
} catch { /* invalid ObjectId */ }
|
|
128
|
+
|
|
129
|
+
if (!filter) {
|
|
130
|
+
return res.status(404).json({ error: "Record not found" });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const keywords = collections.ap_filter_keywords
|
|
134
|
+
? await collections.ap_filter_keywords
|
|
135
|
+
.find({ filterId: filter._id })
|
|
136
|
+
.toArray()
|
|
137
|
+
: [];
|
|
138
|
+
|
|
139
|
+
res.json(serializeFilter(filter, keywords));
|
|
140
|
+
} catch (error) {
|
|
141
|
+
next(error);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ─── PUT /api/v2/filters/:id ────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
router.put("/api/v2/filters/:id", tokenRequired, scopeRequired("write", "write:filters"), async (req, res, next) => {
|
|
148
|
+
try {
|
|
149
|
+
const collections = req.app.locals.mastodonCollections;
|
|
150
|
+
let filter;
|
|
151
|
+
try {
|
|
152
|
+
filter = await collections.ap_filters?.findOne({
|
|
153
|
+
_id: new ObjectId(req.params.id),
|
|
154
|
+
});
|
|
155
|
+
} catch { /* invalid ObjectId */ }
|
|
156
|
+
|
|
157
|
+
if (!filter) {
|
|
158
|
+
return res.status(404).json({ error: "Record not found" });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const update = {};
|
|
162
|
+
if (req.body.title !== undefined) update.title = req.body.title;
|
|
163
|
+
if (req.body.context !== undefined) {
|
|
164
|
+
update.context = Array.isArray(req.body.context)
|
|
165
|
+
? req.body.context
|
|
166
|
+
: [req.body.context].filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
if (req.body.filter_action !== undefined) update.filterAction = req.body.filter_action;
|
|
169
|
+
if (req.body.expires_in !== undefined) {
|
|
170
|
+
update.expiresAt = req.body.expires_in
|
|
171
|
+
? new Date(Date.now() + Number.parseInt(req.body.expires_in, 10) * 1000).toISOString()
|
|
172
|
+
: null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (Object.keys(update).length > 0) {
|
|
176
|
+
await collections.ap_filters.updateOne(
|
|
177
|
+
{ _id: filter._id },
|
|
178
|
+
{ $set: update },
|
|
179
|
+
);
|
|
180
|
+
Object.assign(filter, update);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const keywords = collections.ap_filter_keywords
|
|
184
|
+
? await collections.ap_filter_keywords
|
|
185
|
+
.find({ filterId: filter._id })
|
|
186
|
+
.toArray()
|
|
187
|
+
: [];
|
|
188
|
+
|
|
189
|
+
res.json(serializeFilter(filter, keywords));
|
|
190
|
+
} catch (error) {
|
|
191
|
+
next(error);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ─── DELETE /api/v2/filters/:id ─────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
router.delete("/api/v2/filters/:id", tokenRequired, scopeRequired("write", "write:filters"), async (req, res, next) => {
|
|
198
|
+
try {
|
|
199
|
+
const collections = req.app.locals.mastodonCollections;
|
|
200
|
+
let filterId;
|
|
201
|
+
try {
|
|
202
|
+
filterId = new ObjectId(req.params.id);
|
|
203
|
+
} catch {
|
|
204
|
+
return res.status(404).json({ error: "Record not found" });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await collections.ap_filters?.deleteOne({ _id: filterId });
|
|
208
|
+
await collections.ap_filter_keywords?.deleteMany({ filterId });
|
|
209
|
+
|
|
210
|
+
res.json({});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
next(error);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
export default router;
|