2023-09-16 00:28:06 +00:00
|
|
|
import { ACCOUNT, ACTOR, HOSTNAME, PUBLIC_KEY } from "./env"
|
|
|
|
|
import * as db from "./db"
|
|
|
|
|
import { reqIsActivityPub, send, verify } from "./request"
|
2023-09-20 06:43:48 +00:00
|
|
|
import outbox from "./outbox"
|
2023-09-16 00:28:06 +00:00
|
|
|
|
|
|
|
|
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 == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/inbox\/?$/i))) return postInbox(req, match[1])
|
2023-09-20 06:43:48 +00:00
|
|
|
else if(req.method == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return postOutbox(req, match[1])
|
2023-09-16 00:28:06 +00:00
|
|
|
else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return getOutbox(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(/^\/([^\/]+)\/?$/i))) return getActor(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])
|
|
|
|
|
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-20 06:43:48 +00:00
|
|
|
|
|
|
|
|
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 })
|
|
|
|
|
|
|
|
|
|
// console.log(body)
|
|
|
|
|
|
|
|
|
|
return await outbox(body)
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-16 00:28:06 +00:00
|
|
|
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 })
|
|
|
|
|
|
2023-09-20 06:43:48 +00:00
|
|
|
// console.log(body)
|
2023-09-16 00:28:06 +00:00
|
|
|
|
|
|
|
|
// 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);
|
2023-09-20 06:43:48 +00:00
|
|
|
case "Reject": await reject(body);
|
2023-09-16 00:28:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new Response("", { status: 204 })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const follow = async (body:any) => {
|
|
|
|
|
await send(ACTOR, body.actor, {
|
|
|
|
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
|
|
|
id: `https://${HOSTNAME}/${crypto.randomUUID()}`, // TODO: de-randomise this?
|
|
|
|
|
type: "Accept",
|
|
|
|
|
actor: ACTOR,
|
|
|
|
|
object: body,
|
|
|
|
|
});
|
|
|
|
|
await db.createFollower(body.actor, body.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const undo = async (body:any) => {
|
|
|
|
|
switch (body.object.type) {
|
|
|
|
|
case "Follow": await db.deleteFollower(body.actor); break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const accept = async (body:any) => {
|
|
|
|
|
switch (body.object.type) {
|
|
|
|
|
case "Follow": await db.acceptFollowing(body.actor); break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-20 06:43:48 +00:00
|
|
|
const reject = async (body:any) => {
|
|
|
|
|
switch (body.object.type) {
|
|
|
|
|
case "Follow": await db.deleteFollowing(body.actor); break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-16 00:28:06 +00:00
|
|
|
const getOutbox = async (req:Request, account:string):Promise<Response> => {
|
|
|
|
|
console.log("GetOutbox", account)
|
|
|
|
|
if (ACCOUNT !== account) return new Response("", { status: 404 })
|
|
|
|
|
|
2023-09-20 06:43:48 +00:00
|
|
|
// TODO: Paging?
|
|
|
|
|
const posts = await db.listOutboxActivities()
|
2023-09-16 00:28:06 +00:00
|
|
|
|
|
|
|
|
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"}})
|
|
|
|
|
}
|