Replaces sqlite with postgres and TypeORM
This commit is contained in:
parent
63c679d859
commit
8404479d91
|
@ -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;
|
||||||
|
@ -50,4 +55,4 @@ export interface Actor {
|
||||||
publicKeyPem: string;
|
publicKeyPem: string;
|
||||||
};
|
};
|
||||||
icon: string | object | (string | object)[];
|
icon: string | object | (string | object)[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,54 +34,76 @@ 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();
|
||||||
res.status(500).end(err);
|
} catch (err) {
|
||||||
return;
|
res.status(500).end(err);
|
||||||
}
|
}
|
||||||
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");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) => {
|
return {
|
||||||
if (err) {
|
id: it.id,
|
||||||
reject(err);
|
content: it.content,
|
||||||
} else {
|
published: it.published,
|
||||||
const comments = rows.map(row => {
|
inReplyTo: it.inReplyTo,
|
||||||
return {
|
author: {
|
||||||
id: row.comment_id,
|
id: it.actor.id,
|
||||||
content: row.content,
|
name: it.actor.displayName,
|
||||||
published: row.published,
|
icon: it.actor.iconURL
|
||||||
inReplyTo: row.in_reply_to,
|
} as ActorObject
|
||||||
author: {
|
} as Comment;
|
||||||
id: row.actor_id,
|
});
|
||||||
name: row.display_name,
|
} catch (err) {
|
||||||
icon: row.icon_url
|
console.log("Couldn't load comments: ", err);
|
||||||
} as Actor
|
return [];
|
||||||
} as Comment;
|
}
|
||||||
});
|
//return new Promise((resolve, reject) => {
|
||||||
resolve(comments);
|
//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();
|
||||||
|
//});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
publicKey: {
|
||||||
id: result.id,
|
publicKeyPem: actor.publicKeyPem
|
||||||
name: result.display_name,
|
|
||||||
inbox: result.inbox,
|
|
||||||
icon: result.icon_url,
|
|
||||||
publicKey: {
|
|
||||||
publicKeyPem: result.public_key_pem
|
|
||||||
}
|
|
||||||
} as Actor);
|
|
||||||
} else {
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
} 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 {
|
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);
|
// convert to a Set to deduplicate inboxes
|
||||||
return;
|
(new Set(inboxes)).forEach(inbox => {
|
||||||
}
|
console.log(`Federating ${activity.object.id} to ${inbox}`);
|
||||||
const inboxes = results.map(it => "https://" + new URL(it.inbox).host + "/inbox");
|
signAndSend(activity, 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) {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
res.end();
|
} 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) {
|
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 })
|
||||||
res.end();
|
.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) {
|
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();
|
||||||
|
}
|
||||||
|
|
|
@ -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") {
|
||||||
|
@ -61,4 +60,4 @@ function parseSignature(signature: string): Map<string, string> {
|
||||||
map.set(key, unquoted);
|
map.set(key, unquoted);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[];
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
38
lib/index.ts
38
lib/index.ts
|
@ -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 => {
|
||||||
|
|
||||||
|
//}).catch(console.error);
|
||||||
|
|
||||||
db.run("CREATE TABLE IF NOT EXISTS followers (id TEXT PRIMARY KEY, inbox TEXT)");
|
//const db = new (sqlite3.verbose().Database)(process.env.DB_PATH!);
|
||||||
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 followers (id TEXT PRIMARY KEY, inbox 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)")
|
//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);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,10 +55,10 @@
|
||||||
// "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 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue