bun-activitypub/src/admin.ts

115 lines
3.7 KiB
TypeScript
Raw Normal View History

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