Replaces sqlite with postgres and TypeORM

This commit is contained in:
Shadowfacts 2019-08-17 14:50:18 -04:00
parent 63c679d859
commit 8404479d91
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
14 changed files with 1362 additions and 465 deletions

View File

@ -4,30 +4,35 @@ export interface Activity {
id: string; id: string;
} }
export interface Article extends Activity { export interface Object {
type: string;
id: string;
}
export interface ArticleObject extends Object {
to: string[]; to: string[];
cc: string[]; cc: string[];
} }
export interface Create extends Activity { export interface CreateActivity extends Activity {
to: string[]; to: string[];
cc: string[]; cc: string[];
object: Activity; object: Object;
} }
export interface Follow extends Activity { export interface FollowActivity extends Activity {
object: string; object: string;
} }
export interface Accept extends Activity { export interface AcceptActivity extends Activity {
object: Follow; object: FollowActivity;
} }
export interface Undo extends Activity { export interface UndoActivity extends Activity {
object: Activity; object: Activity;
} }
export interface Note extends Activity { export interface NoteObject extends Object {
attributedTo: string; attributedTo: string;
content: string; content: string;
published: string; published: string;
@ -35,11 +40,11 @@ export interface Note extends Activity {
conversation: string; conversation: string;
} }
export interface Delete extends Activity { export interface DeleteActivity extends Activity {
object: string; object: string;
} }
export interface Actor { export interface ActorObject {
id: string; id: string;
name: string; name: string;
inbox: string; inbox: string;

View File

@ -1,14 +1,19 @@
import express, { Router, Request, Response } from "express"; import express, { Router, Request, Response } from "express";
import { Page, PostMetadata } from "../metadata"; import { Page, PostMetadata } from "../metadata";
import { Article } from "./activity"; import { ArticleObject } from "./activity";
import { Database } from "sqlite3"; import Article from "../entity/Article";
import { getConnection } from "typeorm";
import uuidv4 from "uuid/v4"; import uuidv4 from "uuid/v4";
const domain = process.env.DOMAIN; const domain = process.env.DOMAIN;
export async function setup(posts: Page[], db: Database) { export async function setup(posts: Page[]) {
const repository = getConnection().getRepository(Article);
for (const post of posts) { for (const post of posts) {
const postMeta = <PostMetadata>post.metadata; const postMeta = <PostMetadata>post.metadata;
if (await repository.findOne(postMeta.permalink)) {
continue;
}
const articleObject = { const articleObject = {
"@context": [ "@context": [
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
@ -29,52 +34,74 @@ export async function setup(posts: Page[], db: Database) {
"name": postMeta.title, "name": postMeta.title,
"content": post.text "content": post.text
}; };
db.run("INSERT OR IGNORE INTO articles(id, article_doc, conversation, has_federated) VALUES($id, $article_doc, $conversation, $has_federated)", { const article = new Article();
$id: postMeta.permalink, article.id = postMeta.permalink;
$article_doc: JSON.stringify(articleObject), article.articleObject = articleObject;
$conversation: articleObject.conversation, article.conversation = articleObject.conversation;
$has_federated: 0 article.hasFederated = false;
}, (err) => { await getConnection().manager.save(article);
if (err) console.log(`Encountered error inserting article ${postMeta.permalink}`, err); //db.run("INSERT OR IGNORE INTO articles(id, article_doc, conversation, has_federated) VALUES($id, $article_doc, $conversation, $has_federated)", {
}); //$id: postMeta.permalink,
//$article_doc: JSON.stringify(articleObject),
//$conversation: articleObject.conversation,
//$has_federated: 0
//}, (err) => {
//if (err) console.log(`Encountered error inserting article ${postMeta.permalink}`, err);
//});
} }
} }
export async function toFederate(db: Database): Promise<[string, Article][]> { export async function toFederate(): Promise<[string, ArticleObject][]> {
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
db.all("SELECT id, article_doc FROM articles WHERE has_federated = $has_federated", { const articles: Article[] = await getConnection().createQueryBuilder().select("article").from(Article, "article").where("article.hasFederated = :hasFederated", { hasFederated: false }).getMany();
$has_federated: 0
}, (err, rows) => { let result: [string, ArticleObject][] = [];
if (err) reject(err); articles.forEach(it => {
else { result.push([it.id, it.articleObject]);
let result: [string, Article][] = [];
for (const row of rows) {
result.push([row.id, JSON.parse(row.article_doc)]);
}
resolve(result);
}
}); });
resolve(result);
//db.all("SELECT id, article_doc FROM articles WHERE has_federated = $has_federated", {
//$has_federated: 0
//}, (err, rows) => {
//if (err) reject(err);
//else {
//let result: [string, Article][] = [];
//for (const row of rows) {
//result.push([row.id, JSON.parse(row.article_doc)]);
//}
//resolve(result);
//}
//});
}); });
} }
export function route(router: Router) { export function route(router: Router) {
router.use("/:category/:year/:slug/", (req, res, next) => { router.use("/:category/:year/:slug/", async (req, res, next) => {
const best = req.accepts(["text/html", "application/activity+json"]); const best = req.accepts(["text/html", "application/activity+json"]);
console.log(best); console.log(best);
if (best === "text/html") { if (best === "text/html") {
next(); next();
} else if (best === "application/activity+json") { } else if (best === "application/activity+json") {
const db = <Database>req.app.get("db") const id = `/${req.params.category}/${req.params.year}/${req.params.slug}/`;
db.get("SELECT article_doc FROM articles WHERE id = $id", { const repository = getConnection().getRepository(Article);
$id: `/${req.params.category}/${req.params.year}/${req.params.slug}/` try {
}, (err, result) => { const article = await repository.findOne(id);
if (err) { res.type("application/activity+json").json(article.articleObject).end();
} catch (err) {
res.status(500).end(err); res.status(500).end(err);
return;
} }
res.type("application/activity+json");
res.end(result.article_doc); //const db = <Database>req.app.get("db")
}); //db.get("SELECT article_doc FROM articles WHERE id = $id", {
//$id: `/${req.params.category}/${req.params.year}/${req.params.slug}/`
//}, (err, result) => {
//if (err) {
//res.status(500).end(err);
//return;
//}
//res.type("application/activity+json");
//res.end(result.article_doc);
//});
} else { } else {
res.status(415).end("No acceptable content-type given. text/html or application/activity+json are supported"); res.status(415).end("No acceptable content-type given. text/html or application/activity+json are supported");
} }

View File

@ -1,64 +1,95 @@
import { Router } from "express"; import { Router } from "express";
import { Database } from "sqlite3"; import { NoteObject, ActorObject } from "./activity";
import { Note, Actor } from "./activity";
import { getCachedActor } from "./federate"; import { getCachedActor } from "./federate";
import { getConnection } from "typeorm";
import Note from "../entity/Note";
import Article from "../entity/Article";
const domain = process.env.DOMAIN; const domain = process.env.DOMAIN;
interface Comment extends Note { interface Comment {
author: Actor; id: string;
content: string;
published: string;
inReplyTo: string;
author: ActorObject;
} }
async function getConversationComments(conversation: string, db: Database): Promise<Comment[]> { async function getConversationComments(conversation: string): Promise<Comment[]> {
return new Promise((resolve, reject) => { try {
db.all("SELECT notes.id AS comment_id, notes.content, notes.published, notes.in_reply_to, actors.id AS actor_id, actors.display_name, actors.icon_url FROM notes INNER JOIN actors ON actors.id = notes.attributed_to WHERE notes.conversation = $conversation", { const notes = await getConnection().getRepository(Note).find({ where: { conversation }, relations: ["actor"] });
$conversation: conversation return notes.map(it => {
}, (err, rows) => {
if (err) {
reject(err);
} else {
const comments = rows.map(row => {
return { return {
id: row.comment_id, id: it.id,
content: row.content, content: it.content,
published: row.published, published: it.published,
inReplyTo: row.in_reply_to, inReplyTo: it.inReplyTo,
author: { author: {
id: row.actor_id, id: it.actor.id,
name: row.display_name, name: it.actor.displayName,
icon: row.icon_url icon: it.actor.iconURL
} as Actor } as ActorObject
} as Comment; } as Comment;
}); });
resolve(comments); } catch (err) {
console.log("Couldn't load comments: ", err);
return [];
} }
}) //return new Promise((resolve, reject) => {
}); //db.all("SELECT notes.id AS comment_id, notes.content, notes.published, notes.in_reply_to, actors.id AS actor_id, actors.display_name, actors.icon_url FROM notes INNER JOIN actors ON actors.id = notes.attributed_to WHERE notes.conversation = $conversation", {
//$conversation: conversation
//}, (err, rows) => {
//if (err) {
//reject(err);
//} else {
//const comments = rows.map(row => {
//return {
//id: row.comment_id,
//content: row.content,
//published: row.published,
//inReplyTo: row.in_reply_to,
//author: {
//id: row.actor_id,
//name: row.display_name,
//icon: row.icon_url
//} as ActorObject
//} as Comment;
//});
//resolve(comments);
//}
//})
//});
} }
export default function comments(router: Router) { export default function comments(router: Router) {
router.get("/ap/conversation/:id", async (req, res) => { router.get("/ap/conversation/:id", async (req, res) => {
const db = req.app.get("db") as Database; const comments = await getConversationComments(`https://${domain}/ap/conversation/${req.params.id}`);
const comments = await getConversationComments(`https://${domain}/ap/conversation/${req.params.id}`, db);
res.json(comments).end(); res.json(comments).end();
}); });
router.get("/comments", (req, res) => { router.get("/comments", async (req, res) => {
const id = req.query.id; const id = req.query.id;
if (!id) { if (!id) {
res.sendStatus(400).end(); res.sendStatus(400).end();
return; return;
} }
const db = req.app.get("db") as Database; try {
db.get("SELECT conversation FROM articles WHERE id = $id", { const article = await getConnection().getRepository(Article).findOne(id);
$id: id const comments = await getConversationComments(article.conversation);
}, async (err, result) => {
if (!result || !result.conversation) {
res.json([]).end();
return;
}
const comments = await getConversationComments(result.conversation, db);
res.json(comments).end(); res.json(comments).end();
}); } catch (err) {
console.error("Couldn't retrieve conversation: ", err);
res.json([]).end();
}
//db.get("SELECT conversation FROM articles WHERE id = $id", {
//$id: id
//}, async (err, result) => {
//if (!result || !result.conversation) {
//res.json([]).end();
//return;
//}
//const comments = await getConversationComments(result.conversation, db);
//res.json(comments).end();
//});
}); });
} }

View File

@ -2,13 +2,15 @@ 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 { Database } from "sqlite3"; import { Activity, ArticleObject, FollowActivity, AcceptActivity, ActorObject, CreateActivity } from "./activity";
import { Activity, Article, Create, Actor, Follow, Accept } from "./activity";
import { URL } from "url"; import { URL } from "url";
import { getConnection } from "typeorm";
import Actor from "../entity/Actor";
import Article from "../entity/Article";
const domain = process.env.DOMAIN; const domain = process.env.DOMAIN;
function createActivity(article: Article): Create { function createActivity(article: ArticleObject): CreateActivity {
const uuid = uuidv4(); const uuid = uuidv4();
const createObject = { const createObject = {
"@context": [ "@context": [
@ -24,63 +26,87 @@ function createActivity(article: Article): Create {
return createObject; return createObject;
} }
export async function getActor(url: string, db: Database, forceUpdate: boolean = false): Promise<Actor | null> { export async function getActor(url: string, forceUpdate: boolean = false): Promise<ActorObject | null> {
if (!forceUpdate) { if (!forceUpdate) {
try { try {
const cached = await getCachedActor(url, db); const cached = await getCachedActor(url);
if (cached) return cached; if (cached) return cached;
} catch (err) { } catch (err) {
console.error(`Encountered error getting cached actor ${url}`, err); console.error(`Encountered error getting cached actor ${url}`, err);
} }
} }
const remote = await fetchActor(url); const remote = await fetchActor(url);
if (remote) cacheActor(remote, db); if (remote) cacheActor(remote);
return remote; return remote;
} }
export async function getCachedActor(url: string, db: Database): Promise<Actor | null> { export async function getCachedActor(url: string): Promise<ActorObject | null> {
return new Promise((resolve, reject) => { const result = await getConnection().manager.findByIds(Actor, [url]);
db.get("SELECT * FROM actors WHERE id = $id", { if (result.length > 0) {
$id: url const actor = result[0];
}, (err, result) => { return {
if (err) { id: actor.id,
reject(err); name: actor.displayName,
} else { inbox: actor.inbox,
if (result) { icon: actor.iconURL,
resolve({
id: result.id,
name: result.display_name,
inbox: result.inbox,
icon: result.icon_url,
publicKey: { publicKey: {
publicKeyPem: result.public_key_pem publicKeyPem: actor.publicKeyPem
} }
} as Actor); } as ActorObject;
} else { } else {
resolve(null); 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(actor: Actor, db: Database) { async function cacheActor(actorObject: ActorObject) {
function getIconUrl(icon: string | object): string { function getIconUrl(icon: string | object): string {
return icon instanceof String ? icon : (icon as any).url; return icon instanceof String ? icon : (icon as any).url;
} }
const iconUrl: string = actor.icon instanceof Array ? getIconUrl(actor.icon[0]) : getIconUrl(actor.icon); const iconURL: string = actorObject.icon instanceof Array ? getIconUrl(actorObject.icon[0]) : getIconUrl(actorObject.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)", { const actor = new Actor();
$id: actor.id, actor.id = actorObject.id;
$display_name: actor.name, actor.actorObject = actorObject;
$inbox: actor.inbox, actor.displayName = actorObject.name;
$icon_url: iconUrl, actor.inbox = actorObject.inbox;
$public_key_pem: actor.publicKey.publicKeyPem actor.iconURL = iconURL;
}, (err) => { actor.publicKeyPem = actorObject.publicKey.publicKeyPem;
if (err) console.error(`Encountered error caching actor ${actor.id}`, err); 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<Actor | null> { async function fetchActor(url: string): Promise<ActorObject | null> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request({ request({
url, url,
@ -91,7 +117,7 @@ async function fetchActor(url: string): Promise<Actor | null> {
json: true json: true
}, (err, res) => { }, (err, res) => {
if (err) reject(err); if (err) reject(err);
else resolve(res.body ? res.body as Actor : null); else resolve(res.body ? res.body as ActorObject : null);
}); });
}); });
} }
@ -129,30 +155,38 @@ export async function signAndSend(activity: Activity, inbox: string) {
}); });
} }
async function sendToFollowers(activity: Create, db: Database) { async function sendToFollowers(activity: CreateActivity) {
db.all("SELECT inbox FROM followers", (err, results) => { const followers = await getConnection().createQueryBuilder().select().from(Actor, "actor").where("actor.isFollower = :isFollower", { isFollower: true }).getMany();
if (err) { const inboxes = followers.map(it => "https://" + new URL(it.inbox).host + "/inbox");
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 // convert to a Set to deduplicate inboxes
(new Set(inboxes)) (new Set(inboxes)).forEach(inbox => {
.forEach(inbox => {
console.log(`Federating ${activity.object.id} to ${inbox}`); console.log(`Federating ${activity.object.id} to ${inbox}`);
signAndSend(activity, 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 function federate(toFederate: [string, Article][], db: Database) { export default async function federate(toFederate: [string, ArticleObject][]) {
for (const [id, article] of toFederate) { for (const [id, article] of toFederate) {
sendToFollowers(createActivity(article), db); sendToFollowers(createActivity(article));
db.run("UPDATE articles SET has_federated = 1 WHERE id = $id", { await getConnection().manager.update(Article, id, { hasFederated: true });
$id: id //db.run("UPDATE articles SET has_federated = 1 WHERE id = $id", {
}); //$id: id
break; //});
//break;
} }
} }

View File

@ -1,10 +1,13 @@
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 { getActor, signAndSend } from "./federate";
import { Activity, Follow, Accept, Undo, Create, Note, Delete } 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";
import sanitizeHtml from "sanitize-html"; import sanitizeHtml from "sanitize-html";
import Note from "../entity/Note";
import { getConnection } from "typeorm";
import Actor from "../entity/Actor";
const domain = process.env.DOMAIN; const domain = process.env.DOMAIN;
@ -34,42 +37,43 @@ async function handleFollow(activity: Activity, req: Request, res: Response) {
res.end(); // TODO: handle this better res.end(); // TODO: handle this better
return; return;
} }
const follow = activity as Follow; const follow = activity as FollowActivity;
if (follow.object !== `https://${domain}/ap/actor`) { if (follow.object !== `https://${domain}/ap/actor`) {
res.end(); res.end();
return; return;
} }
const db = req.app.get("db") as Database; const actor = await getActor(follow.actor, true); // always force re-fetch the actor on follow
const actor = await getActor(follow.actor, db, true); // always force re-fetch the actor on follow
if (!actor) { if (!actor) {
// if the actor ceases existing between the time the Follow is sent and when receive it, ignore it and end the request // if the actor ceases existing between the time the Follow is sent and when receive it, ignore it and end the request
res.end(); res.end();
return; return;
} }
const acceptObject = <Accept>{ const acceptObject = {
"@context": [ "@context": "https://www.w3.org/ns/activitystreams",
"https://www.w3.org/ns/activitystreams",
// "https://w3id.org/security/v1"
],
"id": `https://${domain}/ap/${uuidv4()}`, "id": `https://${domain}/ap/${uuidv4()}`,
"type": "Accept", "type": "Accept",
"actor": `https://${domain}/ap/actor`, "actor": `https://${domain}/ap/actor`,
"object": follow "object": follow
}; } as AcceptActivity;
signAndSend(acceptObject, actor.inbox); signAndSend(acceptObject, actor.inbox);
// prefer shared server inbox // prefer shared server inbox
const serverInbox = actor.endpoints && actor.endpoints.sharedInbox ? actor.endpoints.sharedInbox : actor.inbox; const serverInbox = actor.endpoints && actor.endpoints.sharedInbox ? actor.endpoints.sharedInbox : actor.inbox;
db.run("INSERT OR IGNORE INTO followers(id, inbox) VALUES($id, $inbox)", { await getConnection().createQueryBuilder()
$id: actor.id, .update(Actor)
$inbox: serverInbox .set({ isFollower: true })
}, (err) => { .where("id = :id", { id: actor.id })
if (err) console.error(`Encountered error adding follower ${follow.actor}`, err); .execute();
}); //db.run("INSERT OR IGNORE INTO followers(id, inbox) VALUES($id, $inbox)", {
//$id: actor.id,
//$inbox: serverInbox
//}, (err) => {
//if (err) console.error(`Encountered error adding follower ${follow.actor}`, err);
//});
res.end(); res.end();
} }
async function handleCreate(activity: Activity, req: Request, res: Response) { async function handleCreate(activity: Activity, req: Request, res: Response) {
const create = activity as Create; const create = activity as CreateActivity;
if (create.object.type == "Note") { if (create.object.type == "Note") {
handleCreateNote(create, req, res); handleCreateNote(create, req, res);
} else { } else {
@ -90,38 +94,62 @@ function sanitizeStatus(content: string): string {
}); });
} }
async function handleCreateNote(create: Create, req: Request, res: Response) { async function handleCreateNote(create: CreateActivity, req: Request, res: Response) {
const note = create.object as Note; const noteObject = create.object as NoteObject;
const db = req.app.get("db") as Database; getActor(noteObject.attributedTo); // get and cache the actor if it's not already cached
getActor(note.attributedTo, db); // get and cache the actor if it's not already cached const sanitizedContent = sanitizeStatus(noteObject.content);
const sanitizedContent = sanitizeStatus(note.content); const note = new Note();
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)", { note.id = noteObject.id;
$id: note.id, note.actor = await getConnection().getRepository(Actor).findOne(noteObject.attributedTo);
$content: sanitizedContent, note.content = sanitizedContent;
$attributed_to: note.attributedTo, note.attributedTo = noteObject.attributedTo;
$in_reply_to: note.inReplyTo, note.inReplyTo = noteObject.inReplyTo;
$conversation: note.conversation, note.conversation = noteObject.conversation;
$published: note.published note.published = noteObject.published;
}, (err) => { try {
if (err) console.error(`Encountered error storing reply ${note.id}`, err); await getConnection().getRepository(Note).save(note);
} catch (err) {
console.error(`Encountered error storing reply ${noteObject.id}`, err);
}
res.end(); res.end();
}); //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: noteObject.id,
//$content: sanitizedContent,
//$attributed_to: noteObject.attributedTo,
//$in_reply_to: noteObject.inReplyTo,
//$conversation: noteObject.conversation,
//$published: noteObject.published
//}, (err) => {
//if (err) console.error(`Encountered error storing reply ${noteObject.id}`, err);
//res.end();
//});
} }
async function handleDelete(activity: Activity, req: Request, res: Response) { async function handleDelete(activity: Activity, req: Request, res: Response) {
const deleteActivity = activity as Delete; const deleteActivity = activity as DeleteActivity;
const db = req.app.get("db") as Database; try {
db.run("DELETE FROM notes WHERE id = $id, actor = $actor", { await getConnection().getRepository(Note).createQueryBuilder()
$id: deleteActivity.object, .delete()
$actor: deleteActivity.actor .from(Note, "note")
}, (err) => { .where("note.id = :id", { id: deleteActivity.object })
if (err) console.error(`Encountered error deleting ${deleteActivity.object}`, err); .andWhere("note.actor = :actor", { actor: deleteActivity.actor })
.execute();
} catch (err) {
console.error(`Encountered error deleting ${deleteActivity.object}`, err);
}
res.end(); res.end();
}) //const db = req.app.get("db") as Database;
//db.run("DELETE FROM notes WHERE id = $id, actor = $actor", {
//$id: deleteActivity.object,
//$actor: deleteActivity.actor
//}, (err) => {
//if (err) console.error(`Encountered error deleting ${deleteActivity.object}`, err);
//res.end();
//})
} }
async function handleUndo(activity: Activity, req: Request, res: Response) { async function handleUndo(activity: Activity, req: Request, res: Response) {
const undo = activity as Undo; const undo = activity as UndoActivity;
if (undo.object.type === "Follow") { if (undo.object.type === "Follow") {
handleUndoFollow(undo, req, res); handleUndoFollow(undo, req, res);
} else { } else {
@ -129,17 +157,28 @@ async function handleUndo(activity: Activity, req: Request, res: Response) {
} }
} }
async function handleUndoFollow(undo: Undo, req: Request, res: Response) { async function handleUndoFollow(undo: UndoActivity, req: Request, res: Response) {
const follow = undo.object as Follow; const follow = undo.object as FollowActivity;
if (follow.object !== `https://${domain}/ap/actor`) { if (follow.object !== `https://${domain}/ap/actor` || undo.actor !== follow.actor) {
res.end(); res.end();
return; return;
} }
const db = req.app.get("db") as Database; try {
db.run("DELETE FROM followers WHERE id = $id", { await getConnection().createQueryBuilder()
$id: follow.actor .update(Actor)
}, (err) => { .set({ isFollower: false })
if (err) console.error(`Error unfollowing ${follow.actor}`, err); .where("id = :id", { id: follow.actor })
}); .execute();
} catch (err) {
console.error(`Error handling unfollow from ${follow.actor}`, err);
}
res.end(); res.end();
//const db = req.app.get("db") as Database;
//db.run("DELETE FROM followers WHERE id = $id", {
//$id: follow.actor
//}, (err) => {
//if (err) console.error(`Error unfollowing ${follow.actor}`, err);
//});
//res.end();
} }

View File

@ -9,13 +9,12 @@ export = async (req: Request, res: Response, next: NextFunction) => {
next(); next();
return; return;
} }
const db = req.app.get("db") as Database; const actor = await getActor(req.body.actor as string);
const actor = await getActor(req.body.actor as string, db);
if (actor && validate(req, actor.publicKey.publicKeyPem)) { if (actor && validate(req, actor.publicKey.publicKeyPem)) {
next(); next();
} else { } else {
// if the first check fails, force re-fetch the actor and try again // if the first check fails, force re-fetch the actor and try again
const actor = await getActor(req.body.actor as string, db, true); const actor = await getActor(req.body.actor as string, true);
if (!actor) { if (!actor) {
// probably caused by Delete activity for an actor // probably caused by Delete activity for an actor
if (req.body.type === "Delete") { if (req.body.type === "Delete") {

31
lib/entity/Actor.ts Normal file
View File

@ -0,0 +1,31 @@
import { Entity, PrimaryColumn, Column, OneToMany } from "typeorm";
import { ActorObject } from "../activitypub/activity";
import Note from "./Note";
@Entity()
export default class Actor {
@PrimaryColumn()
id: string;
@Column({ type: "json" })
actorObject: ActorObject;
@Column()
isFollower: boolean;
@Column()
displayName: string;
@Column()
inbox: string;
@Column()
iconURL: string;
@Column()
publicKeyPem: string;
@OneToMany(type => Note, note => note.actor)
notes: Note[];
}

20
lib/entity/Article.ts Normal file
View File

@ -0,0 +1,20 @@
import { Entity, PrimaryColumn, Column } from "typeorm";
import { ArticleObject } from "../activitypub/activity";
@Entity()
export default class Article {
@PrimaryColumn()
id: string;
// the ActivityStreams Article object for this article
@Column({ type: "json" })
articleObject: ArticleObject;
@Column()
conversation: string;
@Column()
hasFederated: boolean;
}

27
lib/entity/Note.ts Normal file
View File

@ -0,0 +1,27 @@
import { Entity, PrimaryColumn, Column, ManyToOne } from "typeorm";
import Actor from "./Actor";
@Entity()
export default class Note {
@PrimaryColumn()
id: string;
@Column()
content: string;
@Column()
attributedTo: string;
@Column()
inReplyTo: string;
@Column()
conversation: string;
@Column()
published: string;
@ManyToOne(type => Actor, actor => actor.notes)
actor: Actor;
}

View File

@ -7,14 +7,20 @@ import bodyParser from "body-parser";
import activitypub from "./activitypub"; import activitypub from "./activitypub";
import validateHttpSig from "./activitypub/middleware/http-signature"; import validateHttpSig from "./activitypub/middleware/http-signature";
import sqlite3 from "sqlite3"; //import sqlite3 from "sqlite3";
import "reflect-metadata";
import { createConnection} from "typeorm";
const db = new (sqlite3.verbose().Database)(process.env.DB_PATH!); //createConnection().then(async connection => {
db.run("CREATE TABLE IF NOT EXISTS followers (id TEXT PRIMARY KEY, inbox TEXT)"); //}).catch(console.error);
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)"); //const db = new (sqlite3.verbose().Database)(process.env.DB_PATH!);
db.run("CREATE TABLE IF NOT EXISTS actors (id TEXT PRIMARY KEY, display_name TEXT, inbox TEXT, icon_url TEXT, public_key_pem 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 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();
@ -32,18 +38,24 @@ async function generate(): Promise<Page[]> {
return posts; return posts;
} }
const app = express(); //const app = express();
app.set("db", db); //app.set("db", db);
app.use(morgan("dev")); //app.use(morgan("dev"));
app.use(bodyParser.json({ type: "application/activity+json" })); //app.use(bodyParser.json({ type: "application/activity+json" }));
//db.run("DELETE FROM articles"); //db.run("DELETE FROM articles");
(async () => { (async () => {
const app = express();
app.use(morgan("dev"))
app.use(bodyParser.json({ type: "application/activity+json" }));
const connection = await createConnection();
const posts = await generate(); const posts = await generate();
await activitypub.articles.setup(posts, db); await activitypub.articles.setup(posts);
const toFederate = await activitypub.articles.toFederate(db); const toFederate = await activitypub.articles.toFederate();
const apRouter = Router(); const apRouter = Router();
apRouter.use(validateHttpSig); apRouter.use(validateHttpSig);
@ -60,7 +72,7 @@ app.use(bodyParser.json({ type: "application/activity+json" }));
app.listen(port, () => { app.listen(port, () => {
console.log(`Listening on port ${port}`); console.log(`Listening on port ${port}`);
activitypub.federate(toFederate, db); activitypub.federate(toFederate);
}); });
})(); })();

24
ormconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "blog",
"password": "blog",
"database": "blog",
"synchronize": true,
"logging": true,
"entities": [
"built/entity/**/*.js"
],
"migrations": [
"built/migration/**/*.js"
],
"subscribers": [
"built/subscriber/**/*.js"
],
"cli": {
"entitiesDir": "lib/entity",
"migrationsDir": "lib/migration",
"subscribersDir": "lib/subscriber"
}
}

1119
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,11 +27,13 @@
"highlight.js": "^9.13.1", "highlight.js": "^9.13.1",
"markdown-it": "^8.4.2", "markdown-it": "^8.4.2",
"morgan": "^1.9.1", "morgan": "^1.9.1",
"node-sass": "^4.11.0", "node-sass": "^4.12.0",
"pg": "^7.11.0",
"reflect-metadata": "^0.1.13",
"request": "^2.88.0", "request": "^2.88.0",
"sanitize-html": "^1.20.0", "sanitize-html": "^1.20.0",
"sqlite3": "^4.0.6", "typeorm": "^0.2.18",
"typescript": "^3.2.2", "typescript": "^3.5.2",
"uuid": "^3.3.2" "uuid": "^3.3.2"
} }
} }

View File

@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
/* Basic Options */ /* Basic Options */
"target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
@ -21,7 +22,7 @@
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */ // "strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */ // "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
@ -54,8 +55,8 @@
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */ /* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */ /* Advanced Options */
"resolveJsonModule": true /* Include modules imported with '.json' extension */ "resolveJsonModule": true /* Include modules imported with '.json' extension */