import { Router, Request, Response } from "express"; import uuidv4 from "uuid/v4"; import { getActor, signAndSend } from "./federate"; import { Activity, FollowActivity, AcceptActivity, UndoActivity, CreateActivity, NoteObject, DeleteActivity } from "./activity"; import { Database } from "sqlite3"; import { URL } from "url"; import sanitizeHtml from "sanitize-html"; import Note from "../entity/Note"; import { getConnection } from "typeorm"; import Actor from "../entity/Actor"; const domain = process.env.DOMAIN; export default function inbox(router: Router) { router.post("/ap/inbox", handleInbox); router.post("/inbox", handleInbox); } async function handleInbox(req: Request, res: Response) { console.log(req.body); const activity = req.body as Activity; if (activity.type === "Follow") { handleFollow(activity, req, res); } else if (activity.type === "Create") { handleCreate(activity, req, res); } else if (activity.type === "Undo") { handleUndo(activity, req, res); } else if (activity.type === "Delete") { handleDelete(activity, req, res); } else { res.end(); // TODO: handle this better } } async function handleFollow(activity: Activity, req: Request, res: Response) { if (typeof req.body.object !== "string") { res.end(); // TODO: handle this better return; } const follow = activity as FollowActivity; if (follow.object !== `https://${domain}/ap/actor`) { res.end(); return; } const actor = await getActor(follow.actor, true); // always force re-fetch the actor on follow if (!actor) { // if the actor ceases existing between the time the Follow is sent and when receive it, ignore it and end the request res.end(); return; } const acceptObject = { "@context": "https://www.w3.org/ns/activitystreams", "id": `https://${domain}/ap/${uuidv4()}`, "type": "Accept", "actor": `https://${domain}/ap/actor`, "object": follow } as AcceptActivity; signAndSend(acceptObject, actor.inbox); // prefer shared server inbox const serverInbox = actor.endpoints && actor.endpoints.sharedInbox ? actor.endpoints.sharedInbox : actor.inbox; await getConnection().createQueryBuilder() .update(Actor) .set({ isFollower: true }) .where("id = :id", { id: actor.id }) .execute(); //db.run("INSERT OR IGNORE INTO followers(id, inbox) VALUES($id, $inbox)", { //$id: actor.id, //$inbox: serverInbox //}, (err) => { //if (err) console.error(`Encountered error adding follower ${follow.actor}`, err); //}); res.end(); } async function handleCreate(activity: Activity, req: Request, res: Response) { const create = activity as CreateActivity; if (create.object.type == "Note") { handleCreateNote(create, req, res); } else { res.end(); } } function sanitizeStatus(content: string): string { return sanitizeHtml(content, { allowedTags: ["a", "span", "p", "br", "b", "strong", "i", "em", "s", "del", "u", "code", "pre", "ul", "ol", "li", "blockquote", "img"], allowedAttributes: { "a": ["href", "data-user"], "img": ["src"] }, transformTags: { "a": sanitizeHtml.simpleTransform("a", { rel: "noopener", target: "_blank" }) } }); } async function handleCreateNote(create: CreateActivity, req: Request, res: Response) { const noteObject = create.object as NoteObject; getActor(noteObject.attributedTo); // get and cache the actor if it's not already cached const sanitizedContent = sanitizeStatus(noteObject.content); const note = new Note(); note.id = noteObject.id; note.actor = await getConnection().getRepository(Actor).findOne(noteObject.attributedTo); note.content = sanitizedContent; note.attributedTo = noteObject.attributedTo; note.inReplyTo = noteObject.inReplyTo; note.conversation = noteObject.conversation; note.published = noteObject.published; try { await getConnection().getRepository(Note).save(note); } catch (err) { console.error(`Encountered error storing reply ${noteObject.id}`, err); } res.end(); //db.run("INSERT OR IGNORE INTO notes(id, content, attributed_to, in_reply_to, conversation, published) VALUES($id, $content, $attributed_to, $in_reply_to, $conversation, $published)", { //$id: noteObject.id, //$content: sanitizedContent, //$attributed_to: noteObject.attributedTo, //$in_reply_to: noteObject.inReplyTo, //$conversation: noteObject.conversation, //$published: noteObject.published //}, (err) => { //if (err) console.error(`Encountered error storing reply ${noteObject.id}`, err); //res.end(); //}); } async function handleDelete(activity: Activity, req: Request, res: Response) { const deleteActivity = activity as DeleteActivity; try { await getConnection().getRepository(Note).createQueryBuilder() .delete() .from(Note, "note") .where("note.id = :id", { id: deleteActivity.object }) .andWhere("note.actor = :actor", { actor: deleteActivity.actor }) .execute(); } catch (err) { console.error(`Encountered error deleting ${deleteActivity.object}`, err); } res.end(); //const db = req.app.get("db") as Database; //db.run("DELETE FROM notes WHERE id = $id, actor = $actor", { //$id: deleteActivity.object, //$actor: deleteActivity.actor //}, (err) => { //if (err) console.error(`Encountered error deleting ${deleteActivity.object}`, err); //res.end(); //}) } async function handleUndo(activity: Activity, req: Request, res: Response) { const undo = activity as UndoActivity; if (undo.object.type === "Follow") { handleUndoFollow(undo, req, res); } else { res.end(); } } async function handleUndoFollow(undo: UndoActivity, req: Request, res: Response) { const follow = undo.object as FollowActivity; if (follow.object !== `https://${domain}/ap/actor` || undo.actor !== follow.actor) { res.end(); return; } try { await getConnection().createQueryBuilder() .update(Actor) .set({ isFollower: false }) .where("id = :id", { id: follow.actor }) .execute(); } catch (err) { console.error(`Error handling unfollow from ${follow.actor}`, err); } res.end(); //const db = req.app.get("db") as Database; //db.run("DELETE FROM followers WHERE id = $id", { //$id: follow.actor //}, (err) => { //if (err) console.error(`Error unfollowing ${follow.actor}`, err); //}); //res.end(); }