diff --git a/api/activitypub/actor.ts b/api/activitypub/actor.ts index 27b82cb..3be2804 100644 --- a/api/activitypub/actor.ts +++ b/api/activitypub/actor.ts @@ -1,6 +1,6 @@ import type { VercelRequest, VercelResponse } from '@vercel/node'; -export default function (req: VercelRequest, res: VercelResponse) { +export default async function (req: VercelRequest, res: VercelResponse) { const { headers } = req; if ("accept" in headers) { @@ -13,12 +13,13 @@ export default function (req: VercelRequest, res: VercelResponse) { res.statusCode = 200; res.setHeader("Content-Type", `application/activity+json`); res.json({ - "@context": ["https://www.w3.org/ns/activitystreams", { "@language": "en- US" }], + "@context": ["https://www.w3.org/ns/activitystreams", { "@language": "en-US" }], "type": "Person", "id": "https://coderrrrr.site/coder", "outbox": "https://coderrrrr.site/outbox", "following": "https://coderrrrr.site/following", "followers": "https://coderrrrr.site/followers", + "sharedInbox": "https://coderrrrr.site/sharedInbox" "inbox": "https://coderrrrr.site/inbox", "preferredUsername": "coder", "name": "Deva Midhun's blog", diff --git a/api/activitypub/authorize_interaction.ts b/api/activitypub/authorize_interaction.ts index 0ca4e5d..4ffa75a 100644 --- a/api/activitypub/authorize_interaction.ts +++ b/api/activitypub/authorize_interaction.ts @@ -1,6 +1,6 @@ import type { VercelRequest, VercelResponse } from '@vercel/node'; -export default function (req: VercelRequest, res: VercelResponse) { +export default async function (req: VercelRequest, res: VercelResponse) { res.statusCode = 200; res.setHeader("Content-Type", `application/jrd+json`); res.end('ok'); diff --git a/api/activitypub/following.ts b/api/activitypub/following.ts index 6411324..a5e5066 100644 --- a/api/activitypub/following.ts +++ b/api/activitypub/following.ts @@ -1,6 +1,6 @@ import type { VercelRequest, VercelResponse } from '@vercel/node'; -export default function (req: VercelRequest, res: VercelResponse) { +export default async function (req: VercelRequest, res: VercelResponse) { const output = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://coderrrrr.site/api/activitypub/following", diff --git a/api/activitypub/outbox.ts b/api/activitypub/outbox.ts index 5ab151c..88ff1d2 100644 --- a/api/activitypub/outbox.ts +++ b/api/activitypub/outbox.ts @@ -8,7 +8,7 @@ import { readFileSync } from 'fs'; It's a GET request. This doesn't post it to anyone's timeline. */ -export default function (req: VercelRequest, res: VercelResponse) { +export default async function (req: VercelRequest, res: VercelResponse) { // All of the outbox data is generated at build time, so just return that static file. const file = join(cwd(), 'public', 'outbox.ajson'); const stringified = readFileSync(file, 'utf8'); diff --git a/api/activitypub/sendNote.ts b/api/activitypub/sendNote.ts index 55b02dc..e3a7578 100644 --- a/api/activitypub/sendNote.ts +++ b/api/activitypub/sendNote.ts @@ -19,102 +19,73 @@ if (!admin.apps.length) { const db = admin.firestore(); -/* - Sends the latest not that hasn't yet been sent. -*/ export default async function (req: VercelRequest, res: VercelResponse) { - const { body, query, method, url, headers } = req; - - if ( - headers.authorization !== `Bearer ${process.env.CRON_SECRET}` - ) { + if (req.headers.authorization !== `Bearer ${process.env.CRON_SECRET}`) { return res.status(401).end("Unauthorized"); } - const configCollection = db.collection('config'); - const configRef = configCollection.doc("config"); + const configRef = db.collection('config').doc("config"); const config = await configRef.get(); + const lastId = config.exists ? config.data()?.lastId || "" : ""; - if (config.exists == false) { - // Config doesn't exist, make something - configRef.set({ - "lastId": "" - }); - } - - const configData = config.data(); - let lastId = ""; - if (configData != undefined) { - lastId = configData.lastId; - } - - // Get my outbox because it contains all my notes. - const outboxResponse = await fetch('https://coderrrrr.site/outbox'); - const outbox = (await outboxResponse.json()); - - const followersCollection = db.collection('followers'); - const followersQuerySnapshot = await followersCollection.get(); + // Fetch notes from outbox + const outboxResponse = await fetch("https://coderrrrr.site/api/activitypub/outbox"); + const outbox = await outboxResponse.json(); + const followersSnapshot = await db.collection('followers').get(); let lastSuccessfulSentId = ""; - for (const followerDoc of followersQuerySnapshot.docs) { + const inboxes = new Set(); // Track unique inboxes to avoid duplicate sending + + for (const followerDoc of followersSnapshot.docs) { const follower = followerDoc.data(); - try { - const actorUrl = (typeof follower.actor == "string") ? follower.actor : follower.actor.id; - console.log(`Fetching actor information for ${actorUrl}`) - const actorInformation = await fetchActorInformation(actorUrl); - if (actorInformation == undefined) { - // We can't send to this actor, so skip the actor. We should log it. - continue; - } - - if (actorInformation.inbox == undefined) { - console.log( - `Actor ${actorUrl} doesn't have an inbox, so we can't send to them. ${actorInformation}` - ); - } - const actorInbox = new URL(actorInformation.inbox.toString()); + const actorUrl = typeof follower.actor === "string" ? follower.actor : follower.actor.id; - for (const iteIdx in (outbox.orderedItems)) { - // We have to break somewhere... do it after the first. - const item = (outbox.orderedItems)[iteIdx]; - - console.log(`Checking ID ${item.id}, ${lastId}`); - if (item.id == `${lastId}`) { - lastSuccessfulSentId = item.id; - // We've already posted this, don't try and send it again. - console.log(`${item.id} has already been posted - don't attempt`) - break; - } - - if (item.object != undefined) { - // We might not need this. - item.object.published = (new Date()).toISOString(); - } - - // Item will be an entity, i.e, { Create { Note } } - try { - console.log(`Sending to ${actorInbox}`); - - const response = await sendSignedRequest(actorInbox, item); - console.log(`Send result: ${actorInbox}`, response.status, response.statusText, await response.text()); - - // It's not been sent. - lastSuccessfulSentId = item.id; // we shouldn't really set this every time. - } catch (sendSignedError) { - console.log("Error sending signed request", sendSignedError) - } - - break; // At some point we might want to post more than one post, so remove this. - } - } catch (ex) { - console.log("Error", ex); + console.log(`Fetching actor information for ${actorUrl}`); + const actorInformation = await fetchActorInformation(actorUrl); + if (!actorInformation || !actorInformation.inbox) { + console.log(`Skipping ${actorUrl}: No valid inbox`); + continue; } + + const inboxUrl = actorInformation.sharedInbox || actorInformation.inbox; + inboxes.add(inboxUrl.toString()); // Add to set to ensure unique delivery } - configRef.set({ - "lastId": lastSuccessfulSentId - }); + for (const item of outbox.orderedItems) { + if (item.id === lastId) { + console.log(`${item.id} has already been posted - skipping`); + break; + } + + if (item.object) { + item.object.published = new Date().toISOString(); + } + + for (const inboxUrl of inboxes) { + try { + console.log(`Sending to ${inboxUrl}`); + + const response = await sendSignedRequest(new URL(inboxUrl), item, { + headers: { + "Accept": "application/activity+json", + "Content-Type": "application/activity+json" + } + }); + + console.log(`Send result: ${response.status} ${response.statusText}`); + const responseText = await response.text(); + console.log("Response body:", responseText); + + lastSuccessfulSentId = item.id; + } catch (error) { + console.error(`Error sending to ${inboxUrl}:`, error); + } + } + break; // Only send the latest post for now + } + + await configRef.set({ "lastId": lastSuccessfulSentId }); res.status(200).end("ok"); }; diff --git a/vercel.json b/vercel.json index 88cc394..fe4a743 100644 --- a/vercel.json +++ b/vercel.json @@ -47,6 +47,10 @@ "source": "/inbox", "destination": "/api/activitypub/inbox.ts" }, + { + "source": "/sharedInbox", + "destination": "/api/activitypub/sharedInbox.ts" + }, { "source": "/outbox", "destination": "/api/activitypub/outbox.ts" @@ -54,7 +58,7 @@ ], "headers": [ { - "source": "/(.*).ajson", + "source": "/(.*).json", "headers": [ { "key": "content-type",