forked from shadowfacts/shadowfacts.net
185 lines
6.1 KiB
TypeScript
185 lines
6.1 KiB
TypeScript
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();
|
|
}
|