import { createFollowing, deleteFollowing, doActivity, getFollowing, listFollowers } from "./db" import { ACTOR, ADMIN_PASSWORD, ADMIN_USERNAME, BASE_URL } from "./env" import { send } from "./request" export default (req: Request): Response | Promise | undefined => { const url = new URL(req.url) if(!url.pathname.startsWith('/admin')) return undefined url.pathname = url.pathname.substring(6) if(ADMIN_USERNAME && ADMIN_PASSWORD && !checkAuth(req.headers)) return new Response("", { status: 401 }) let match if(req.method === "GET" && (match = url.pathname.match(/^\/test\/?$/i))) return new Response("", { status: 204 }) else if(req.method == "POST" && (match = url.pathname.match(/^\/create\/?$/i))) return create(req) else if(req.method == "POST" && (match = url.pathname.match(/^\/follow\/([^\/]+)\/?$/i))) return follow(req, match[1]) else if(req.method == "DELETE" && (match = url.pathname.match(/^\/follow\/([^\/]+)\/?$/i))) return unfollow(req, match[1]) return undefined } const checkAuth = (headers: Headers): Boolean => { // check the basic auth header const auth = headers.get("Authorization") const split = auth?.split("") if(!split || split.length != 2 || split[0] !== "Basic") return false const decoded = atob(split[1]) const [username, password] = decoded.split(":") return username === ADMIN_USERNAME && password === ADMIN_PASSWORD } // create an activity const create = async (req:Request):Promise => { const body = await req.json() // create the object, merging in supplied data const date = new Date() const id = date.getTime().toString(16) const object = { attributedTo: ACTOR, published: date.toISOString(), to: ["https://www.w3.org/ns/activitystreams#Public"], cc: [`${ACTOR}/followers`], url: `${ACTOR}/post/${id}`, id: `${ACTOR}/post/${id}`, ...body.object } const activity = { "@context": "https://www.w3.org/ns/activitystreams", id: `${ACTOR}/post/${id}/activity`, type: "Create", published: date.toISOString(), actor: ACTOR, to: ["https://www.w3.org/ns/activitystreams#Public"], cc: [`${ACTOR}/followers`], ...body, object: { ...object } } // TODO: actually create the object (and the activity??) await doActivity(activity, id) // loop through the list of followers for (const follower of await listFollowers()) { // send the activity to each follower send(ACTOR, follower.actor, { ...activity, cc: [follower.actor], }); } // return HTTP 204: no content (success) return new Response("", { status: 204 }) } const follow = async (req:Request, handle:string):Promise => { const id = BASE_URL + '@' + handle // send the follow request to the supplied actor await send(ACTOR, handle, { "@context": "https://www.w3.org/ns/activitystreams", id, type: "Follow", actor: ACTOR, object: handle, }); await createFollowing(handle, id) return new Response("", { status: 204 }) } const unfollow = async (req:Request, handle:string):Promise => { // check to see if we are already following. If not, just return success const existing = await getFollowing(handle) if (!existing) return new Response("", { status: 204 }) // TODO: send the unfollow request (technically an undo follow activity) await send(ACTOR, handle, { "@context": "https://www.w3.org/ns/activitystreams", id: existing.id + "/undo", type: "Undo", actor: ACTOR, object: { id: existing.id, type: "Follow", actor: ACTOR, object: handle, }, }); // delete the following reference from the database deleteFollowing(handle); return new Response("", { status: 204 }) }