bun-activitypub/src/outbox.ts

167 lines
5.7 KiB
TypeScript
Raw Normal View History

import { idsFromValue } from "./activitypub"
import * as db from "./db";
import { ACTOR } from "./env"
import { fetchObject, send } from "./request";
export default async function outbox(activity:any):Promise<Response> {
const date = new Date()
const id = `${date.getTime().toString(16)}`
console.log('outbox', id, activity)
// https://www.w3.org/TR/activitypub/#object-without-create
if(!activity.actor && !(activity.object || activity.target || activity.result || activity.origin || activity.instrument)) {
const object = activity
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
type: "Create",
actor: ACTOR,
object
}
const { to, bto, cc, bcc, audience } = object
if(to) activity.to = to
if(bto) activity.bto = bto
if(cc) activity.cc = cc
if(bcc) activity.bcc = bcc
if(audience) activity.audience = audience
}
activity.id = `${ACTOR}/outbox/${id}`
if(!activity.published) activity.published = date.toISOString()
if(activity.type === 'Create' && activity.object && Object(activity.object) === activity.object) {
// When a Create activity is posted, the actor of the activity SHOULD be copied onto the object's attributedTo field.
activity.object.attributedTo = activity.actor
if(!activity.object.published) activity.object.published = activity.published
}
// get the main recipients ([...new Set()] is to dedupe)
const recipientList = [...new Set([...idsFromValue(activity.to), ...idsFromValue(activity.cc), ...idsFromValue(activity.audience)])]
// add in the blind recipients
const finalRecipientList = [...new Set([...recipientList, ...idsFromValue(activity.bto), ...idsFromValue(activity.bcc)])]
// remove the blind recipients from the activity
delete activity.bto
delete activity.bcc
// now that has been taken care of, it's time to update our local data, depending on the contents of the activity
switch(activity.type) {
case "Accept": await accept(activity, id); break;
case "Follow": await follow(activity, id); break;
case "Like": await like(activity, id); break;
case "Dislike": await dislike(activity, id); break;
case "Annouce": await announce(activity, id); break;
case "Create": await create(activity, id); break;
case "Undo": await undo(activity); break;
case "Delete": await deletePost(activity); break;
// TODO: case "Anncounce": return await share(activity)
}
// save the activity data for the outbox
await db.createOutboxActivity(activity, id)
// send to the appropriate recipients
finalRecipientList.forEach((to) => {
if (to.startsWith(ACTOR + "/followers")) db.listFollowers().then(followers => followers.forEach(f => send(ACTOR, f.actor, activity)))
else if (to === "https://www.w3.org/ns/activitystreams#Public") return // there's nothing to "send" to here
else if (to) send(ACTOR, to, activity)
})
return new Response("", { status: 201, headers: { location: activity.id } })
}
async function create(activity:any, id:string) {
activity.object.id = activity.object.url = `${ACTOR}/post/${id}`
await db.createPost(activity.object, id)
return true
}
async function accept(activity:any, id:string) {
return true
}
async function follow(activity:any, id:string) {
await db.createFollowing(activity.object , id)
return true
}
async function like(activity:any, id:string) {
if(typeof activity.object === 'string'){
await db.createLiked(activity.object, id)
activity.object = await fetchObject(ACTOR, activity.object)
}
else {
const liked = await idsFromValue(activity.object)
liked.forEach(l => db.createLiked(l, id))
}
await db.createPost(activity, id)
return true
}
async function dislike(activity:any, id:string) {
if(typeof activity.object === 'string'){
await db.createDisliked(activity.object, id)
activity.object = await fetchObject(ACTOR, activity.object)
}
else {
const disliked = await idsFromValue(activity.object)
disliked.forEach(l => db.createDisliked(l, id))
}
await db.createPost(activity, id)
return true
}
async function announce(activity:any, id:string) {
if(typeof activity.object === 'string'){
await db.createShared(activity.object, id)
activity.object = await fetchObject(ACTOR, activity.object)
}
else {
const shared = await idsFromValue(activity.object)
shared.forEach(l => db.createShared(l, id))
}
await db.createPost(activity, id)
return true
}
// async function share(activity:any) {
// let object = activity.object
// if(typeof object === 'string') {
// try{
// object = await fetchObject(object)
// }
// catch { }
// }
// db.createShared(object)
// return true
// }
async function undo(activity:any) {
const id = await idsFromValue(activity.object).at(0)
if (!id) return true
const match = id.match(/\/([0-9a-f]+)\/?$/)
const local_id = match ? match[1] : id
console.log('undo', local_id)
try{
const existing = await db.getOutboxActivity(local_id)
switch(activity.object.type) {
case "Follow": await db.deleteFollowing(existing.object); break;
case "Like": idsFromValue(existing.object).forEach(async id => await db.deleteLiked(id)); await db.deletePost(local_id); break;
case "Dislike": idsFromValue(existing.object).forEach(async id => await db.deleteDisliked(id)); await db.deletePost(local_id); break;
case "Announce": idsFromValue(existing.object).forEach(async id => await db.deleteShared(id)); await db.deletePost(local_id); break;
}
}
catch {
return false
}
return true
}
async function deletePost(activity:any) {
const id = await idsFromValue(activity.object).at(0)
if(!id) return false
const post = await db.getPostByURL(id)
if(!post) return false
await db.deletePost(post.local_id)
return true
}