More federation work

This commit is contained in:
Shadowfacts 2019-08-18 18:09:28 -04:00
parent aea8a2d826
commit ce30ca9d34
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
3 changed files with 83 additions and 40 deletions

View File

@ -33,6 +33,9 @@ export interface UndoActivity extends Activity {
} }
export interface NoteObject extends Object { export interface NoteObject extends Object {
to: string[];
cc: string[];
directMessage?: boolean;
attributedTo: string; attributedTo: string;
content: string; content: string;
published: string; published: string;
@ -46,7 +49,9 @@ export interface DeleteActivity extends Activity {
export interface ActorObject { export interface ActorObject {
id: string; id: string;
url: string;
name: string; name: string;
preferredUsername: string;
inbox: string; inbox: string;
endpoints?: { endpoints?: {
sharedInbox?: string; sharedInbox?: string;

View File

@ -2,7 +2,13 @@ import { promises as fs } from "fs";
import crypto from "crypto"; import crypto from "crypto";
import uuidv4 from "uuid/v4"; import uuidv4 from "uuid/v4";
import request from "request"; 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 { URL } from "url";
import { getConnection } from "typeorm"; import { getConnection } from "typeorm";
import Actor from "../entity/Actor"; import Actor from "../entity/Actor";
@ -10,7 +16,7 @@ import Article from "../entity/Article";
const domain = process.env.DOMAIN; const domain = process.env.DOMAIN;
function createActivity(article: ArticleObject): CreateActivity { export function createActivity(object: ArticleObject | NoteObject): CreateActivity {
const uuid = uuidv4(); const uuid = uuidv4();
const createObject = { const createObject = {
"@context": [ "@context": [
@ -19,9 +25,9 @@ function createActivity(article: ArticleObject): CreateActivity {
"type": "Create", "type": "Create",
"id": `https://${domain}/ap/${uuid}`, "id": `https://${domain}/ap/${uuid}`,
"actor": `https://${domain}/ap/actor`, "actor": `https://${domain}/ap/actor`,
"to": article.to, "to": object.to,
"cc": article.cc, "cc": object.cc,
"object": article "object": object
}; };
return createObject; return createObject;
} }
@ -44,15 +50,7 @@ export async function getCachedActor(url: string): Promise<ActorObject | null> {
const result = await getConnection().manager.findByIds(Actor, [url]); const result = await getConnection().manager.findByIds(Actor, [url]);
if (result.length > 0) { if (result.length > 0) {
const actor = result[0]; const actor = result[0];
return { return actor.actorObject;
id: actor.id,
name: actor.displayName,
inbox: actor.inbox,
icon: actor.iconURL,
publicKey: {
publicKeyPem: actor.publicKeyPem
}
} as ActorObject;
} else { } else {
return null; return null;
} }

View File

@ -1,6 +1,6 @@
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import uuidv4 from "uuid/v4"; 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 { Activity, FollowActivity, AcceptActivity, UndoActivity, CreateActivity, NoteObject, DeleteActivity } from "./activity";
import { Database } from "sqlite3"; import { Database } from "sqlite3";
import { URL } from "url"; import { URL } from "url";
@ -8,6 +8,7 @@ import sanitizeHtml from "sanitize-html";
import Note from "../entity/Note"; import Note from "../entity/Note";
import { getConnection } from "typeorm"; import { getConnection } from "typeorm";
import Actor from "../entity/Actor"; import Actor from "../entity/Actor";
import Article from "../entity/Article";
const domain = process.env.DOMAIN; const domain = process.env.DOMAIN;
@ -16,7 +17,7 @@ export default function inbox(router: Router) {
router.post("/inbox", handleInbox); router.post("/inbox", handleInbox);
} }
async function handleInbox(req: Request, res: Response) { function handleInbox(req: Request, res: Response) {
console.log(req.body); console.log(req.body);
const activity = req.body as Activity; const activity = req.body as Activity;
if (activity.type === "Follow") { if (activity.type === "Follow") {
@ -55,15 +56,22 @@ async function handleFollow(activity: Activity, req: Request, res: Response) {
"actor": `https://${domain}/ap/actor`, "actor": `https://${domain}/ap/actor`,
"object": follow "object": follow
} as AcceptActivity; } as AcceptActivity;
signAndSend(acceptObject, actor.inbox); try {
// prefer shared server inbox await signAndSend(acceptObject, actor.inbox);
const serverInbox = actor.endpoints && actor.endpoints.sharedInbox ? actor.endpoints.sharedInbox : actor.inbox; // prefer shared server inbox
await getConnection().createQueryBuilder() const serverInbox = actor.endpoints && actor.endpoints.sharedInbox ? actor.endpoints.sharedInbox : actor.inbox;
.update(Actor) await getConnection().createQueryBuilder()
.set({ isFollower: true }) .update(Actor)
.where("id = :id", { id: actor.id }) .set({
.execute(); isFollower: true,
res.end(); 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) { 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) { async function handleCreateNote(create: CreateActivity, req: Request, res: Response) {
const noteObject = create.object as NoteObject; const noteObject = create.object as NoteObject;
const actor = await getActor(noteObject.attributedTo); // get and cache the actor if it's not already cached 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(); const asPublic = "https://www.w3.org/ns/activitystreams#Public";
note.id = noteObject.id; const article = await getConnection().getRepository(Article).findOne({ where: { conversation: noteObject.conversation } });
note.actor = await getConnection().getRepository(Actor).findOne(actor.id); if (!article) {
note.content = sanitizedContent; console.log(`Ignoring message not in response to an article: ${noteObject.id}`);
note.attributedTo = noteObject.attributedTo; res.end();
note.inReplyTo = noteObject.inReplyTo; } else if (!process.env.ACCEPT_NON_PUBLIC_NOTES && !noteObject.to.includes(asPublic) && !noteObject.cc.includes(asPublic)) {
note.conversation = noteObject.conversation; console.log(`Ignoring non-public post: ${noteObject.id}`);
note.published = noteObject.published;
try { try {
await getConnection().getRepository(Note).save(note); const note: NoteObject = {
} catch (err) { type: "Note",
console.error(`Encountered error storing reply ${noteObject.id}`, err); id: `https://${domain}/ap/object/${uuidv4()}`,
to: [actor.id],
cc: [],
directMessage: true,
attributedTo: `https://${domain}/ap/actor`,
content: `<a href="${actor.url}" class="mention">@<span>${actor.preferredUsername}</span></a> 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) { async function handleDelete(activity: Activity, req: Request, res: Response) {
@ -123,7 +163,7 @@ async function handleDelete(activity: Activity, req: Request, res: Response) {
res.end(); res.end();
} }
async function handleUndo(activity: Activity, req: Request, res: Response) { function handleUndo(activity: Activity, req: Request, res: Response) {
const undo = activity as UndoActivity; const undo = activity as UndoActivity;
if (undo.object.type === "Follow") { if (undo.object.type === "Follow") {
handleUndoFollow(undo, req, res); handleUndoFollow(undo, req, res);