import { idsFromValue } from "./activitypub" import * as db from "./db" import { ADMIN_PASSWORD, ADMIN_USERNAME, activityPubTypes } from "./env" import outbox from "./outbox" import { fetchObject } from "./request" import ACTOR from "../actor" 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(/^\/rebuild\/?$/i))) return rebuild(req) 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]) else if(req.method == "POST" && (match = url.pathname.match(/^\/dislike\/(.+)\/?$/i))) return dislike(req, match[1]) else if(req.method == "DELETE" && (match = url.pathname.match(/^\/dislike\/(.+)\/?$/i))) return undislike(req, match[1]) else if(req.method == "POST" && (match = url.pathname.match(/^\/share\/(.+)\/?$/i))) return share(req, match[1]) else if(req.method == "DELETE" && (match = url.pathname.match(/^\/share\/(.+)\/?$/i))) return unshare(req, match[1]) else if(req.method == "POST" && (match = url.pathname.match(/^\/reply\/(.+)\/?$/i))) return create(req, match[1]) else if(req.method == "DELETE" && (match = url.pathname.match(/^\/delete\/(.+)\/?$/i))) return deletePost(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 } // rebuild the 11ty static pages export const rebuild = async(req:Request):Promise => { await db.rebuild() return new Response("", { status: 201 }) } // create an activity const create = async (req:Request, inReplyTo:string|null = null):Promise => { const body = await req.json() if(!inReplyTo && body.object.inReplyTo) inReplyTo = body.object.inReplyTo const original = inReplyTo ? await (await fetchObject(inReplyTo)).json() : null // create the object, merging in supplied data const date = new Date() const object = { attributedTo: ACTOR.id, published: date.toISOString(), to: ["https://www.w3.org/ns/activitystreams#Public"], cc: [ACTOR.followers], ...body.object } if(inReplyTo){ object.inReplyTo = original || inReplyTo object.cc.push(inReplyTo) } const activity = { "@context": "https://www.w3.org/ns/activitystreams", type: "Create", published: date.toISOString(), actor: ACTOR.id, to: object.to, cc: object.cc, ...body, object: { ...object } } return await outbox(activity) } const follow = async (req:Request, handle:string):Promise => { let url if(handle.startsWith('@')) handle = handle.substring(1) try { url = new URL(handle).href } catch { // this is not a valid url. Probably a someone@domain.tld format const [_, host] = handle.split('@') if(!host) return new Response('account not url or name@domain.tld', { status: 400 }) const res = await fetch(`https://${host}/.well-known/webfinger/?resource=acct:${handle}`, { headers: { 'accept': 'application/jrd+json'}}) const webfinger = await res.json() if(!webfinger.links) return new Response("", { status: 404 }) const links:any[] = webfinger.links const actorLink = links.find(l => l.rel === "self" && (activityPubTypes.includes(l.type))) if(!actorLink) return new Response("", { status: 404 }) url = actorLink.href } console.log(`Following ${url}`) // send the follow request to the supplied actor return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Follow", actor: ACTOR.id, object: url, to: [url, "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 db.getFollowing(handle) if (!existing) return new Response("", { status: 204 }) const activity = await db.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.id, object: activity, to: activity.to }) } const like = async (req:Request, object_url:string):Promise => { const object = await (await fetchObject(object_url)).json() return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Like", actor: ACTOR.id, object: object, to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"], cc: [ACTOR.followers] }) } 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 db.listLiked() let existing = liked.find(o => o.object_id === object_id) if (!existing){ const object = await (await fetchObject(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 db.getOutboxActivity(existing.id) return undo(activity) } const dislike = async (req:Request, object_url:string):Promise => { const object = await (await fetchObject(object_url)).json() return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Dislike", actor: ACTOR.id, object: object, to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"], cc: [ACTOR.followers] }) } const undislike = async (req:Request, object_id:string):Promise => { // check to see if we are already following. If not, just return success const disliked = await db.listDisliked() let existing = disliked.find(o => o.object_id === object_id) if (!existing){ const object = await (await fetchObject(object_id)).json() idsFromValue(object).forEach(id => { const e = disliked.find(o => o.object_id === id) if(e) existing = e }) } if (!existing) return new Response("No dislike found to delete", { status: 204 }) const activity = await db.getOutboxActivity(existing.id) return undo(activity) } const share = async (req:Request, object_url:string):Promise => { const object = await (await fetchObject(object_url)).json() return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Announce", actor: ACTOR.id, object: object, to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"], cc: [ACTOR.followers] }) } const unshare = async (req:Request, object_id:string):Promise => { // check to see if we are already following. If not, just return success const shared = await db.listShared() let existing = shared.find(o => o.object_id === object_id) if (!existing){ const object = await (await fetchObject(object_id)).json() idsFromValue(object).forEach(id => { const e = shared.find(o => o.object_id === id) if(e) existing = e }) } if (!existing) return new Response("No share found to delete", { status: 204 }) const activity = await db.getOutboxActivity(existing.id) return undo(activity) } const undo = async(activity:any):Promise => { // outbox will also take care of the deletion return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Undo", actor: ACTOR.id, object: activity, to: activity.to, cc: activity.cc }) } const deletePost = async (req:Request, id:string):Promise => { const post = await db.getPostByURL(id) if(!post) return new Response("", { status: 404 }) const activity = await db.getOutboxActivity(post.local_id) return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Delete", actor: ACTOR.id, to: activity.to, cc: activity.cc, audience: activity.audience, object: { id, type: "Tombstone" } }) }