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

View File

@ -1,14 +1,19 @@
import express, { Router, Request, Response } from "express";
import { Page, PostMetadata } from "../metadata";
import { Article } from "./activity";
import { Database } from "sqlite3";
import { ArticleObject } from "./activity";
import Article from "../entity/Article";
import { getConnection } from "typeorm";
import uuidv4 from "uuid/v4";
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) {
const postMeta = <PostMetadata>post.metadata;
if (await repository.findOne(postMeta.permalink)) {
continue;
}
const articleObject = {
"@context": [
"https://www.w3.org/ns/activitystreams",
@ -29,52 +34,74 @@ export async function setup(posts: Page[], db: Database) {
"name": postMeta.title,
"content": post.text
};
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);
});
const article = new Article();
article.id = postMeta.permalink;
article.articleObject = articleObject;
article.conversation = articleObject.conversation;
article.hasFederated = false;
await getConnection().manager.save(article);
//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][]> {
return new Promise((resolve, reject) => {
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 async function toFederate(): Promise<[string, ArticleObject][]> {
return new Promise(async (resolve, reject) => {
const articles: Article[] = await getConnection().createQueryBuilder().select("article").from(Article, "article").where("article.hasFederated = :hasFederated", { hasFederated: false }).getMany();
let result: [string, ArticleObject][] = [];
articles.forEach(it => {
result.push([it.id, it.articleObject]);
});
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) {
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"]);
console.log(best);
if (best === "text/html") {
next();
} else if (best === "application/activity+json") {
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);
});
const id = `/${req.params.category}/${req.params.year}/${req.params.slug}/`;
const repository = getConnection().getRepository(Article);
try {
const article = await repository.findOne(id);
res.type("application/activity+json").json(article.articleObject).end();
} catch (err) {
res.status(500).end(err);
}
//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 {
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 { Database } from "sqlite3";
import { Note, Actor } from "./activity";
import { NoteObject, ActorObject } from "./activity";
import { getCachedActor } from "./federate";
import { getConnection } from "typeorm";
import Note from "../entity/Note";
import Article from "../entity/Article";
const domain = process.env.DOMAIN;
interface Comment extends Note {
author: Actor;
interface Comment {
id: string;
content: string;
published: string;
inReplyTo: string;
author: ActorObject;
}
async function getConversationComments(conversation: string, db: Database): Promise<Comment[]> {
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 Actor
} as Comment;
});
resolve(comments);
}
})
});
async function getConversationComments(conversation: string): Promise<Comment[]> {
try {
const notes = await getConnection().getRepository(Note).find({ where: { conversation }, relations: ["actor"] });
return notes.map(it => {
return {
id: it.id,
content: it.content,
published: it.published,
inReplyTo: it.inReplyTo,
author: {
id: it.actor.id,
name: it.actor.displayName,
icon: it.actor.iconURL
} as ActorObject
} as Comment;
});
} 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) {
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}`, db);
const comments = await getConversationComments(`https://${domain}/ap/conversation/${req.params.id}`);
res.json(comments).end();
});
router.get("/comments", (req, res) => {
router.get("/comments", async (req, res) => {
const id = req.query.id;
if (!id) {
res.sendStatus(400).end();
return;
}
const db = req.app.get("db") as Database;
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);
try {
const article = await getConnection().getRepository(Article).findOne(id);
const comments = await getConversationComments(article.conversation);
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 uuidv4 from "uuid/v4";
import request from "request";
import { Database } from "sqlite3";
import { Activity, Article, Create, Actor, Follow, Accept } from "./activity";
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: Article): Create {
function createActivity(article: ArticleObject): CreateActivity {
const uuid = uuidv4();
const createObject = {
"@context": [
@ -24,63 +26,87 @@ function createActivity(article: Article): Create {
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) {
try {
const cached = await getCachedActor(url, db);
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, db);
if (remote) cacheActor(remote);
return remote;
}
export async function getCachedActor(url: string, db: Database): Promise<Actor | null> {
return new Promise((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 Actor);
} else {
resolve(null);
}
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(actor: Actor, db: Database) {
async function cacheActor(actorObject: ActorObject) {
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);
});
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<Actor | null> {
async function fetchActor(url: string): Promise<ActorObject | null> {
return new Promise((resolve, reject) => {
request({
url,
@ -91,7 +117,7 @@ async function fetchActor(url: string): Promise<Actor | null> {
json: true
}, (err, res) => {
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) {
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);
});
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 function federate(toFederate: [string, Article][], db: Database) {
export default async function federate(toFederate: [string, ArticleObject][]) {
for (const [id, article] of toFederate) {
sendToFollowers(createActivity(article), db);
db.run("UPDATE articles SET has_federated = 1 WHERE id = $id", {
$id: id
});
break;
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;
}
}

View File

@ -1,10 +1,13 @@
import { Router, Request, Response } from "express";
import uuidv4 from "uuid/v4";
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 { URL } from "url";
import sanitizeHtml from "sanitize-html";
import Note from "../entity/Note";
import { getConnection } from "typeorm";
import Actor from "../entity/Actor";
const domain = process.env.DOMAIN;
@ -34,42 +37,43 @@ async function handleFollow(activity: Activity, req: Request, res: Response) {
res.end(); // TODO: handle this better
return;
}
const follow = activity as Follow;
const follow = activity as FollowActivity;
if (follow.object !== `https://${domain}/ap/actor`) {
res.end();
return;
}
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 actor = await getActor(follow.actor, true); // always force re-fetch the actor on follow
if (!actor) {
// if the actor ceases existing between the time the Follow is sent and when receive it, ignore it and end the request
res.end();
return;
}
const acceptObject = <Accept>{
"@context": [
"https://www.w3.org/ns/activitystreams",
// "https://w3id.org/security/v1"
],
const acceptObject = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": `https://${domain}/ap/${uuidv4()}`,
"type": "Accept",
"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;
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);
});
await getConnection().createQueryBuilder()
.update(Actor)
.set({ isFollower: true })
.where("id = :id", { id: actor.id })
.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();
}
async function handleCreate(activity: Activity, req: Request, res: Response) {
const create = activity as Create;
const create = activity as CreateActivity;
if (create.object.type == "Note") {
handleCreateNote(create, req, res);
} else {
@ -90,38 +94,62 @@ function sanitizeStatus(content: string): string {
});
}
async function handleCreateNote(create: Create, req: Request, res: Response) {
const note = create.object as Note;
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);
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,
$content: sanitizedContent,
$attributed_to: note.attributedTo,
$in_reply_to: note.inReplyTo,
$conversation: note.conversation,
$published: note.published
}, (err) => {
if (err) console.error(`Encountered error storing reply ${note.id}`, err);
res.end();
});
async function handleCreateNote(create: CreateActivity, req: Request, res: Response) {
const noteObject = create.object as NoteObject;
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(noteObject.attributedTo);
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);
}
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) {
const deleteActivity = activity as Delete;
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();
})
const deleteActivity = activity as DeleteActivity;
try {
await getConnection().getRepository(Note).createQueryBuilder()
.delete()
.from(Note, "note")
.where("note.id = :id", { id: deleteActivity.object })
.andWhere("note.actor = :actor", { actor: deleteActivity.actor })
.execute();
} catch (err) {
console.error(`Encountered error deleting ${deleteActivity.object}`, err);
}
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) {
const undo = activity as Undo;
const undo = activity as UndoActivity;
if (undo.object.type === "Follow") {
handleUndoFollow(undo, req, res);
} else {
@ -129,17 +157,28 @@ async function handleUndo(activity: Activity, req: Request, res: Response) {
}
}
async function handleUndoFollow(undo: Undo, req: Request, res: Response) {
const follow = undo.object as Follow;
if (follow.object !== `https://${domain}/ap/actor`) {
async function handleUndoFollow(undo: UndoActivity, req: Request, res: Response) {
const follow = undo.object as FollowActivity;
if (follow.object !== `https://${domain}/ap/actor` || undo.actor !== follow.actor) {
res.end();
return;
}
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);
});
try {
await getConnection().createQueryBuilder()
.update(Actor)
.set({ isFollower: false })
.where("id = :id", { id: follow.actor })
.execute();
} catch (err) {
console.error(`Error handling unfollow from ${follow.actor}`, err);
}
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();
return;
}
const db = req.app.get("db") as Database;
const actor = await getActor(req.body.actor as string, db);
const actor = await getActor(req.body.actor as string);
if (actor && validate(req, actor.publicKey.publicKeyPem)) {
next();
} else {
// 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) {
// probably caused by Delete activity for an actor
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 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)");
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)")
//}).catch(console.error);
//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 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[]> {
generators.copy();
@ -32,18 +38,24 @@ async function generate(): Promise<Page[]> {
return posts;
}
const app = express();
app.set("db", db);
app.use(morgan("dev"));
app.use(bodyParser.json({ type: "application/activity+json" }));
//const app = express();
//app.set("db", db);
//app.use(morgan("dev"));
//app.use(bodyParser.json({ type: "application/activity+json" }));
//db.run("DELETE FROM articles");
(async () => {
const app = express();
app.use(morgan("dev"))
app.use(bodyParser.json({ type: "application/activity+json" }));
const connection = await createConnection();
const posts = await generate();
await activitypub.articles.setup(posts, db);
const toFederate = await activitypub.articles.toFederate(db);
await activitypub.articles.setup(posts);
const toFederate = await activitypub.articles.toFederate();
const apRouter = Router();
apRouter.use(validateHttpSig);
@ -60,7 +72,7 @@ app.use(bodyParser.json({ type: "application/activity+json" }));
app.listen(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"
}
}

1117
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",
"markdown-it": "^8.4.2",
"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",
"sanitize-html": "^1.20.0",
"sqlite3": "^4.0.6",
"typescript": "^3.2.2",
"typeorm": "^0.2.18",
"typescript": "^3.5.2",
"uuid": "^3.3.2"
}
}

View File

@ -1,5 +1,6 @@
{
"compilerOptions": {
"experimentalDecorators": true,
/* Basic Options */
"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'. */
@ -21,7 +22,7 @@
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* 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. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "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. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"resolveJsonModule": true /* Include modules imported with '.json' extension */