bun-activitypub/src/activitypub.ts
2023-09-27 09:49:32 +10:00

202 lines
No EOL
8 KiB
TypeScript

import { ACCOUNT, ACTOR, HOSTNAME, PUBLIC_KEY } from "./env"
import * as db from "./db"
import { reqIsActivityPub, send, verify } from "./request"
import outbox from "./outbox"
import inbox from "./inbox"
export default (req: Request): Response | Promise<Response> | undefined => {
const url = new URL(req.url)
let match
if(req.method === "GET" && url.pathname === "/test") return new Response("", { status: 204 })
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/?$/i))) return getActor(req, match[1])
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return getOutbox(req, match[1])
// else if(req.method == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/inbox\/?$/i))) return postInbox(req, match[1])
// else if(req.method == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return postOutbox(req, match[1])
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/followers\/?$/i))) return getFollowers(req, match[1])
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/following\/?$/i))) return getFollowing(req, match[1])
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/posts\/([^\/]+)\/?$/i))) return getPost(req, match[1], match[2])
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/posts\/([^\/]+)\/activity\/?$/i))) return getActivity(req, match[1], match[2])
else if(req.method == "GET" && (match = url.pathname.match(/^\/?$/i))) return getActor(req, ACCOUNT)
else if(req.method == "GET" && (match = url.pathname.match(/^\/outbox\/?$/i))) return getOutbox(req, ACCOUNT)
else if(req.method == "POST" && (match = url.pathname.match(/^\/inbox\/?$/i))) return postInbox(req, ACCOUNT)
else if(req.method == "POST" && (match = url.pathname.match(/^\/outbox\/?$/i))) return postOutbox(req, ACCOUNT)
else if(req.method == "GET" && (match = url.pathname.match(/^\/followers\/?$/i))) return getFollowers(req, ACCOUNT)
else if(req.method == "GET" && (match = url.pathname.match(/^\/following\/?$/i))) return getFollowing(req, ACCOUNT)
else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/?$/i))) return getPost(req, ACCOUNT, match[1])
else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/activity\/?$/i))) return getActivity(req, ACCOUNT, match[1])
return undefined
}
export function idsFromValue(value:any):string[] {
if (!value) return []
else if (typeof value === 'string') return [value]
else if (value.id) return [value.id]
else if (Array.isArray(value)) return value.map(v => idsFromValue(v)).flat(Infinity) as string[]
return []
}
const postOutbox = async (req:Request, account:string):Promise<Response> => {
console.log("PostOutbox", account)
if (ACCOUNT !== account) return new Response("", { status: 404 })
const bodyText = await req.text()
// TODO: verify calls to the outbox, whether that be by basic authentication, bearer, or otherwise.
const body = JSON.parse(bodyText)
// ensure that the verified actor matches the actor in the request body
if (ACTOR !== body.actor) return new Response("", { status: 401 })
return await outbox(body)
}
const postInbox = async (req:Request, account:string):Promise<Response> => {
console.log("PostInbox", account)
if (ACCOUNT !== account) return new Response("", { status: 404 })
const bodyText = await req.text()
/** If the request successfully verifies against the public key, `from` is the actor who sent it. */
let from = "";
try {
// verify the signed HTTP request
from = await verify(req, bodyText);
} catch (err) {
return new Response("", { status: 401 })
}
const body = JSON.parse(bodyText)
// ensure that the verified actor matches the actor in the request body
if (from !== body.actor) return new Response("", { status: 401 })
return await inbox(body)
// TODO: add support for more types! we want replies, likes, boosts, etc!
// switch (body.type) {
// case "Follow": await follow(body);
// case "Undo": await undo(body);
// case "Accept": await accept(body);
// case "Reject": await reject(body);
// }
// return new Response("", { status: 204 })
}
const getOutbox = async (req:Request, account:string):Promise<Response> => {
console.log("GetOutbox", account)
if (ACCOUNT !== account) return new Response("", { status: 404 })
// TODO: Paging?
const posts = await db.listOutboxActivities()
return Response.json({
"@context": "https://www.w3.org/ns/activitystreams",
id: `${ACTOR}/outbox`,
type: "OrderedCollection",
totalItems: posts.length,
orderedItems: posts.map((post) => ({
...post,
actor: ACTOR
})).sort( (a,b) => new Date(b.published).getTime() - new Date(a.published).getTime())
}, { headers: { "Content-Type": "application/activity+json"} })
}
const getFollowers = async (req:Request, account:String):Promise<Response> => {
console.log("GetFollowers", account)
if (ACCOUNT !== account) return new Response("", { status: 404 })
const url = new URL(req.url)
const page = url.searchParams.get("page")
const followers = await db.listFollowers()
if(!page) return Response.json({
"@context": "https://www.w3.org/ns/activitystreams",
id: `${ACTOR}/followers`,
type: "OrderedCollection",
totalItems: followers.length,
first: `${ACTOR}/followers?page=1`,
})
else return Response.json({
"@context": "https://www.w3.org/ns/activitystreams",
id: `${ACTOR}/followers?page=${page}`,
type: "OrderedCollectionPage",
partOf: `${ACTOR}/followers`,
totalItems: followers.length,
orderedItems: followers.map(follower => follower.actor)
})
}
const getFollowing = async (req:Request, account:String):Promise<Response> => {
console.log("GetFollowing", account)
if (ACCOUNT !== account) return new Response("", { status: 404 })
const url = new URL(req.url)
const page = url.searchParams.get("page")
const following = await db.listFollowing()
if(!page) return Response.json({
"@context": "https://www.w3.org/ns/activitystreams",
id: `${ACTOR}/following`,
type: "OrderedCollection",
totalItems: following.length,
first: `${ACTOR}/following?page=1`,
})
else return Response.json({
"@context": "https://www.w3.org/ns/activitystreams",
id: `${ACTOR}/following?page=${page}`,
type: "OrderedCollectionPage",
partOf: `${ACTOR}/following`,
totalItems: following.length,
orderedItems: following.map(follow => follow.actor)
})
}
const getActor = async (req:Request, account:string):Promise<Response> => {
console.log("GetActor", account)
if (ACCOUNT !== account) return new Response("", { status: 404 })
if(reqIsActivityPub(req)) return Response.json({
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
],
id: ACTOR,
type: "Person",
preferredUsername: ACCOUNT,
url: ACTOR,
manuallyApprovesFollowers: false,
discoverable: true,
published: "2023-09-14T00:00:00Z",
inbox: `${ACTOR}/inbox`,
outbox: `${ACTOR}/outbox`,
followers: `${ACTOR}/followers`,
following: `${ACTOR}/following`,
publicKey: {
id: `${ACTOR}#main-key`,
owner: ACTOR,
publicKeyPem: PUBLIC_KEY,
},
}, { headers: { "Content-Type": "application/activity+json"}})
else return Response.json(await db.listPosts())
}
const getPost = async (req:Request, account:string, id:string):Promise<Response> => {
console.log("GetPost", account, id)
if (ACCOUNT !== account) return new Response("", { status: 404 })
if(reqIsActivityPub(req)) return Response.json((await db.getActivity(id)).object, { headers: { "Content-Type": "application/activity+json"}})
else return Response.json(await db.getPost(id))
}
const getActivity = async (req:Request, account:string, id:string):Promise<Response> => {
console.log("GetActivity", account, id)
if (ACCOUNT !== account) return new Response("", { status: 404 })
return Response.json((await db.getActivity(id)), { headers: { "Content-Type": "application/activity+json"}})
}