Hello Typescript!

This commit is contained in:
undefined committed 2020-07-07 19:58:21 +08:00
1 parent c9b6412527
commit ba03d7a7bf
98 files changed
+3186 -2616

No files matched your search

@@ -1,9 +1,9 @@
const { EventEmitter } = require('events');
const cluster = require('cluster');
import { EventEmitter } from 'events';
import cluster from 'cluster';
const bus = new EventEmitter();
function subscribe(events, handler, funcName) {
export function subscribe(events, handler, funcName) {
if (!funcName) {
for (const event of events) bus.on(event, handler);
} else {
@@ -14,7 +14,7 @@ function subscribe(events, handler, funcName) {
}
}
function unsubscribe(events, handler, funcName) {
export function unsubscribe(events, handler, funcName) {
if (!funcName) {
for (const event of events) bus.off(event, handler);
} else {
@@ -27,7 +27,7 @@ function unsubscribe(events, handler, funcName) {
}
}
function publish(event, payload, isMaster = true) {
export function publish(event, payload, isMaster = true) {
// Process forked by pm2 would also have process.send
if (isMaster && process.send && !cluster.isMaster) {
process.send({
@@ -40,6 +40,8 @@ function publish(event, payload, isMaster = true) {
}
}
global.Hydro.service.bus = module.exports = {
global.Hydro.service.bus = {
subscribe, unsubscribe, publish,
};
export default global.Hydro.service.bus;
-22
View File
@@ -1,22 +0,0 @@
const Mongo = require('mongodb');
const bus = require('./bus');
const options = require('../options');
let mongourl = 'mongodb://';
if (options.username) mongourl += `${options.username}:${options.password}@`;
mongourl += `${options.host}:${options.port}/${options.name}`;
/**
* @type {import('mongodb').Db}
*/
let db = null;
Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true })
.then((Client) => {
db = Client.db(options.name);
db.Client = Client;
bus.publish('system_database_connected', null);
});
global.Hydro.service.db = module.exports = {
collection: (c) => db.collection(c),
dropDatabase: () => db.dropDatabase(),
};
+23
View File
@@ -0,0 +1,23 @@
import mongodb from 'mongodb';
import * as bus from './bus';
import options from '../options';
let mongourl = 'mongodb://';
if (options.username) mongourl += `${options.username}:${options.password}@`;
mongourl += `${options.host}:${options.port}/${options.name}`;
export let db: mongodb.Db = null;
mongodb.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true })
.then((Client) => {
db = Client.db(options.name);
bus.publish('system_database_connected', null);
});
export function collection(c: string) {
return db.collection(c);
}
global.Hydro.service.db = { collection };
export default { collection };
-6
View File
@@ -1,6 +0,0 @@
const { GridFSBucket } = require('mongodb');
const db = require('./db');
if (!db.s) db.s = {};
global.Hydro.service.gridfs = module.exports = new GridFSBucket(db);
+8
View File
@@ -0,0 +1,8 @@
import { GridFSBucket } from 'mongodb';
import { db } from './db';
const exp = new GridFSBucket(db);
global.Hydro.service.gridfs = exp;
export default exp;
@@ -1,5 +1,5 @@
const db = require('./db');
const sysinfo = require('../lib/sysinfo');
import * as db from './db';
import * as sysinfo from '../lib/sysinfo';
const coll = db.collection('status');
@@ -23,4 +23,6 @@ async function postInit() {
setInterval(update, 60 * 1000);
}
global.Hydro.service.monitor = module.exports = { postInit };
global.Hydro.service.monitor = { postInit };
export default { postInit };
@@ -1,24 +1,26 @@
const assert = require('assert');
const path = require('path');
const os = require('os');
const _ = require('lodash');
const { ObjectID } = require('mongodb');
const Koa = require('koa');
const yaml = require('js-yaml');
const morgan = require('koa-morgan');
const Body = require('koa-body');
const Router = require('koa-router');
const cache = require('koa-static-cache');
const sockjs = require('sockjs');
const http = require('http');
const validator = require('../lib/validator');
const template = require('../lib/template');
const misc = require('../lib/misc');
const user = require('../model/user');
const system = require('../model/system');
const blacklist = require('../model/blacklist');
const token = require('../model/token');
const opcount = require('../model/opcount');
import assert from 'assert';
import path from 'path';
import os from 'os';
import http from 'http';
import _ from 'lodash';
import { ObjectID } from 'mongodb';
import Koa from 'koa';
import yaml from 'js-yaml';
import morgan from 'koa-morgan';
import Body from 'koa-body';
import Router from 'koa-router';
import cache from 'koa-static-cache';
import sockjs from 'sockjs';
import { render } from '../lib/template';
import * as validator from '../lib/validator';
import * as misc from '../lib/misc';
import * as user from '../model/user';
import * as system from '../model/system';
import * as blacklist from '../model/blacklist';
import * as token from '../model/token';
import * as opcount from '../model/opcount';
const {
UserNotFoundError, BlacklistedError, PermissionError,
UserFacingError, ValidationError, PrivilegeError,
@@ -31,21 +33,21 @@ const app = new Koa();
let server;
const router = new Router();
const _validateObjectId = (id, key) => {
const _validateObjectId = (id: string | ObjectID, key: string) => {
if (ObjectID.isValid(id)) return new ObjectID(id);
throw new ValidationError(key);
};
const _bool = (val) => !!val;
const _splitAndTrim = (val) => {
const _bool = (val: any) => !!val;
const _splitAndTrim = (val: string) => {
const t = val.split(',').map((i) => i.trim().split('+'));
return _.flatten(t);
};
const _date = (val) => {
const _date = (val: string) => {
const d = val.split('-');
assert(d.length === 3);
return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
};
const _time = (val) => {
const _time = (val: string) => {
const t = val.split(':');
assert(t.length === 2);
return `${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`;
@@ -67,29 +69,32 @@ const validate = {
tag: _splitAndTrim,
beginAtDate: _date,
beginAtTime: _time,
pid: (pid) => (Number.isSafeInteger(parseInt(pid, 10)) ? parseInt(pid, 10) : pid),
pid: (pid: string) => (Number.isSafeInteger(parseInt(pid, 10)) ? parseInt(pid, 10) : pid),
content: validator.checkContent,
title: validator.checkTitle,
uid: (uid) => parseInt(validator.checkUid(uid), 10),
uid: (uid: string) => parseInt(validator.checkUid(uid), 10),
password: validator.checkPassword,
mail: validator.checkEmail,
uname: validator.checkUname,
page: (page) => {
if (Number.isSafeInteger(parseInt(page, 10))) page = parseInt(page, 10);
page: (_page: string) => {
let page: number;
if (Number.isSafeInteger(parseInt(_page, 10))) page = parseInt(_page, 10);
if (page <= 0) throw new ValidationError('page');
return page;
},
duration: (duration) => {
if (!Number.isNaN(parseFloat(duration))) duration = parseFloat(duration);
duration: (_duration: string) => {
let duration: number;
if (!Number.isNaN(parseFloat(_duration))) duration = parseFloat(_duration);
if (duration <= 0) throw new ValidationError('duration');
return duration;
},
pids: (pids) => {
const res = pids.split(',').map((i) => i.trim());
for (const i in res) {
if (Number.isSafeInteger(parseInt(res[i], 10))) res[i] = parseInt(res[i], 10);
pids: (pids: string) => {
const src = pids.split(',').map((i) => i.trim());
const d: number[] = [];
for (const i in src) {
if (Number.isSafeInteger(parseInt(src[i], 10))) d[i] = parseInt(src[i], 10);
}
return res;
return d;
},
role: validator.checkRole,
penaltyRules: (penaltyRules) => {
@@ -122,15 +127,16 @@ async function prepare() {
}
const HandlerMixin = (Class) => class extends Class {
async renderHTML(name, context) {
async renderHTML(name: string, context: any): Promise<string> {
if (enableLog) console.time(name);
this.hasPerm = (perm) => this.user.hasPerm(perm);
// FIXME fix this ugly hack
this._user = { ...this.user, gravatar: misc.gravatar(this.user.gravatar, 128) };
const res = await template.render(name, Object.assign(context, {
const res = await render(name, Object.assign(context, {
handler: this,
// @ts-ignore
url: (...args) => this.url(...args),
_: (str) => (str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''),
_: (str: string) => (str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''),
user: this.user,
}));
if (enableLog) console.timeEnd(name);
@@ -185,7 +191,7 @@ const HandlerMixin = (Class) => class extends Class {
url(name, kwargs = {}) {
let res = '#';
const args = { ...kwargs };
const args: any = { ...kwargs };
try {
if (this.args.domainId !== 'system') {
name += '_with_domainId';
@@ -203,11 +209,45 @@ const HandlerMixin = (Class) => class extends Class {
}
};
const Handler = HandlerMixin(class {
/**
* @param {import('koa').Context} ctx
*/
constructor(ctx, args) {
export const Handler = HandlerMixin(class {
UIContext: any;
args: any;
ctx: Koa.Context;
request: {
host: string,
hostname: string,
ip: string,
headers: any,
cookies: any,
body: any,
files: any,
query: any,
path: string,
params: any,
referer: any,
json: boolean,
};
response: {
body: any,
type: string,
status: number,
template: string | undefined,
redirect: string | undefined,
disposition: string | undefined,
attachment: (name: string) => void,
};
session: any;
csrfToken: string;
user: any;
constructor(ctx: Koa.Context, args: any) {
this.ctx = ctx;
this.args = args;
this.request = {
@@ -222,6 +262,7 @@ const Handler = HandlerMixin(class {
path: ctx.path,
params: ctx.params,
referer: ctx.request.headers.referer || '/',
json: (ctx.request.headers.accept || '').includes('application/json'),
};
this.response = {
body: {},
@@ -230,6 +271,7 @@ const Handler = HandlerMixin(class {
template: null,
redirect: null,
attachment: (name) => ctx.attachment(name),
disposition: null,
};
this.UIContext = {
cdn_prefix: '/',
@@ -238,17 +280,18 @@ const Handler = HandlerMixin(class {
this.session = {};
}
async render(name, context) {
async render(name: string, context: any) {
// @ts-ignore
this.response.body = await this.renderHTML(name, context);
this.response.type = 'text/html';
}
back(body) {
back(body: any) {
this.response.body = body || this.response.body || {};
this.response.redirect = this.request.headers.referer || '/';
}
binary(data, name) {
binary(data: any, name: string) {
this.response.body = data;
this.response.template = null;
this.response.type = 'application/octet-stream';
@@ -275,7 +318,6 @@ const Handler = HandlerMixin(class {
}
async init({ domainId }) {
this.preferJson = (this.request.headers.accept || '').includes('application/json');
await Promise.all([
this.getSession(),
this.getBdoc(),
@@ -286,7 +328,7 @@ const Handler = HandlerMixin(class {
]);
}
async checkCsrfToken(csrfToken) {
async checkCsrfToken(csrfToken: any) {
const sdoc = await token.get(csrfToken, token.TYPE_CSRF_TOKEN, false);
if (!sdoc || sdoc.uid !== this.user._id) throw new CsrfTokenError(csrfToken);
}
@@ -296,7 +338,7 @@ const Handler = HandlerMixin(class {
await this.renderBody();
} catch (error) {
this.response.status = error instanceof UserFacingError ? error.code : 500;
if (this.preferJson) this.response.body = { error };
if (this.request.json) this.response.body = { error };
else await this.render(error instanceof UserFacingError ? 'error.html' : 'bsod.html', { error });
}
await this.putResponse();
@@ -304,9 +346,9 @@ const Handler = HandlerMixin(class {
}
async renderBody() {
if (!this.response.redirect && !this.preferJson) {
if (!this.response.redirect && !this.request.json) {
if (this.response.body || this.response.template) {
if (this.request.query.noTemplate || this.preferJson) return;
if (this.request.query.noTemplate || this.request.json) return;
const templateName = this.request.query.template || this.response.template;
if (templateName) {
this.response.body = this.response.body || {};
@@ -318,7 +360,7 @@ const Handler = HandlerMixin(class {
async putResponse() {
if (this.response.disposition) this.ctx.set('Content-Disposition', this.response.disposition);
if (this.response.redirect && !this.preferJson) {
if (this.response.redirect && !this.request.json) {
this.ctx.response.type = 'application/octet-stream';
this.ctx.response.status = 302;
this.ctx.redirect(this.response.redirect);
@@ -331,7 +373,7 @@ const Handler = HandlerMixin(class {
this.ctx.response.body = this.response.body;
this.ctx.response.status = this.response.status || 200;
}
this.ctx.response.type = this.preferJson
this.ctx.response.type = this.request.json
? 'application/json'
: this.response.type
? this.response.type
@@ -367,7 +409,7 @@ const Handler = HandlerMixin(class {
},
);
}
const cookie = { secure: await system.get('session.secure') };
const cookie: any = { secure: await system.get('session.secure') };
if (this.session.save) {
cookie.expires = this.session.expireAt;
cookie.maxAge = expireSeconds;
@@ -469,36 +511,47 @@ const Checker = (permPrivChecker) => {
};
};
function Route(name, route, RouteHandler, ...permPrivChecker) {
export function Route(name, route, RouteHandler, ...permPrivChecker) {
const checker = Checker(permPrivChecker);
router.all(name, route, (ctx) => handle(ctx, RouteHandler, checker));
router.all(`${name}_with_domainId`, `/d/:domainId${route}`, (ctx) => handle(ctx, RouteHandler, checker));
}
const ConnectionHandler = HandlerMixin(class {
/**
* @param {import('sockjs').Connection} conn
*/
constructor(conn) {
export const ConnectionHandler = HandlerMixin(class {
conn: sockjs.Connection;
request: {
params: any
headers: any
ip: string
}
session: any
user: any
constructor(conn: sockjs.Connection) {
this.conn = conn;
this.request = {
params: {},
headers: conn.headers,
ip: this.conn.remoteAddress,
};
const p = (conn.url.split('?')[1] || '').split('&');
this.session = {};
const p: any = (conn.url.split('?')[1] || '').split('&');
for (const i in p) p[i] = p[i].split('=');
for (const i in p) this.request.params[p[i][0]] = decodeURIComponent(p[i][1]);
}
send(data) {
send(data: JSON) {
this.conn.write(JSON.stringify(data));
}
close(code, reason) {
this.conn.close(code, reason);
close(code: number, reason: string) {
this.conn.close(code.toString(), reason);
}
onerror(err) {
onerror(err: Error) {
console.error(err);
this.close(1001, err.toString());
}
@@ -516,7 +569,16 @@ const ConnectionHandler = HandlerMixin(class {
}
});
function Connection(name, prefix, RouteConnHandler, ...permPrivChecker) {
interface IConnectionHandler {
// @ts-ignore
new(conn: sockjs.Connection): ConnectionHandler;
}
export function Connection(
name: string, prefix: string,
RouteConnHandler: IConnectionHandler,
...permPrivChecker: Array<number | string | Function>
) {
const sock = sockjs.createServer({ prefix });
const checker = Checker(permPrivChecker);
sock.on('connection', async (conn) => {
@@ -557,16 +619,16 @@ function Connection(name, prefix, RouteConnHandler, ...permPrivChecker) {
sock.installHandlers(server);
}
function Middleware(middleware) {
export function Middleware(middleware: Koa.Middleware) {
app.use(middleware);
}
function Validate(key, func) {
export function Validate(key: string, func: Function) {
if (validate[key]) validate[key].push(func);
else validate[key] = [func];
}
async function start() {
export async function start() {
const [disableLog, port] = await system.getMany(['server.log', 'server.port']);
if (!disableLog) {
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
@@ -577,6 +639,8 @@ async function start() {
console.log('Server listening at: %s', port);
}
global.Hydro.service.server = module.exports = {
global.Hydro.service.server = {
Handler, ConnectionHandler, Route, Connection, Middleware, Validate, prepare, start,
};
export default global.Hydro.service.server;