import { idsFromValue } from "./activitypub" import { getFollowing, getOutboxActivity, listLiked } from "./db" import { ACTOR, ADMIN_PASSWORD, ADMIN_USERNAME, BASE_URL } from "./env" import outbox from "./outbox" import { fetchObject } 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]) else if(req.method == "POST" && (match = url.pathname.match(/^\/like\/(.+)\/?$/i))) return like(req, match[1]) else if(req.method == "DELETE" && (match = url.pathname.match(/^\/like\/(.+)\/?$/i))) return unlike(req, match[1]) console.log(`Couldn't match admin path ${req.method} "${url.pathname}"`) 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 object = { attributedTo: ACTOR, published: date.toISOString(), to: ["https://www.w3.org/ns/activitystreams#Public"], cc: [`${ACTOR}/followers`], ...body.object } const activity = { "@context": "https://www.w3.org/ns/activitystreams", type: "Create", published: date.toISOString(), actor: ACTOR, to: ["https://www.w3.org/ns/activitystreams#Public"], cc: [`${ACTOR}/followers`], ...body, object: { ...object } } return await outbox(activity) } const follow = async (req:Request, handle:string):Promise => { // send the follow request to the supplied actor return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Follow", actor: ACTOR, object: handle, to: [handle, "https://www.w3.org/ns/activitystreams#Public"] }) } 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 }) const activity = await getOutboxActivity(existing.id) // outbox will also take care of the deletion return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Undo", actor: ACTOR, object: activity, to: activity.to }) } const like = async (req:Request, object_url:string):Promise => { const object = await (await fetchObject(ACTOR, object_url)).json() return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Like", actor: ACTOR, object: object, to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"] }) } const unlike = async (req:Request, object_id:string):Promise => { // check to see if we are already following. If not, just return success const liked = await listLiked() let existing = liked.find(o => o.object_id === object_id) if (!existing){ const object = await (await fetchObject(ACTOR, object_id)).json() idsFromValue(object).forEach(id => { const e = liked.find(o => o.object_id === id) if(e) existing = e }) } if (!existing) return new Response("No like found to delete", { status: 204 }) const activity = await getOutboxActivity(existing.id) // outbox will also take care of the deletion return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Undo", actor: ACTOR, object: activity, to: activity.to }) }