bun-activitypub/src/admin.ts

125 lines
4.4 KiB
TypeScript
Raw Normal View History

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<Response> | 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<Response> => {
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<Response> => {
// 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<Response> => {
// 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<Response> => {
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<Response> => {
// 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
})
}