@rmdes/indiekit-endpoint-activitypub 2.15.4 → 3.2.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 +47 -0
- package/lib/mastodon/entities/account.js +200 -0
- package/lib/mastodon/entities/instance.js +1 -0
- package/lib/mastodon/entities/media.js +38 -0
- package/lib/mastodon/entities/notification.js +118 -0
- package/lib/mastodon/entities/relationship.js +38 -0
- package/lib/mastodon/entities/sanitize.js +111 -0
- package/lib/mastodon/entities/status.js +289 -0
- package/lib/mastodon/helpers/id-mapping.js +32 -0
- package/lib/mastodon/helpers/interactions.js +278 -0
- package/lib/mastodon/helpers/pagination.js +130 -0
- package/lib/mastodon/middleware/cors.js +25 -0
- package/lib/mastodon/middleware/error-handler.js +37 -0
- package/lib/mastodon/middleware/scope-required.js +86 -0
- package/lib/mastodon/middleware/token-required.js +57 -0
- package/lib/mastodon/router.js +96 -0
- package/lib/mastodon/routes/accounts.js +740 -0
- package/lib/mastodon/routes/instance.js +207 -0
- package/lib/mastodon/routes/media.js +43 -0
- package/lib/mastodon/routes/notifications.js +257 -0
- package/lib/mastodon/routes/oauth.js +545 -0
- package/lib/mastodon/routes/search.js +146 -0
- package/lib/mastodon/routes/statuses.js +605 -0
- package/lib/mastodon/routes/stubs.js +380 -0
- package/lib/mastodon/routes/timelines.js +296 -0
- package/package.json +2 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instance info endpoints for Mastodon Client API.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/v2/instance — v2 format (primary)
|
|
5
|
+
* GET /api/v1/instance — v1 format (fallback for older clients)
|
|
6
|
+
*/
|
|
7
|
+
import express from "express";
|
|
8
|
+
import { serializeAccount } from "../entities/account.js";
|
|
9
|
+
|
|
10
|
+
const router = express.Router(); // eslint-disable-line new-cap
|
|
11
|
+
|
|
12
|
+
// ─── GET /api/v2/instance ────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
router.get("/api/v2/instance", async (req, res, next) => {
|
|
15
|
+
try {
|
|
16
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
17
|
+
const domain = req.get("host");
|
|
18
|
+
const collections = req.app.locals.mastodonCollections;
|
|
19
|
+
const pluginOptions = req.app.locals.mastodonPluginOptions || {};
|
|
20
|
+
|
|
21
|
+
const profile = await collections.ap_profile.findOne({});
|
|
22
|
+
const contactAccount = profile
|
|
23
|
+
? serializeAccount(profile, {
|
|
24
|
+
baseUrl,
|
|
25
|
+
isLocal: true,
|
|
26
|
+
handle: pluginOptions.handle || "user",
|
|
27
|
+
})
|
|
28
|
+
: null;
|
|
29
|
+
|
|
30
|
+
res.json({
|
|
31
|
+
domain,
|
|
32
|
+
title: profile?.name || domain,
|
|
33
|
+
version: "4.0.0 (compatible; Indiekit ActivityPub)",
|
|
34
|
+
source_url: "https://github.com/getindiekit/indiekit",
|
|
35
|
+
description: profile?.summary || `An Indiekit instance at ${domain}`,
|
|
36
|
+
usage: {
|
|
37
|
+
users: {
|
|
38
|
+
active_month: 1,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
thumbnail: {
|
|
42
|
+
url: profile?.icon || `${baseUrl}/favicon.ico`,
|
|
43
|
+
blurhash: null,
|
|
44
|
+
versions: {},
|
|
45
|
+
},
|
|
46
|
+
icon: [],
|
|
47
|
+
languages: ["en"],
|
|
48
|
+
configuration: {
|
|
49
|
+
urls: {
|
|
50
|
+
streaming: "",
|
|
51
|
+
},
|
|
52
|
+
accounts: {
|
|
53
|
+
max_featured_tags: 10,
|
|
54
|
+
max_pinned_statuses: 10,
|
|
55
|
+
},
|
|
56
|
+
statuses: {
|
|
57
|
+
max_characters: 5000,
|
|
58
|
+
max_media_attachments: 4,
|
|
59
|
+
characters_reserved_per_url: 23,
|
|
60
|
+
},
|
|
61
|
+
media_attachments: {
|
|
62
|
+
supported_mime_types: [
|
|
63
|
+
"image/jpeg",
|
|
64
|
+
"image/png",
|
|
65
|
+
"image/gif",
|
|
66
|
+
"image/webp",
|
|
67
|
+
"video/mp4",
|
|
68
|
+
"video/webm",
|
|
69
|
+
"audio/mpeg",
|
|
70
|
+
"audio/ogg",
|
|
71
|
+
],
|
|
72
|
+
image_size_limit: 16_777_216,
|
|
73
|
+
image_matrix_limit: 16_777_216,
|
|
74
|
+
video_size_limit: 67_108_864,
|
|
75
|
+
video_frame_rate_limit: 60,
|
|
76
|
+
video_matrix_limit: 16_777_216,
|
|
77
|
+
},
|
|
78
|
+
polls: {
|
|
79
|
+
max_options: 4,
|
|
80
|
+
max_characters_per_option: 50,
|
|
81
|
+
min_expiration: 300,
|
|
82
|
+
max_expiration: 2_592_000,
|
|
83
|
+
},
|
|
84
|
+
translation: {
|
|
85
|
+
enabled: false,
|
|
86
|
+
},
|
|
87
|
+
vapid: {
|
|
88
|
+
public_key: "",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
registrations: {
|
|
92
|
+
enabled: false,
|
|
93
|
+
approval_required: true,
|
|
94
|
+
message: null,
|
|
95
|
+
url: null,
|
|
96
|
+
},
|
|
97
|
+
api_versions: {
|
|
98
|
+
mastodon: 0,
|
|
99
|
+
},
|
|
100
|
+
contact: {
|
|
101
|
+
email: "",
|
|
102
|
+
account: contactAccount,
|
|
103
|
+
},
|
|
104
|
+
rules: [],
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
next(error);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ─── GET /api/v1/instance ────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
router.get("/api/v1/instance", async (req, res, next) => {
|
|
114
|
+
try {
|
|
115
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
116
|
+
const domain = req.get("host");
|
|
117
|
+
const collections = req.app.locals.mastodonCollections;
|
|
118
|
+
const pluginOptions = req.app.locals.mastodonPluginOptions || {};
|
|
119
|
+
|
|
120
|
+
const profile = await collections.ap_profile.findOne({});
|
|
121
|
+
|
|
122
|
+
// Get approximate counts
|
|
123
|
+
let statusCount = 0;
|
|
124
|
+
let domainCount = 0;
|
|
125
|
+
try {
|
|
126
|
+
statusCount = await collections.ap_timeline.countDocuments({});
|
|
127
|
+
// Rough domain count from unique follower domains
|
|
128
|
+
const followers = await collections.ap_followers
|
|
129
|
+
.find({}, { projection: { actorUrl: 1 } })
|
|
130
|
+
.toArray();
|
|
131
|
+
const domains = new Set(
|
|
132
|
+
followers
|
|
133
|
+
.map((f) => {
|
|
134
|
+
try {
|
|
135
|
+
return new URL(f.actorUrl).hostname;
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
.filter(Boolean),
|
|
141
|
+
);
|
|
142
|
+
domainCount = domains.size;
|
|
143
|
+
} catch {
|
|
144
|
+
// Non-critical
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
res.json({
|
|
148
|
+
uri: domain,
|
|
149
|
+
title: profile?.name || domain,
|
|
150
|
+
short_description: profile?.summary || "",
|
|
151
|
+
description: profile?.summary || `An Indiekit instance at ${domain}`,
|
|
152
|
+
email: "",
|
|
153
|
+
version: "4.0.0 (compatible; Indiekit ActivityPub)",
|
|
154
|
+
urls: {
|
|
155
|
+
streaming_api: "",
|
|
156
|
+
},
|
|
157
|
+
stats: {
|
|
158
|
+
user_count: 1,
|
|
159
|
+
status_count: statusCount,
|
|
160
|
+
domain_count: domainCount,
|
|
161
|
+
},
|
|
162
|
+
thumbnail: profile?.icon || null,
|
|
163
|
+
languages: ["en"],
|
|
164
|
+
registrations: false,
|
|
165
|
+
approval_required: true,
|
|
166
|
+
invites_enabled: false,
|
|
167
|
+
configuration: {
|
|
168
|
+
statuses: {
|
|
169
|
+
max_characters: 5000,
|
|
170
|
+
max_media_attachments: 4,
|
|
171
|
+
characters_reserved_per_url: 23,
|
|
172
|
+
},
|
|
173
|
+
media_attachments: {
|
|
174
|
+
supported_mime_types: [
|
|
175
|
+
"image/jpeg",
|
|
176
|
+
"image/png",
|
|
177
|
+
"image/gif",
|
|
178
|
+
"image/webp",
|
|
179
|
+
],
|
|
180
|
+
image_size_limit: 16_777_216,
|
|
181
|
+
image_matrix_limit: 16_777_216,
|
|
182
|
+
video_size_limit: 67_108_864,
|
|
183
|
+
video_frame_rate_limit: 60,
|
|
184
|
+
video_matrix_limit: 16_777_216,
|
|
185
|
+
},
|
|
186
|
+
polls: {
|
|
187
|
+
max_options: 4,
|
|
188
|
+
max_characters_per_option: 50,
|
|
189
|
+
min_expiration: 300,
|
|
190
|
+
max_expiration: 2_592_000,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
contact_account: profile
|
|
194
|
+
? serializeAccount(profile, {
|
|
195
|
+
baseUrl,
|
|
196
|
+
isLocal: true,
|
|
197
|
+
handle: pluginOptions.handle || "user",
|
|
198
|
+
})
|
|
199
|
+
: null,
|
|
200
|
+
rules: [],
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
next(error);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
export default router;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media endpoints for Mastodon Client API.
|
|
3
|
+
*
|
|
4
|
+
* POST /api/v2/media — upload media attachment (stub — returns 422 until storage is configured)
|
|
5
|
+
* POST /api/v1/media — legacy upload endpoint (redirects to v2)
|
|
6
|
+
* GET /api/v1/media/:id — get media attachment status
|
|
7
|
+
* PUT /api/v1/media/:id — update media metadata (description/focus)
|
|
8
|
+
*/
|
|
9
|
+
import express from "express";
|
|
10
|
+
|
|
11
|
+
const router = express.Router(); // eslint-disable-line new-cap
|
|
12
|
+
|
|
13
|
+
// ─── POST /api/v2/media ─────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
router.post("/api/v2/media", (req, res) => {
|
|
16
|
+
// Media upload requires multer/multipart handling + storage backend.
|
|
17
|
+
// For now, return 422 so clients show a user-friendly error.
|
|
18
|
+
res.status(422).json({
|
|
19
|
+
error: "Media uploads are not yet supported on this server",
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// ─── POST /api/v1/media (legacy) ────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
router.post("/api/v1/media", (req, res) => {
|
|
26
|
+
res.status(422).json({
|
|
27
|
+
error: "Media uploads are not yet supported on this server",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ─── GET /api/v1/media/:id ──────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
router.get("/api/v1/media/:id", (req, res) => {
|
|
34
|
+
res.status(404).json({ error: "Record not found" });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ─── PUT /api/v1/media/:id ──────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
router.put("/api/v1/media/:id", (req, res) => {
|
|
40
|
+
res.status(404).json({ error: "Record not found" });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export default router;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification endpoints for Mastodon Client API.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/v1/notifications — list notifications with pagination
|
|
5
|
+
* GET /api/v1/notifications/:id — single notification
|
|
6
|
+
* POST /api/v1/notifications/clear — clear all notifications
|
|
7
|
+
* POST /api/v1/notifications/:id/dismiss — dismiss single notification
|
|
8
|
+
*/
|
|
9
|
+
import express from "express";
|
|
10
|
+
import { ObjectId } from "mongodb";
|
|
11
|
+
import { serializeNotification } from "../entities/notification.js";
|
|
12
|
+
import { buildPaginationQuery, parseLimit, setPaginationHeaders } from "../helpers/pagination.js";
|
|
13
|
+
|
|
14
|
+
const router = express.Router(); // eslint-disable-line new-cap
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mastodon type -> internal type reverse mapping for filtering.
|
|
18
|
+
*/
|
|
19
|
+
const REVERSE_TYPE_MAP = {
|
|
20
|
+
favourite: "like",
|
|
21
|
+
reblog: "boost",
|
|
22
|
+
follow: "follow",
|
|
23
|
+
follow_request: "follow_request",
|
|
24
|
+
mention: { $in: ["reply", "mention", "dm"] },
|
|
25
|
+
poll: "poll",
|
|
26
|
+
update: "update",
|
|
27
|
+
"admin.report": "report",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ─── GET /api/v1/notifications ──────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
router.get("/api/v1/notifications", async (req, res, next) => {
|
|
33
|
+
try {
|
|
34
|
+
const token = req.mastodonToken;
|
|
35
|
+
if (!token) {
|
|
36
|
+
return res.status(401).json({ error: "The access token is invalid" });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const collections = req.app.locals.mastodonCollections;
|
|
40
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
41
|
+
const limit = parseLimit(req.query.limit);
|
|
42
|
+
|
|
43
|
+
// Build base filter
|
|
44
|
+
const baseFilter = {};
|
|
45
|
+
|
|
46
|
+
// types[] — include only these Mastodon types
|
|
47
|
+
const includeTypes = normalizeArray(req.query["types[]"] || req.query.types);
|
|
48
|
+
if (includeTypes.length > 0) {
|
|
49
|
+
const internalTypes = resolveInternalTypes(includeTypes);
|
|
50
|
+
if (internalTypes.length > 0) {
|
|
51
|
+
baseFilter.type = { $in: internalTypes };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// exclude_types[] — exclude these Mastodon types
|
|
56
|
+
const excludeTypes = normalizeArray(req.query["exclude_types[]"] || req.query.exclude_types);
|
|
57
|
+
if (excludeTypes.length > 0) {
|
|
58
|
+
const excludeInternal = resolveInternalTypes(excludeTypes);
|
|
59
|
+
if (excludeInternal.length > 0) {
|
|
60
|
+
baseFilter.type = { ...baseFilter.type, $nin: excludeInternal };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Apply cursor pagination
|
|
65
|
+
const { filter, sort, reverse } = buildPaginationQuery(baseFilter, {
|
|
66
|
+
max_id: req.query.max_id,
|
|
67
|
+
min_id: req.query.min_id,
|
|
68
|
+
since_id: req.query.since_id,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let items = await collections.ap_notifications
|
|
72
|
+
.find(filter)
|
|
73
|
+
.sort(sort)
|
|
74
|
+
.limit(limit)
|
|
75
|
+
.toArray();
|
|
76
|
+
|
|
77
|
+
if (reverse) {
|
|
78
|
+
items.reverse();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Batch-fetch referenced timeline items to avoid N+1
|
|
82
|
+
const statusMap = await batchFetchStatuses(collections, items);
|
|
83
|
+
|
|
84
|
+
// Serialize notifications
|
|
85
|
+
const notifications = items.map((notif) =>
|
|
86
|
+
serializeNotification(notif, {
|
|
87
|
+
baseUrl,
|
|
88
|
+
statusMap,
|
|
89
|
+
interactionState: {
|
|
90
|
+
favouritedIds: new Set(),
|
|
91
|
+
rebloggedIds: new Set(),
|
|
92
|
+
bookmarkedIds: new Set(),
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
).filter(Boolean);
|
|
96
|
+
|
|
97
|
+
// Set pagination headers
|
|
98
|
+
setPaginationHeaders(res, req, items, limit);
|
|
99
|
+
|
|
100
|
+
res.json(notifications);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
next(error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ─── GET /api/v1/notifications/:id ──────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
router.get("/api/v1/notifications/:id", async (req, res, next) => {
|
|
109
|
+
try {
|
|
110
|
+
const token = req.mastodonToken;
|
|
111
|
+
if (!token) {
|
|
112
|
+
return res.status(401).json({ error: "The access token is invalid" });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const collections = req.app.locals.mastodonCollections;
|
|
116
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
117
|
+
|
|
118
|
+
let objectId;
|
|
119
|
+
try {
|
|
120
|
+
objectId = new ObjectId(req.params.id);
|
|
121
|
+
} catch {
|
|
122
|
+
return res.status(404).json({ error: "Record not found" });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const notif = await collections.ap_notifications.findOne({ _id: objectId });
|
|
126
|
+
if (!notif) {
|
|
127
|
+
return res.status(404).json({ error: "Record not found" });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const statusMap = await batchFetchStatuses(collections, [notif]);
|
|
131
|
+
|
|
132
|
+
const notification = serializeNotification(notif, {
|
|
133
|
+
baseUrl,
|
|
134
|
+
statusMap,
|
|
135
|
+
interactionState: {
|
|
136
|
+
favouritedIds: new Set(),
|
|
137
|
+
rebloggedIds: new Set(),
|
|
138
|
+
bookmarkedIds: new Set(),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
res.json(notification);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
next(error);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ─── POST /api/v1/notifications/clear ───────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
router.post("/api/v1/notifications/clear", async (req, res, next) => {
|
|
151
|
+
try {
|
|
152
|
+
const token = req.mastodonToken;
|
|
153
|
+
if (!token) {
|
|
154
|
+
return res.status(401).json({ error: "The access token is invalid" });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const collections = req.app.locals.mastodonCollections;
|
|
158
|
+
await collections.ap_notifications.deleteMany({});
|
|
159
|
+
res.json({});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
next(error);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ─── POST /api/v1/notifications/:id/dismiss ─────────────────────────────────
|
|
166
|
+
|
|
167
|
+
router.post("/api/v1/notifications/:id/dismiss", async (req, res, next) => {
|
|
168
|
+
try {
|
|
169
|
+
const token = req.mastodonToken;
|
|
170
|
+
if (!token) {
|
|
171
|
+
return res.status(401).json({ error: "The access token is invalid" });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const collections = req.app.locals.mastodonCollections;
|
|
175
|
+
|
|
176
|
+
let objectId;
|
|
177
|
+
try {
|
|
178
|
+
objectId = new ObjectId(req.params.id);
|
|
179
|
+
} catch {
|
|
180
|
+
return res.status(404).json({ error: "Record not found" });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await collections.ap_notifications.deleteOne({ _id: objectId });
|
|
184
|
+
res.json({});
|
|
185
|
+
} catch (error) {
|
|
186
|
+
next(error);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Normalize query param to array (handles string or array).
|
|
194
|
+
*/
|
|
195
|
+
function normalizeArray(param) {
|
|
196
|
+
if (!param) return [];
|
|
197
|
+
return Array.isArray(param) ? param : [param];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Convert Mastodon notification types to internal types.
|
|
202
|
+
*/
|
|
203
|
+
function resolveInternalTypes(mastodonTypes) {
|
|
204
|
+
const result = [];
|
|
205
|
+
for (const t of mastodonTypes) {
|
|
206
|
+
const mapped = REVERSE_TYPE_MAP[t];
|
|
207
|
+
if (mapped) {
|
|
208
|
+
if (mapped.$in) {
|
|
209
|
+
result.push(...mapped.$in);
|
|
210
|
+
} else {
|
|
211
|
+
result.push(mapped);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Batch-fetch timeline items referenced by notifications.
|
|
220
|
+
*
|
|
221
|
+
* @param {object} collections
|
|
222
|
+
* @param {Array} notifications
|
|
223
|
+
* @returns {Promise<Map<string, object>>} Map of targetUrl -> timeline item
|
|
224
|
+
*/
|
|
225
|
+
async function batchFetchStatuses(collections, notifications) {
|
|
226
|
+
const statusMap = new Map();
|
|
227
|
+
|
|
228
|
+
const targetUrls = [
|
|
229
|
+
...new Set(
|
|
230
|
+
notifications
|
|
231
|
+
.map((n) => n.targetUrl)
|
|
232
|
+
.filter(Boolean),
|
|
233
|
+
),
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
if (targetUrls.length === 0 || !collections.ap_timeline) {
|
|
237
|
+
return statusMap;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const items = await collections.ap_timeline
|
|
241
|
+
.find({
|
|
242
|
+
$or: [
|
|
243
|
+
{ uid: { $in: targetUrls } },
|
|
244
|
+
{ url: { $in: targetUrls } },
|
|
245
|
+
],
|
|
246
|
+
})
|
|
247
|
+
.toArray();
|
|
248
|
+
|
|
249
|
+
for (const item of items) {
|
|
250
|
+
if (item.uid) statusMap.set(item.uid, item);
|
|
251
|
+
if (item.url) statusMap.set(item.url, item);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return statusMap;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export default router;
|