Cache actors
This commit is contained in:
parent
81e2fca2aa
commit
6b4ea192e1
|
@ -41,8 +41,13 @@ export interface Delete extends Activity {
|
||||||
|
|
||||||
export interface Actor {
|
export interface Actor {
|
||||||
id: string;
|
id: string;
|
||||||
|
name: string;
|
||||||
inbox: string;
|
inbox: string;
|
||||||
|
endpoints?: {
|
||||||
|
sharedInbox?: string;
|
||||||
|
}
|
||||||
publicKey: {
|
publicKey: {
|
||||||
publicKeyPem: string;
|
publicKeyPem: string;
|
||||||
};
|
};
|
||||||
|
icon: string | object | (string | object)[];
|
||||||
}
|
}
|
|
@ -12,7 +12,7 @@ export default async function actor(router: Router) {
|
||||||
],
|
],
|
||||||
"type": "Person",
|
"type": "Person",
|
||||||
"id": `https://${domain}/ap/actor`,
|
"id": `https://${domain}/ap/actor`,
|
||||||
"preferredUsername": "shadowfacts",
|
"preferredUsername": "blog",
|
||||||
"name": "shadowfacts' blog",
|
"name": "shadowfacts' blog",
|
||||||
"icon": {
|
"icon": {
|
||||||
"type": "Image",
|
"type": "Image",
|
||||||
|
|
|
@ -24,7 +24,59 @@ function createActivity(article: Article): Create {
|
||||||
return createObject;
|
return createObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchActor(url: string): Promise<Actor> {
|
export async function getActor(url: string, db: Database, forceUpdate: boolean = false): Promise<Actor> {
|
||||||
|
if (forceUpdate) {
|
||||||
|
try {
|
||||||
|
const cached = await getCachedActor(url, db);
|
||||||
|
if (cached) return cached;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Encountered error getting cached actor ${url}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const remote = await fetchActor(url);
|
||||||
|
cacheActor(remote, db);
|
||||||
|
return remote;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCachedActor(url: string, db: Database): Promise<Actor> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.get("SELECT * FROM actors WHERE id = $id", {
|
||||||
|
$id: url
|
||||||
|
}, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
id: result.id,
|
||||||
|
name: result.display_name,
|
||||||
|
inbox: result.inbox,
|
||||||
|
icon: result.icon_url,
|
||||||
|
publicKey: {
|
||||||
|
publicKeyPem: result.public_key_pem
|
||||||
|
}
|
||||||
|
} as Actor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cacheActor(actor: Actor, db: Database) {
|
||||||
|
function getIconUrl(icon: string | object): string {
|
||||||
|
return icon instanceof String ? icon : (icon as any).url;
|
||||||
|
}
|
||||||
|
const iconUrl: string = actor.icon instanceof Array ? getIconUrl(actor.icon[0]) : getIconUrl(actor.icon);
|
||||||
|
db.run("INSERT OR REPLACE INTO actors(id, display_name, inbox, icon_url, public_key_pem) VALUES($id, $display_name, $inbox, $icon_url, $public_key_pem)", {
|
||||||
|
$id: actor.id,
|
||||||
|
$display_name: actor.name,
|
||||||
|
$inbox: actor.inbox,
|
||||||
|
$icon_url: iconUrl,
|
||||||
|
$public_key_pem: actor.publicKey.publicKeyPem
|
||||||
|
}, (err) => {
|
||||||
|
if (err) console.error(`Encountered error caching actor ${actor.id}`, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchActor(url: string): Promise<Actor> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
request({
|
request({
|
||||||
url,
|
url,
|
||||||
|
@ -35,7 +87,7 @@ export async function fetchActor(url: string): Promise<Actor> {
|
||||||
json: true
|
json: true
|
||||||
}, (err, res) => {
|
}, (err, res) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else resolve(<Actor>res.body);
|
else resolve(res.body as Actor);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -74,6 +126,7 @@ export async function signAndSend(activity: Activity, inbox: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendToFollowers(activity: Create, db: Database) {
|
async function sendToFollowers(activity: Create, db: Database) {
|
||||||
|
// TODO: only send to unique inboxes
|
||||||
db.each("SELECT inbox FROM followers", (err, result) => {
|
db.each("SELECT inbox FROM followers", (err, result) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log("Error getting followers: ", err);
|
console.log("Error getting followers: ", err);
|
||||||
|
|
|
@ -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 { fetchActor, signAndSend } from "./federate";
|
import { getActor, signAndSend } from "./federate";
|
||||||
import { Activity, Follow, Accept, Undo, Create, Note, Delete } from "./activity";
|
import { Activity, Follow, Accept, Undo, Create, Note, Delete } from "./activity";
|
||||||
import { Database } from "sqlite3";
|
import { Database } from "sqlite3";
|
||||||
import { URL } from "url";
|
import { URL } from "url";
|
||||||
|
@ -39,7 +39,8 @@ async function handleFollow(activity: Activity, req: Request, res: Response) {
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const actor = await fetchActor(follow.actor);
|
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>{
|
const acceptObject = <Accept>{
|
||||||
"@context": [
|
"@context": [
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
@ -51,8 +52,8 @@ async function handleFollow(activity: Activity, req: Request, res: Response) {
|
||||||
"object": follow
|
"object": follow
|
||||||
};
|
};
|
||||||
signAndSend(acceptObject, actor.inbox);
|
signAndSend(acceptObject, actor.inbox);
|
||||||
const db = req.app.get("db") as Database;
|
// prefer shared server inbox
|
||||||
const serverInbox = new URL("/inbox", actor.inbox).toString();
|
const serverInbox = actor.endpoints && actor.endpoints.sharedInbox ? actor.endpoints.sharedInbox : actor.inbox;
|
||||||
db.run("INSERT OR IGNORE INTO followers(id, inbox) VALUES($id, $inbox)", {
|
db.run("INSERT OR IGNORE INTO followers(id, inbox) VALUES($id, $inbox)", {
|
||||||
$id: actor.id,
|
$id: actor.id,
|
||||||
$inbox: serverInbox
|
$inbox: serverInbox
|
||||||
|
@ -87,6 +88,7 @@ function sanitizeStatus(content: string): string {
|
||||||
async function handleCreateNote(create: Create, req: Request, res: Response) {
|
async function handleCreateNote(create: Create, req: Request, res: Response) {
|
||||||
const note = create.object as Note;
|
const note = create.object as Note;
|
||||||
const db = req.app.get("db") as Database;
|
const db = req.app.get("db") as Database;
|
||||||
|
getActor(note.attributedTo, db); // get and cache the actor if it's not already cached
|
||||||
const sanitizedContent = sanitizeStatus(note.content);
|
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)", {
|
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,
|
$id: note.id,
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
import crypto, { createVerify } from "crypto";
|
import crypto, { createVerify } from "crypto";
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { fetchActor } from "../federate";
|
import { getActor } from "../federate";
|
||||||
import { IncomingHttpHeaders } from "http";
|
import { IncomingHttpHeaders } from "http";
|
||||||
|
import { Database } from "sqlite3";
|
||||||
|
|
||||||
export = async (req: Request, res: Response, next: NextFunction) => {
|
export = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const actor = await fetchActor(req.body.actor as string);
|
const db = req.app.get("db") as Database;
|
||||||
|
const actor = await getActor(req.body.actor as string, db);
|
||||||
if (validate(req, actor.publicKey.publicKeyPem)) {
|
if (validate(req, actor.publicKey.publicKeyPem)) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
console.log(`Could not validate HTTP signature for ${req.body.actor}`);
|
// if the first check fails, force re-fetch the actor and try again
|
||||||
res.status(401).end("Could not validate HTTP signature");
|
const actor = await getActor(req.body.actor as string, db, true);
|
||||||
|
if (validate(req, actor.publicKey.publicKeyPem)) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
console.log(`Could not validate HTTP signature for ${req.body.actor}`);
|
||||||
|
res.status(401).end("Could not validate HTTP signature");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ const db = new (sqlite3.verbose().Database)(process.env.DB_PATH!);
|
||||||
db.run("CREATE TABLE IF NOT EXISTS followers (id TEXT PRIMARY KEY, inbox TEXT)");
|
db.run("CREATE TABLE IF NOT EXISTS followers (id TEXT PRIMARY KEY, inbox TEXT)");
|
||||||
db.run("CREATE TABLE IF NOT EXISTS articles (id TEXT PRIMARY KEY, article_doc TEXT, conversation TEXT, has_federated INT)");
|
db.run("CREATE TABLE IF NOT EXISTS articles (id TEXT PRIMARY KEY, article_doc TEXT, conversation TEXT, has_federated INT)");
|
||||||
db.run("CREATE TABLE IF NOT EXISTS notes (id TEXT PRIMARY KEY, content TEXT, attributed_to TEXT, in_reply_to TEXT, conversation TEXT, published TEXT)");
|
db.run("CREATE TABLE IF NOT EXISTS notes (id TEXT PRIMARY KEY, content TEXT, attributed_to TEXT, in_reply_to TEXT, conversation TEXT, published TEXT)");
|
||||||
|
db.run("CREATE TABLE IF NOT EXISTS actors (id TEXT PRIMARY KEY, display_name TEXT, inbox TEXT, icon_url TEXT, public_key_pem TEXT)")
|
||||||
|
|
||||||
async function generate(): Promise<Page[]> {
|
async function generate(): Promise<Page[]> {
|
||||||
generators.copy();
|
generators.copy();
|
||||||
|
|
Loading…
Reference in New Issue