shadowfacts.net/lib/activitypub/inbox.ts

140 lines
4.4 KiB
TypeScript
Raw Normal View History

import { Router, Request, Response } from "express";
import uuidv4 from "uuid/v4";
2019-02-24 15:21:14 +00:00
import { getActor, signAndSend } from "./federate";
import { Activity, Follow, Accept, Undo, Create, Note, Delete } from "./activity";
import { Database } from "sqlite3";
import { URL } from "url";
2019-02-22 00:17:59 +00:00
import sanitizeHtml from "sanitize-html";
const domain = process.env.DOMAIN;
2019-02-20 23:07:29 +00:00
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 Follow;
if (follow.object !== `https://${domain}/ap/actor`) {
res.end();
return;
}
2019-02-24 15:21:14 +00:00
const db = req.app.get("db") as Database;
const actor = await getActor(follow.actor, db, true); // always force re-fetch the actor on follow
const acceptObject = <Accept>{
"@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);
2019-02-24 15:21:14 +00:00
// prefer shared server inbox
const serverInbox = actor.endpoints && actor.endpoints.sharedInbox ? actor.endpoints.sharedInbox : actor.inbox;
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();
}
}
2019-02-22 00:17:59 +00:00
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;
2019-02-22 00:17:59 +00:00
const db = req.app.get("db") as Database;
2019-02-24 15:21:14 +00:00
getActor(note.attributedTo, db); // get and cache the actor if it's not already cached
2019-02-22 00:17:59 +00:00
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);
res.end();
2019-02-22 00:17:59 +00:00
});
}
async function handleDelete(activity: Activity, req: Request, res: Response) {
const deleteActivity = activity as Delete;
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 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();
}