2023-09-20 06:43:48 +00:00
|
|
|
import { idsFromValue } from "./activitypub"
|
2023-09-21 07:04:27 +00:00
|
|
|
import * as db from "./db"
|
2023-09-28 01:15:25 +00:00
|
|
|
import { ADMIN_PASSWORD, ADMIN_USERNAME } from "./env"
|
2023-09-20 06:43:48 +00:00
|
|
|
import outbox from "./outbox"
|
2023-09-26 23:50:06 +00:00
|
|
|
import { activityMimeTypes, fetchObject } from "./request"
|
2023-09-28 01:15:25 +00:00
|
|
|
import ACTOR from "../actor"
|
2023-09-16 00:28:06 +00:00
|
|
|
|
|
|
|
|
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 })
|
2023-09-27 04:13:10 +00:00
|
|
|
else if(req.method == "POST" && (match = url.pathname.match(/^\/rebuild\/?$/i))) return rebuild(req)
|
2023-09-16 00:28:06 +00:00
|
|
|
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])
|
2023-09-20 06:43:48 +00:00
|
|
|
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])
|
2023-09-21 07:04:27 +00:00
|
|
|
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])
|
2023-09-20 06:43:48 +00:00
|
|
|
|
|
|
|
|
console.log(`Couldn't match admin path ${req.method} "${url.pathname}"`)
|
2023-09-16 00:28:06 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 04:13:10 +00:00
|
|
|
// rebuild the 11ty static pages
|
|
|
|
|
export const rebuild = async(req:Request):Promise<Response> => {
|
|
|
|
|
await db.rebuild()
|
|
|
|
|
return new Response("", { status: 201 })
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-16 00:28:06 +00:00
|
|
|
// create an activity
|
2023-09-21 07:04:27 +00:00
|
|
|
const create = async (req:Request, inReplyTo:string|null = null):Promise<Response> => {
|
2023-09-16 00:28:06 +00:00
|
|
|
const body = await req.json()
|
|
|
|
|
|
2023-09-21 07:04:27 +00:00
|
|
|
if(!inReplyTo && body.object.inReplyTo) inReplyTo = body.object.inReplyTo
|
|
|
|
|
|
2023-09-28 01:15:25 +00:00
|
|
|
const original = inReplyTo ? await (await fetchObject(inReplyTo)).json() : null
|
2023-09-21 07:04:27 +00:00
|
|
|
|
2023-09-16 00:28:06 +00:00
|
|
|
// create the object, merging in supplied data
|
|
|
|
|
const date = new Date()
|
|
|
|
|
const object = {
|
2023-09-28 01:15:25 +00:00
|
|
|
attributedTo: ACTOR.id,
|
2023-09-16 00:28:06 +00:00
|
|
|
published: date.toISOString(),
|
|
|
|
|
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
2023-09-28 01:15:25 +00:00
|
|
|
cc: [ACTOR.followers],
|
2023-09-16 00:28:06 +00:00
|
|
|
...body.object
|
|
|
|
|
}
|
2023-09-21 07:04:27 +00:00
|
|
|
if(inReplyTo){
|
|
|
|
|
object.inReplyTo = original || inReplyTo
|
|
|
|
|
object.cc.push(inReplyTo)
|
|
|
|
|
}
|
2023-09-16 00:28:06 +00:00
|
|
|
|
|
|
|
|
const activity = {
|
2023-09-20 06:43:48 +00:00
|
|
|
"@context": "https://www.w3.org/ns/activitystreams",
|
2023-09-16 00:28:06 +00:00
|
|
|
type: "Create",
|
|
|
|
|
published: date.toISOString(),
|
2023-09-28 01:15:25 +00:00
|
|
|
actor: ACTOR.id,
|
2023-09-21 07:04:27 +00:00
|
|
|
to: object.to,
|
|
|
|
|
cc: object.cc,
|
2023-09-16 00:28:06 +00:00
|
|
|
...body,
|
|
|
|
|
object: { ...object }
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-20 06:43:48 +00:00
|
|
|
return await outbox(activity)
|
2023-09-16 00:28:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const follow = async (req:Request, handle:string):Promise<Response> => {
|
2023-09-26 23:50:06 +00:00
|
|
|
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
|
2023-09-28 01:15:25 +00:00
|
|
|
const [_, host] = handle.split('@')
|
2023-09-26 23:50:06 +00:00
|
|
|
const res = await fetch(`https://${host}/.well-known/webfinger/?resource=acct:${handle}`)
|
|
|
|
|
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" && (activityMimeTypes.includes(l.type)))
|
|
|
|
|
if(!actorLink) return new Response("", { status: 404 })
|
|
|
|
|
url = actorLink.href
|
|
|
|
|
}
|
|
|
|
|
console.log(`Follow ${url}`)
|
|
|
|
|
|
2023-09-16 00:28:06 +00:00
|
|
|
// send the follow request to the supplied actor
|
2023-09-20 06:43:48 +00:00
|
|
|
return await outbox({
|
2023-09-16 00:28:06 +00:00
|
|
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
|
|
|
type: "Follow",
|
2023-09-28 01:15:25 +00:00
|
|
|
actor: ACTOR.id,
|
2023-09-26 23:50:06 +00:00
|
|
|
object: url,
|
|
|
|
|
to: [url, "https://www.w3.org/ns/activitystreams#Public"]
|
2023-09-20 06:43:48 +00:00
|
|
|
})
|
2023-09-16 00:28:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const unfollow = async (req:Request, handle:string):Promise<Response> => {
|
|
|
|
|
// check to see if we are already following. If not, just return success
|
2023-09-21 07:04:27 +00:00
|
|
|
const existing = await db.getFollowing(handle)
|
2023-09-16 00:28:06 +00:00
|
|
|
if (!existing) return new Response("", { status: 204 })
|
2023-09-21 07:04:27 +00:00
|
|
|
const activity = await db.getOutboxActivity(existing.id)
|
2023-09-20 06:43:48 +00:00
|
|
|
// outbox will also take care of the deletion
|
|
|
|
|
return await outbox({
|
2023-09-16 00:28:06 +00:00
|
|
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
|
|
|
type: "Undo",
|
2023-09-28 01:15:25 +00:00
|
|
|
actor: ACTOR.id,
|
2023-09-20 06:43:48 +00:00
|
|
|
object: activity,
|
|
|
|
|
to: activity.to
|
|
|
|
|
})
|
|
|
|
|
}
|
2023-09-16 00:28:06 +00:00
|
|
|
|
2023-09-20 06:43:48 +00:00
|
|
|
const like = async (req:Request, object_url:string):Promise<Response> => {
|
2023-09-28 01:15:25 +00:00
|
|
|
const object = await (await fetchObject(object_url)).json()
|
2023-09-16 00:28:06 +00:00
|
|
|
|
2023-09-20 06:43:48 +00:00
|
|
|
return await outbox({
|
|
|
|
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
|
|
|
type: "Like",
|
2023-09-28 01:15:25 +00:00
|
|
|
actor: ACTOR.id,
|
2023-09-20 06:43:48 +00:00
|
|
|
object: object,
|
2023-09-21 07:04:27 +00:00
|
|
|
to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"],
|
2023-09-28 01:15:25 +00:00
|
|
|
cc: [ACTOR.followers]
|
2023-09-20 06:43:48 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const unlike = async (req:Request, object_id:string):Promise<Response> => {
|
|
|
|
|
// check to see if we are already following. If not, just return success
|
2023-09-21 07:04:27 +00:00
|
|
|
const liked = await db.listLiked()
|
2023-09-20 06:43:48 +00:00
|
|
|
let existing = liked.find(o => o.object_id === object_id)
|
|
|
|
|
if (!existing){
|
2023-09-28 01:15:25 +00:00
|
|
|
const object = await (await fetchObject(object_id)).json()
|
2023-09-20 06:43:48 +00:00
|
|
|
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 })
|
2023-09-21 07:04:27 +00:00
|
|
|
const activity = await db.getOutboxActivity(existing.id)
|
|
|
|
|
return undo(activity)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dislike = async (req:Request, object_url:string):Promise<Response> => {
|
2023-09-28 01:15:25 +00:00
|
|
|
const object = await (await fetchObject(object_url)).json()
|
2023-09-21 07:04:27 +00:00
|
|
|
|
|
|
|
|
return await outbox({
|
|
|
|
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
|
|
|
type: "Dislike",
|
2023-09-28 01:15:25 +00:00
|
|
|
actor: ACTOR.id,
|
2023-09-21 07:04:27 +00:00
|
|
|
object: object,
|
|
|
|
|
to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"],
|
2023-09-28 01:15:25 +00:00
|
|
|
cc: [ACTOR.followers]
|
2023-09-21 07:04:27 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const undislike = async (req:Request, object_id:string):Promise<Response> => {
|
|
|
|
|
// 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){
|
2023-09-28 01:15:25 +00:00
|
|
|
const object = await (await fetchObject(object_id)).json()
|
2023-09-21 07:04:27 +00:00
|
|
|
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<Response> => {
|
2023-09-28 01:15:25 +00:00
|
|
|
const object = await (await fetchObject(object_url)).json()
|
2023-09-21 07:04:27 +00:00
|
|
|
|
|
|
|
|
return await outbox({
|
|
|
|
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
|
|
|
type: "Announce",
|
2023-09-28 01:15:25 +00:00
|
|
|
actor: ACTOR.id,
|
2023-09-21 07:04:27 +00:00
|
|
|
object: object,
|
|
|
|
|
to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"],
|
2023-09-28 01:15:25 +00:00
|
|
|
cc: [ACTOR.followers]
|
2023-09-21 07:04:27 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const unshare = async (req:Request, object_id:string):Promise<Response> => {
|
|
|
|
|
// 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){
|
2023-09-28 01:15:25 +00:00
|
|
|
const object = await (await fetchObject(object_id)).json()
|
2023-09-21 07:04:27 +00:00
|
|
|
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<Response> => {
|
2023-09-20 06:43:48 +00:00
|
|
|
// outbox will also take care of the deletion
|
|
|
|
|
return await outbox({
|
|
|
|
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
|
|
|
type: "Undo",
|
2023-09-28 01:15:25 +00:00
|
|
|
actor: ACTOR.id,
|
2023-09-20 06:43:48 +00:00
|
|
|
object: activity,
|
2023-09-21 07:04:27 +00:00
|
|
|
to: activity.to,
|
|
|
|
|
cc: activity.cc
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deletePost = async (req:Request, id:string):Promise<Response> => {
|
|
|
|
|
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",
|
2023-09-28 01:15:25 +00:00
|
|
|
actor: ACTOR.id,
|
2023-09-21 07:04:27 +00:00
|
|
|
to: activity.to,
|
|
|
|
|
cc: activity.cc,
|
|
|
|
|
audience: activity.audience,
|
|
|
|
|
object: {
|
|
|
|
|
id,
|
|
|
|
|
type: "Tombstone"
|
|
|
|
|
}
|
2023-09-20 06:43:48 +00:00
|
|
|
})
|
2023-09-16 00:28:06 +00:00
|
|
|
}
|