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 {
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;

View File

@ -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<ActorObject | null> {
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;
}

View File

@ -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: `<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) {
@ -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);