import { ACCOUNT, ACTOR, HOSTNAME, PUBLIC_KEY } from "./env" import * as db from "./db" import { reqIsActivityPub, send, verify } from "./request" import outbox from "./outbox" import inbox from "./inbox" export default (req: Request): Response | Promise | undefined => { const url = new URL(req.url) let match if(req.method === "GET" && url.pathname === "/test") return new Response("", { status: 204 }) // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/?$/i))) return getActor(req, match[1]) // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return getOutbox(req, match[1]) // else if(req.method == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/inbox\/?$/i))) return postInbox(req, match[1]) // else if(req.method == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return postOutbox(req, match[1]) // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/followers\/?$/i))) return getFollowers(req, match[1]) // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/following\/?$/i))) return getFollowing(req, match[1]) // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/posts\/([^\/]+)\/?$/i))) return getPost(req, match[1], match[2]) // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/posts\/([^\/]+)\/activity\/?$/i))) return getActivity(req, match[1], match[2]) else if(req.method == "GET" && (match = url.pathname.match(/^\/?$/i))) return getActor(req, ACCOUNT) else if(req.method == "GET" && (match = url.pathname.match(/^\/outbox\/?$/i))) return getOutbox(req, ACCOUNT) else if(req.method == "POST" && (match = url.pathname.match(/^\/inbox\/?$/i))) return postInbox(req, ACCOUNT) else if(req.method == "POST" && (match = url.pathname.match(/^\/outbox\/?$/i))) return postOutbox(req, ACCOUNT) else if(req.method == "GET" && (match = url.pathname.match(/^\/followers\/?$/i))) return getFollowers(req, ACCOUNT) else if(req.method == "GET" && (match = url.pathname.match(/^\/following\/?$/i))) return getFollowing(req, ACCOUNT) else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/?$/i))) return getPost(req, ACCOUNT, match[1]) else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/activity\/?$/i))) return getActivity(req, ACCOUNT, match[1]) return undefined } export function idsFromValue(value:any):string[] { if (!value) return [] else if (typeof value === 'string') return [value] else if (value.id) return [value.id] else if (Array.isArray(value)) return value.map(v => idsFromValue(v)).flat(Infinity) as string[] return [] } const postOutbox = async (req:Request, account:string):Promise => { console.log("PostOutbox", account) if (ACCOUNT !== account) return new Response("", { status: 404 }) const bodyText = await req.text() // TODO: verify calls to the outbox, whether that be by basic authentication, bearer, or otherwise. const body = JSON.parse(bodyText) // ensure that the verified actor matches the actor in the request body if (ACTOR !== body.actor) return new Response("", { status: 401 }) return await outbox(body) } const postInbox = async (req:Request, account:string):Promise => { console.log("PostInbox", account) if (ACCOUNT !== account) return new Response("", { status: 404 }) const bodyText = await req.text() /** If the request successfully verifies against the public key, `from` is the actor who sent it. */ let from = ""; try { // verify the signed HTTP request from = await verify(req, bodyText); } catch (err) { return new Response("", { status: 401 }) } const body = JSON.parse(bodyText) // ensure that the verified actor matches the actor in the request body if (from !== body.actor) return new Response("", { status: 401 }) return await inbox(body) // TODO: add support for more types! we want replies, likes, boosts, etc! // switch (body.type) { // case "Follow": await follow(body); // case "Undo": await undo(body); // case "Accept": await accept(body); // case "Reject": await reject(body); // } // return new Response("", { status: 204 }) } const getOutbox = async (req:Request, account:string):Promise => { console.log("GetOutbox", account) if (ACCOUNT !== account) return new Response("", { status: 404 }) // TODO: Paging? const posts = await db.listOutboxActivities() return Response.json({ "@context": "https://www.w3.org/ns/activitystreams", id: `${ACTOR}/outbox`, type: "OrderedCollection", totalItems: posts.length, orderedItems: posts.map((post) => ({ ...post, actor: ACTOR })).sort( (a,b) => new Date(b.published).getTime() - new Date(a.published).getTime()) }, { headers: { "Content-Type": "application/activity+json"} }) } const getFollowers = async (req:Request, account:String):Promise => { console.log("GetFollowers", account) if (ACCOUNT !== account) return new Response("", { status: 404 }) const url = new URL(req.url) const page = url.searchParams.get("page") const followers = await db.listFollowers() if(!page) return Response.json({ "@context": "https://www.w3.org/ns/activitystreams", id: `${ACTOR}/followers`, type: "OrderedCollection", totalItems: followers.length, first: `${ACTOR}/followers?page=1`, }) else return Response.json({ "@context": "https://www.w3.org/ns/activitystreams", id: `${ACTOR}/followers?page=${page}`, type: "OrderedCollectionPage", partOf: `${ACTOR}/followers`, totalItems: followers.length, orderedItems: followers.map(follower => follower.actor) }) } const getFollowing = async (req:Request, account:String):Promise => { console.log("GetFollowing", account) if (ACCOUNT !== account) return new Response("", { status: 404 }) const url = new URL(req.url) const page = url.searchParams.get("page") const following = await db.listFollowing() if(!page) return Response.json({ "@context": "https://www.w3.org/ns/activitystreams", id: `${ACTOR}/following`, type: "OrderedCollection", totalItems: following.length, first: `${ACTOR}/following?page=1`, }) else return Response.json({ "@context": "https://www.w3.org/ns/activitystreams", id: `${ACTOR}/following?page=${page}`, type: "OrderedCollectionPage", partOf: `${ACTOR}/following`, totalItems: following.length, orderedItems: following.map(follow => follow.actor) }) } const getActor = async (req:Request, account:string):Promise => { console.log("GetActor", account) if (ACCOUNT !== account) return new Response("", { status: 404 }) if(reqIsActivityPub(req)) return Response.json({ "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", ], id: ACTOR, type: "Person", preferredUsername: ACCOUNT, url: ACTOR, manuallyApprovesFollowers: false, discoverable: true, published: "2023-09-14T00:00:00Z", inbox: `${ACTOR}/inbox`, outbox: `${ACTOR}/outbox`, followers: `${ACTOR}/followers`, following: `${ACTOR}/following`, publicKey: { id: `${ACTOR}#main-key`, owner: ACTOR, publicKeyPem: PUBLIC_KEY, }, }, { headers: { "Content-Type": "application/activity+json"}}) else return Response.json(await db.listPosts()) } const getPost = async (req:Request, account:string, id:string):Promise => { console.log("GetPost", account, id) if (ACCOUNT !== account) return new Response("", { status: 404 }) if(reqIsActivityPub(req)) return Response.json((await db.getActivity(id)).object, { headers: { "Content-Type": "application/activity+json"}}) else return Response.json(await db.getPost(id)) } const getActivity = async (req:Request, account:string, id:string):Promise => { console.log("GetActivity", account, id) if (ACCOUNT !== account) return new Response("", { status: 404 }) return Response.json((await db.getActivity(id)), { headers: { "Content-Type": "application/activity+json"}}) }