/** * Boost/Unboost interaction controllers. * Sends Announce and Undo(Announce) activities via Fedify. */ import { validateToken } from "../csrf.js"; import { resolveAuthor } from "../resolve-author.js"; import { createContext, getHandle, getPublicationUrl, isFederationReady } from "../federation-actions.js"; /** * POST /admin/reader/boost — send an Announce activity to followers. */ export function boostController(mountPath, plugin) { return async (request, response, next) => { try { if (!validateToken(request)) { return response.status(403).json({ success: false, error: "Invalid CSRF token", }); } const { url } = request.body; if (!url) { return response.status(400).json({ success: false, error: "Missing post URL", }); } if (!isFederationReady(plugin)) { return response.status(503).json({ success: false, error: "Federation not initialized", }); } const { Announce } = await import("@fedify/fedify/vocab"); const handle = getHandle(plugin); const ctx = createContext(plugin); const uuid = crypto.randomUUID(); const baseUrl = getPublicationUrl(plugin).replace(/\/$/, ""); const activityId = `${baseUrl}/activitypub/boosts/${uuid}`; const publicAddress = new URL( "https://www.w3.org/ns/activitystreams#Public", ); const followersUri = ctx.getFollowersUri(handle); // Construct Announce activity const announce = new Announce({ id: new URL(activityId), actor: ctx.getActorUri(handle), object: new URL(url), to: publicAddress, cc: followersUri, }); // Send to followers via shared inbox await ctx.sendActivity({ identifier: handle }, "followers", announce, { preferSharedInbox: true, syncCollection: true, orderingKey: url, }); // Also send directly to the original post author const documentLoader = await ctx.getDocumentLoader({ identifier: handle, }); const { application } = request.app.locals; const rsaKey = await plugin._loadRsaPrivateKey(); const recipient = await resolveAuthor( url, ctx, documentLoader, application?.collections, { privateKey: rsaKey, keyId: `${ctx.getActorUri(handle).href}#main-key`, }, ); if (recipient) { try { await ctx.sendActivity( { identifier: handle }, recipient, announce, { orderingKey: url }, ); console.info( `[ActivityPub] Sent boost directly to ${recipient.id?.href || "author"}`, ); } catch (error) { console.warn( `[ActivityPub] Direct boost delivery to author failed:`, error.message, ); } } // Track the interaction const interactions = application?.collections?.get("ap_interactions"); if (interactions) { await interactions.updateOne( { objectUrl: url, type: "boost" }, { $set: { objectUrl: url, type: "boost", activityId, createdAt: new Date().toISOString(), }, }, { upsert: true }, ); } console.info(`[ActivityPub] Sent Announce (boost) for ${url}`); return response.json({ success: true, type: "boost", objectUrl: url, }); } catch (error) { console.error("[ActivityPub] Boost failed:", error.message); return response.status(500).json({ success: false, error: "Boost failed. Please try again later.", }); } }; } /** * POST /admin/reader/unboost — send an Undo(Announce) to followers. */ export function unboostController(mountPath, plugin) { return async (request, response, next) => { try { if (!validateToken(request)) { return response.status(403).json({ success: false, error: "Invalid CSRF token", }); } const { url } = request.body; if (!url) { return response.status(400).json({ success: false, error: "Missing post URL", }); } if (!isFederationReady(plugin)) { return response.status(503).json({ success: false, error: "Federation not initialized", }); } const { application } = request.app.locals; const interactions = application?.collections?.get("ap_interactions"); const existing = interactions ? await interactions.findOne({ objectUrl: url, type: "boost" }) : null; if (!existing) { return response.status(404).json({ success: false, error: "No boost found for this post", }); } const { Announce, Undo } = await import("@fedify/fedify/vocab"); const handle = getHandle(plugin); const ctx = createContext(plugin); // Construct Undo(Announce) const announce = new Announce({ id: existing.activityId ? new URL(existing.activityId) : undefined, actor: ctx.getActorUri(handle), object: new URL(url), }); const undo = new Undo({ actor: ctx.getActorUri(handle), object: announce, }); // Send to followers await ctx.sendActivity({ identifier: handle }, "followers", undo, { preferSharedInbox: true, syncCollection: true, orderingKey: url, }); // Remove the interaction record if (interactions) { await interactions.deleteOne({ objectUrl: url, type: "boost" }); } console.info(`[ActivityPub] Sent Undo(Announce) for ${url}`); return response.json({ success: true, type: "unboost", objectUrl: url, }); } catch (error) { console.error("[ActivityPub] Unboost failed:", error.message); return response.status(500).json({ success: false, error: "Unboost failed. Please try again later.", }); } }; }