shadowfacts.net/lib/activitypub/federate.ts

159 lines
4.5 KiB
TypeScript
Raw Normal View History

import { promises as fs } from "fs";
import crypto from "crypto";
import uuidv4 from "uuid/v4";
import request from "request";
import { Database } from "sqlite3";
import { Activity, Article, Create, Actor, Follow, Accept } from "./activity";
import { URL } from "url";
const domain = process.env.DOMAIN;
function createActivity(article: Article): Create {
const uuid = uuidv4();
const createObject = {
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": `https://${domain}/ap/${uuid}`,
"actor": `https://${domain}/ap/actor`,
"to": article.to,
"cc": article.cc,
"object": article
};
return createObject;
}
export async function getActor(url: string, db: Database, forceUpdate: boolean = false): Promise<Actor | null> {
if (!forceUpdate) {
2019-02-24 15:21:14 +00:00
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);
if (remote) cacheActor(remote, db);
2019-02-24 15:21:14 +00:00
return remote;
}
2019-06-30 02:38:47 +00:00
export async function getCachedActor(url: string, db: Database): Promise<Actor | null> {
2019-02-24 15:21:14 +00:00
return new Promise((resolve, reject) => {
db.get("SELECT * FROM actors WHERE id = $id", {
$id: url
}, (err, result) => {
if (err) {
reject(err);
} else {
2019-06-30 02:38:47 +00:00
if (result) {
resolve({
id: result.id,
name: result.display_name,
inbox: result.inbox,
icon: result.icon_url,
publicKey: {
publicKeyPem: result.public_key_pem
}
} as Actor);
} else {
resolve(null);
}
2019-02-24 15:21:14 +00:00
}
});
});
}
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 | null> {
return new Promise((resolve, reject) => {
request({
url,
headers: {
"Accept": "application/activity+json"
},
method: "GET",
json: true
}, (err, res) => {
if (err) reject(err);
else resolve(res.body ? res.body as Actor : null);
});
});
}
export async function signAndSend(activity: Activity, inbox: string) {
const targetDomain = new URL(inbox).hostname;
const inboxFragment = inbox.replace("https://" + targetDomain, "");
const date = new Date();
const privKey = (await fs.readFile(process.env.PRIV_KEY_PEM!)).toString();
const signer = crypto.createSign("sha256");
const stringToSign = `(request-target): post ${inboxFragment}\nhost: ${targetDomain}\ndate: ${date.toUTCString()}`;
signer.update(stringToSign);
signer.end();
2019-02-20 23:07:29 +00:00
const signature = signer.sign(privKey, "base64");
const header = `keyId="https://${domain}/ap/actor#main-key",headers="(request-target) host date",signature="${signature}"`;
console.log("Sending:", activity);
console.log("stringToSign:", stringToSign);
console.log("Signature: " + header);
request({
url: inbox,
headers: {
"Host": targetDomain,
"Date": date.toUTCString(),
"Signature": header,
"Accept": "application/activity+json, application/json"
},
method: "POST",
json: true,
body: activity
}, (err, res) => {
console.log("Sent message to inbox at", targetDomain);
console.log("Response status code", res.statusCode);
console.log(res.body);
if (err) console.log("Error:", err, res);
});
}
async function sendToFollowers(activity: Create, db: Database) {
db.all("SELECT inbox FROM followers", (err, results) => {
if (err) {
console.log("Error getting followers: ", err);
return;
}
const inboxes = results.map(it => "https://" + new URL(it.inbox).host + "/inbox");
// convert to a Set to deduplicate inboxes
(new Set(inboxes))
.forEach(inbox => {
console.log(`Federating ${activity.object.id} to ${inbox}`);
signAndSend(activity, inbox);
});
});
}
export default function federate(toFederate: [string, Article][], db: Database) {
for (const [id, article] of toFederate) {
sendToFollowers(createActivity(article), db);
db.run("UPDATE articles SET has_federated = 1 WHERE id = $id", {
$id: id
});
break;
}
}