193 lines
5.9 KiB
TypeScript
193 lines
5.9 KiB
TypeScript
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 { URL } from "url";
|
|
import { getConnection } from "typeorm";
|
|
import Actor from "../entity/Actor";
|
|
import Article from "../entity/Article";
|
|
|
|
const domain = process.env.DOMAIN;
|
|
|
|
function createActivity(article: ArticleObject): CreateActivity {
|
|
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, forceUpdate: boolean = false): Promise<ActorObject | null> {
|
|
if (!forceUpdate) {
|
|
try {
|
|
const cached = await getCachedActor(url);
|
|
if (cached) return cached;
|
|
} catch (err) {
|
|
console.error(`Encountered error getting cached actor ${url}`, err);
|
|
}
|
|
}
|
|
const remote = await fetchActor(url);
|
|
if (remote) cacheActor(remote);
|
|
return remote;
|
|
}
|
|
|
|
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;
|
|
} else {
|
|
return null;
|
|
}
|
|
//return new Promise(async (resolve, reject) => {
|
|
//db.get("SELECT * FROM actors WHERE id = $id", {
|
|
//$id: url
|
|
//}, (err, result) => {
|
|
//if (err) {
|
|
//reject(err);
|
|
//} else {
|
|
//if (result) {
|
|
//resolve({
|
|
//id: result.id,
|
|
//name: result.display_name,
|
|
//inbox: result.inbox,
|
|
//icon: result.icon_url,
|
|
//publicKey: {
|
|
//publicKeyPem: result.public_key_pem
|
|
//}
|
|
//} as ActorObject);
|
|
//} else {
|
|
//resolve(null);
|
|
//}
|
|
//}
|
|
//});
|
|
//});
|
|
}
|
|
|
|
async function cacheActor(actorObject: ActorObject) {
|
|
function getIconUrl(icon: string | object): string {
|
|
return icon instanceof String ? icon : (icon as any).url;
|
|
}
|
|
const iconURL: string = actorObject.icon instanceof Array ? getIconUrl(actorObject.icon[0]) : getIconUrl(actorObject.icon);
|
|
const actor = new Actor();
|
|
actor.id = actorObject.id;
|
|
actor.actorObject = actorObject;
|
|
actor.displayName = actorObject.name;
|
|
actor.inbox = actorObject.inbox;
|
|
actor.iconURL = iconURL;
|
|
actor.publicKeyPem = actorObject.publicKey.publicKeyPem;
|
|
actor.isFollower = false;
|
|
await getConnection().manager.save(actor);
|
|
//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<ActorObject | 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 ActorObject : 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();
|
|
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: CreateActivity) {
|
|
const followers = await getConnection().createQueryBuilder().select().from(Actor, "actor").where("actor.isFollower = :isFollower", { isFollower: true }).getMany();
|
|
const inboxes = followers.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);
|
|
});
|
|
//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 async function federate(toFederate: [string, ArticleObject][]) {
|
|
for (const [id, article] of toFederate) {
|
|
|
|
sendToFollowers(createActivity(article));
|
|
await getConnection().manager.update(Article, id, { hasFederated: true });
|
|
//db.run("UPDATE articles SET has_federated = 1 WHERE id = $id", {
|
|
//$id: id
|
|
//});
|
|
//break;
|
|
}
|
|
}
|
|
|