import { Router, Request, Response } from "express"; import uuidv4 from "uuid/v4"; import { fetchActor, signAndSend } from "./federate"; import { Activity, Follow, Accept, Undo, Create, Note } from "./activity"; import { Database } from "sqlite3"; import { URL } from "url"; import sanitizeHtml from "sanitize-html"; 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 { 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 Follow; if (follow.object !== `https://${domain}/ap/actor`) { res.end(); return; } const actor = await fetchActor(follow.actor); const acceptObject = { "@context": [ "https://www.w3.org/ns/activitystreams", // "https://w3id.org/security/v1" ], "id": `https://${domain}/ap/${uuidv4()}`, "type": "Accept", "actor": `https://${domain}/ap/actor`, "object": follow }; signAndSend(acceptObject, actor.inbox); const db = req.app.get("db") as Database; const serverInbox = new URL("/inbox", actor.inbox).toString(); 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 Create; 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: Create, req: Request, res: Response) { const note = create.object as Note; const db = req.app.get("db") as Database; const sanitizedContent = sanitizeStatus(note.content); 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: note.id, $content: sanitizedContent, $attributed_to: note.attributedTo, $in_reply_to: note.inReplyTo, $conversation: note.conversation, $published: note.published }, (err) => { if (err) console.error(`Encountered error storing reply ${note.id}`, err); }); } async function handleUndo(activity: Activity, req: Request, res: Response) { const undo = activity as Undo; if (undo.object.type === "Follow") { handleUndoFollow(undo, req, res); } else { res.end(); } } async function handleUndoFollow(undo: Undo, req: Request, res: Response) { const follow = undo.object as Follow; if (follow.object !== `https://${domain}/ap/actor`) { res.end(); return; } 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(); }