From ce30ca9d3408e787ea3c4cc80ab998307a315540 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 18 Aug 2019 18:09:28 -0400 Subject: [PATCH] More federation work --- lib/activitypub/activity.ts | 5 ++ lib/activitypub/federate.ts | 26 +++++------ lib/activitypub/inbox.ts | 92 ++++++++++++++++++++++++++----------- 3 files changed, 83 insertions(+), 40 deletions(-) diff --git a/lib/activitypub/activity.ts b/lib/activitypub/activity.ts index c7d57f2..fc2df0b 100644 --- a/lib/activitypub/activity.ts +++ b/lib/activitypub/activity.ts @@ -33,6 +33,9 @@ export interface UndoActivity extends Activity { } export interface NoteObject extends Object { + to: string[]; + cc: string[]; + directMessage?: boolean; attributedTo: string; content: string; published: string; @@ -46,7 +49,9 @@ export interface DeleteActivity extends Activity { export interface ActorObject { id: string; + url: string; name: string; + preferredUsername: string; inbox: string; endpoints?: { sharedInbox?: string; diff --git a/lib/activitypub/federate.ts b/lib/activitypub/federate.ts index b9522bf..43e670a 100644 --- a/lib/activitypub/federate.ts +++ b/lib/activitypub/federate.ts @@ -2,7 +2,13 @@ import { promises as fs } from "fs"; import crypto from "crypto"; import uuidv4 from "uuid/v4"; import request from "request"; -import { Activity, ArticleObject, FollowActivity, AcceptActivity, ActorObject, CreateActivity } from "./activity"; +import { + Activity, + ArticleObject, + ActorObject, + CreateActivity, + NoteObject +} from "./activity"; import { URL } from "url"; import { getConnection } from "typeorm"; import Actor from "../entity/Actor"; @@ -10,7 +16,7 @@ import Article from "../entity/Article"; const domain = process.env.DOMAIN; -function createActivity(article: ArticleObject): CreateActivity { +export function createActivity(object: ArticleObject | NoteObject): CreateActivity { const uuid = uuidv4(); const createObject = { "@context": [ @@ -19,9 +25,9 @@ function createActivity(article: ArticleObject): CreateActivity { "type": "Create", "id": `https://${domain}/ap/${uuid}`, "actor": `https://${domain}/ap/actor`, - "to": article.to, - "cc": article.cc, - "object": article + "to": object.to, + "cc": object.cc, + "object": object }; return createObject; } @@ -44,15 +50,7 @@ export async function getCachedActor(url: string): Promise { const result = await getConnection().manager.findByIds(Actor, [url]); if (result.length > 0) { const actor = result[0]; - return { - id: actor.id, - name: actor.displayName, - inbox: actor.inbox, - icon: actor.iconURL, - publicKey: { - publicKeyPem: actor.publicKeyPem - } - } as ActorObject; + return actor.actorObject; } else { return null; } diff --git a/lib/activitypub/inbox.ts b/lib/activitypub/inbox.ts index 0de1deb..f80b475 100644 --- a/lib/activitypub/inbox.ts +++ b/lib/activitypub/inbox.ts @@ -1,6 +1,6 @@ import { Router, Request, Response } from "express"; import uuidv4 from "uuid/v4"; -import { getActor, signAndSend } from "./federate"; +import {createActivity, getActor, signAndSend} from "./federate"; import { Activity, FollowActivity, AcceptActivity, UndoActivity, CreateActivity, NoteObject, DeleteActivity } from "./activity"; import { Database } from "sqlite3"; import { URL } from "url"; @@ -8,6 +8,7 @@ import sanitizeHtml from "sanitize-html"; import Note from "../entity/Note"; import { getConnection } from "typeorm"; import Actor from "../entity/Actor"; +import Article from "../entity/Article"; const domain = process.env.DOMAIN; @@ -16,7 +17,7 @@ export default function inbox(router: Router) { router.post("/inbox", handleInbox); } -async function handleInbox(req: Request, res: Response) { +function handleInbox(req: Request, res: Response) { console.log(req.body); const activity = req.body as Activity; if (activity.type === "Follow") { @@ -55,15 +56,22 @@ async function handleFollow(activity: Activity, req: Request, res: Response) { "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(); - res.end(); + try { + await 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, + inbox: serverInbox + }) + .where("id = :id", {id: actor.id}) + .execute(); + res.end(); + } catch (err) { + res.status(500).json({ message: "Encountered error handling follow.", error: err }).end(); + } } async function handleCreate(activity: Activity, req: Request, res: Response) { @@ -91,21 +99,53 @@ function sanitizeStatus(content: string): string { async function handleCreateNote(create: CreateActivity, req: Request, res: Response) { const noteObject = create.object as NoteObject; const actor = await 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(actor.id); - 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); + + const asPublic = "https://www.w3.org/ns/activitystreams#Public"; + const article = await getConnection().getRepository(Article).findOne({ where: { conversation: noteObject.conversation } }); + if (!article) { + console.log(`Ignoring message not in response to an article: ${noteObject.id}`); + res.end(); + } else if (!process.env.ACCEPT_NON_PUBLIC_NOTES && !noteObject.to.includes(asPublic) && !noteObject.cc.includes(asPublic)) { + console.log(`Ignoring non-public post: ${noteObject.id}`); + + try { + const note: NoteObject = { + type: "Note", + id: `https://${domain}/ap/object/${uuidv4()}`, + to: [actor.id], + cc: [], + directMessage: true, + attributedTo: `https://${domain}/ap/actor`, + content: `@${actor.preferredUsername} Non-public posts are not accepted. To respond to a blog post, use either Public or Unlisted.`, + published: new Date().toISOString(), + inReplyTo: noteObject.id, + conversation: noteObject.conversation || `https://${domain}/ap/conversation/${uuidv4()}` + }; + const responseCreate = createActivity(note); + signAndSend(responseCreate, actor.inbox); + } catch (err) { + console.error(`Couldn't send non-public reply Note to ${noteObject.id}`, err); + } + + res.end(); + } else { + const sanitizedContent = sanitizeStatus(noteObject.content); + const note = new Note(); + note.id = noteObject.id; + note.actor = await getConnection().getRepository(Actor).findOne(actor.id); + 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); + res.end(); + } catch (err) { + console.error(`Encountered error storing reply ${noteObject.id}`, err); + res.status(500).end(); + } } - res.end(); } async function handleDelete(activity: Activity, req: Request, res: Response) { @@ -123,7 +163,7 @@ async function handleDelete(activity: Activity, req: Request, res: Response) { res.end(); } -async function handleUndo(activity: Activity, req: Request, res: Response) { +function handleUndo(activity: Activity, req: Request, res: Response) { const undo = activity as UndoActivity; if (undo.object.type === "Follow") { handleUndoFollow(undo, req, res);