mastodon api: add GET /api/v1/accounts/:id/boosts endpoint
Returns own boosts from ap_interactions + ap_timeline as Mastodon-compatible status array with reblog wrappers. Chronicle PAI uses this to archive all own boost activity from Indiekit instances. Params: limit, since_date, max_date (ISO 8601)
This commit is contained in:
@@ -503,6 +503,119 @@ router.get("/api/v1/accounts/:id/statuses", tokenRequired, scopeRequired("read",
|
||||
}
|
||||
});
|
||||
|
||||
// ─── GET /api/v1/accounts/:id/boosts ────────────────────────────────────────
|
||||
// Chronicle PAI extension — returns own boosts from ap_interactions + ap_timeline.
|
||||
// ap_interactions stores {objectUrl, createdAt} for each own Announce activity.
|
||||
// ap_timeline stores the boosted post (with its original author).
|
||||
// Returns Mastodon-compatible status array with reblog wrapper (like /statuses).
|
||||
|
||||
router.get("/api/v1/accounts/:id/boosts", tokenRequired, scopeRequired("read", "read:statuses"), async (req, res, next) => {
|
||||
try {
|
||||
const collections = req.app.locals.mastodonCollections;
|
||||
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
||||
const limit = parseLimit(req.query.limit);
|
||||
|
||||
// Pagination and since filter
|
||||
const maxDate = req.query.max_date ? new Date(req.query.max_date) : null;
|
||||
const sinceDate = req.query.since_date ? new Date(req.query.since_date) : null;
|
||||
|
||||
// Build filter for ap_interactions
|
||||
const interactionFilter = { type: "boost" };
|
||||
if (sinceDate) interactionFilter.createdAt = { ...interactionFilter.createdAt, $gt: sinceDate.toISOString() };
|
||||
if (maxDate) interactionFilter.createdAt = { ...interactionFilter.createdAt, $lt: maxDate.toISOString() };
|
||||
|
||||
const interactions = await collections.ap_interactions
|
||||
.find(interactionFilter)
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
|
||||
if (interactions.length === 0) {
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
// Look up each boosted post in ap_timeline by objectUrl
|
||||
const objectUrls = interactions.map((i) => i.objectUrl);
|
||||
const timelineItems = await collections.ap_timeline
|
||||
.find({ uid: { $in: objectUrls } })
|
||||
.toArray();
|
||||
|
||||
// Build lookup map for O(1) access
|
||||
const timelineMap = new Map(timelineItems.map((item) => [item.uid, item]));
|
||||
|
||||
// Get own actor profile for the boost wrapper account
|
||||
const profile = await collections.ap_profile.findOne({});
|
||||
|
||||
const statuses = interactions.flatMap((interaction) => {
|
||||
const original = timelineMap.get(interaction.objectUrl);
|
||||
if (!original) return []; // post not in local timeline, skip
|
||||
|
||||
const innerStatus = serializeStatus(original, {
|
||||
baseUrl,
|
||||
favouritedIds: new Set(),
|
||||
rebloggedIds: new Set(),
|
||||
bookmarkedIds: new Set(),
|
||||
pinnedIds: new Set(),
|
||||
});
|
||||
|
||||
// Wrap in boost envelope — outer status represents the boost action
|
||||
return [{
|
||||
id: `boost-${interaction._id || interaction.objectUrl}`,
|
||||
created_at: interaction.createdAt,
|
||||
in_reply_to_id: null,
|
||||
in_reply_to_account_id: null,
|
||||
sensitive: false,
|
||||
spoiler_text: "",
|
||||
visibility: "public",
|
||||
language: null,
|
||||
uri: interaction.objectUrl,
|
||||
url: interaction.objectUrl,
|
||||
replies_count: 0,
|
||||
reblogs_count: 0,
|
||||
favourites_count: 0,
|
||||
edited_at: null,
|
||||
favourited: false,
|
||||
reblogged: true,
|
||||
muted: false,
|
||||
bookmarked: false,
|
||||
pinned: false,
|
||||
content: "",
|
||||
filtered: [],
|
||||
reblog: innerStatus,
|
||||
application: null,
|
||||
account: profile ? {
|
||||
id: profile._id?.toString() || "1",
|
||||
username: profile.preferredUsername || "user",
|
||||
acct: profile.preferredUsername || "user",
|
||||
display_name: profile.name || "",
|
||||
url: profile.url || baseUrl,
|
||||
avatar: profile.icon?.url || "",
|
||||
avatar_static: profile.icon?.url || "",
|
||||
header: "",
|
||||
header_static: "",
|
||||
note: "",
|
||||
emojis: [],
|
||||
fields: [],
|
||||
followers_count: 0,
|
||||
following_count: 0,
|
||||
statuses_count: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
} : null,
|
||||
media_attachments: [],
|
||||
mentions: [],
|
||||
tags: [],
|
||||
emojis: [],
|
||||
card: null,
|
||||
poll: null,
|
||||
}];
|
||||
});
|
||||
|
||||
res.json(statuses);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── GET /api/v1/accounts/:id/followers ─────────────────────────────────────
|
||||
|
||||
router.get("/api/v1/accounts/:id/followers", tokenRequired, scopeRequired("read", "read:follows"), async (req, res, next) => {
|
||||
|
||||
Reference in New Issue
Block a user