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 { 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}/posts/${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 }