From 627e64a9011f3e1714bf1598859a9ca6b8669695 Mon Sep 17 00:00:00 2001 From: undefined Date: Sun, 3 May 2020 15:33:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AB=99=E5=86=85=E6=B6=88?= =?UTF-8?q?=E6=81=AF=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=AE=A8=E8=AE=BA=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E4=B8=A2=E5=A4=B1=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/buildModule.js | 20 ++ build/buildTemplate.js | 44 +-- build/ignoreFailure.js | 5 + build/index.js | 17 +- build/template.js | 43 +++ hydro/development.js | 41 +-- hydro/handler/home.js | 61 ++-- hydro/handler/notfound.js | 4 +- hydro/handler/user.js | 2 +- hydro/install.js | 7 +- hydro/lib/i18n.js | 12 +- hydro/lib/loader.js | 49 --- hydro/lib/markdown.js | 10 +- hydro/lib/misc.js | 2 +- hydro/lib/nav.js | 18 + hydro/lib/template.js | 50 ++- hydro/loader.js | 134 ++++++++ hydro/model/discussion.js | 7 +- hydro/model/message.js | 72 ++++ hydro/model/user.js | 20 +- hydro/permission.js | 2 +- hydro/service/cron.js | 33 ++ hydro/service/db.js | 2 +- hydro/service/server.js | 50 ++- package.json | 8 +- templates/components/comments_discussion.html | 10 +- templates/components/user.html | 2 +- templates/discussion_detail.html | 2 +- templates/discussion_main_or_node.html | 2 +- templates/domain_manage_dashboard.html | 2 +- templates/home_messages.html | 1 + templates/partials/discussion_list.html | 2 +- templates/partials/nav.html | 25 +- templates/record_detail_status.html | 4 +- templates/record_detail_summary.html | 2 +- templates/record_main_tr.html | 2 +- templates/training_detail.html | 2 +- templates/user_detail.html | 2 +- ui/build/config/webpack.js | 3 +- .../MessagePadDialogueContentContainer.js | 8 +- .../MessagePadDialogueListContainer.js | 12 +- .../messagepad/MessagePadInputContainer.js | 2 +- .../messagepad/reducers/dialogues.js | 12 +- yarn.lock | 314 ++++-------------- 44 files changed, 582 insertions(+), 540 deletions(-) create mode 100644 build/ignoreFailure.js create mode 100644 build/template.js delete mode 100644 hydro/lib/loader.js create mode 100644 hydro/lib/nav.js create mode 100644 hydro/loader.js create mode 100644 hydro/model/message.js create mode 100644 hydro/service/cron.js diff --git a/build/buildModule.js b/build/buildModule.js index 72f30cc31..f798c24a9 100644 --- a/build/buildModule.js +++ b/build/buildModule.js @@ -1,8 +1,10 @@ /* eslint-disable import/no-extraneous-dependencies */ const fs = require('fs'); const webpack = require('webpack'); +const yaml = require('js-yaml'); const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); const root = require('./root'); +const template = require('./template'); const exist = (name) => { try { @@ -50,6 +52,24 @@ const build = async (type) => { resolve(); }); }); + for (const i of modules) { + if (!i.startsWith('.')) { + if (exist(`module/${i}/locale`)) { + const locales = fs.readdirSync(root(`module/${i}/locale`)); + const lang = {}; + for (const j of locales) { + const content = fs.readFileSync(root(`module/${i}/locale/${j}`)).toString(); + lang[i.split('.')[0]] = yaml.safeLoad(content); + } + const file = root(`.build/module/${i}/locale.json`); + fs.writeFileSync(file, JSON.stringify(lang)); + } + if (exist(`module/${i}/template`)) { + const file = root(`.build/module/${i}/template.yaml`); + fs.writeFileSync(file, yaml.safeDump(template(`module/${i}/template`))); + } + } + } }; module.exports = build; diff --git a/build/buildTemplate.js b/build/buildTemplate.js index b1a18ec9d..fc5c55e11 100644 --- a/build/buildTemplate.js +++ b/build/buildTemplate.js @@ -1,45 +1,15 @@ const fs = require('fs'); const yaml = require('js-yaml'); const root = require('./root'); - -function getFiles(folder) { - const files = []; - const f = fs.readdirSync(root(folder)); - for (const i of f) { - if (!i.startsWith('.')) { - if (fs.statSync(root(`${folder}/${i}`)).isDirectory()) { - const g = getFiles(`${folder}/${i}`); - for (const j of g) files.push(`${i}/${j}`); - } else files.push(i); - } - } - return files; -} +const template = require('./template'); const build = async () => { - const files = getFiles('templates'); - const exclude = ['partials/training_default.json', 'partials/problem_default.md']; - const templates = {}; - for (const i of files) { - let template = fs.readFileSync(root(`templates/${i}`)).toString(); - if (!exclude.includes(i)) { - template = template - .trim() - .replace(/ *\n */gmi, ' ') - .replace(/, /gmi, ',') - .replace(/%} {%/gmi, '%}{%') - .replace(/ %}/gmi, '%}') - .replace(/{% /gmi, '{%') - .replace(/> <') - .replace(/}} {{/gmi, '>{{') - .replace(/{{ /gmi, '{{') - .replace(/ }}/gmi, '}}') - .trim(); - } - templates[i] = template; - } - fs.writeFileSync(root('.build/template.yaml'), yaml.safeDump(templates)); + const exclude = [ + 'partials/training_default.json', + 'partials/problem_default.md', + 'bsod.html', + ]; + fs.writeFileSync(root('.build/template.yaml'), yaml.safeDump(template('templates', exclude))); }; module.exports = build; diff --git a/build/ignoreFailure.js b/build/ignoreFailure.js new file mode 100644 index 000000000..9e53982ef --- /dev/null +++ b/build/ignoreFailure.js @@ -0,0 +1,5 @@ +module.exports = function ignoreFailure(func, ...params) { + try { + func(...params); + } catch (e) { } // eslint-disable-line no-empty +}; diff --git a/build/index.js b/build/index.js index b05876b68..2ece9623f 100644 --- a/build/index.js +++ b/build/index.js @@ -1,18 +1,15 @@ const fs = require('fs'); const root = require('./root'); +const ignoreFailure = require('./ignoreFailure'); async function build(type) { if (!['development', 'production'].includes(type)) throw new Error(`Unknown type: ${type}`); - try { - fs.rmdirSync(root('.build')); - } catch (e) { } - fs.mkdirSync(root('.build')); - await Promise.all([ - require('./locales')(type), - require('./buildModule')(type), - require('./buildTemplate')(type), - require('./webpack')(type), - ]); + ignoreFailure(fs.mkdirSync, root('.cache')); + ignoreFailure(fs.mkdirSync, root('.build')); + await require('./locales')(type); + await require('./buildModule')(type); + await require('./buildTemplate')(type); + await require('./webpack')(type); } module.exports = build; diff --git a/build/template.js b/build/template.js new file mode 100644 index 000000000..6f3ec9e26 --- /dev/null +++ b/build/template.js @@ -0,0 +1,43 @@ +const fs = require('fs'); +const root = require('./root'); + +function getFiles(folder) { + const files = []; + const f = fs.readdirSync(root(folder)); + for (const i of f) { + if (!i.startsWith('.')) { + if (fs.statSync(root(`${folder}/${i}`)).isDirectory()) { + const g = getFiles(`${folder}/${i}`); + for (const j of g) files.push(`${i}/${j}`); + } else files.push(i); + } + } + return files; +} + +const build = (dir, exclude = []) => { + const files = getFiles(dir); + const templates = {}; + for (const i of files) { + let template = fs.readFileSync(root(`${dir}/${i}`)).toString(); + if (!exclude.includes(i)) { + template = template + .trim() + .replace(/ *\n */gmi, ' ') + .replace(/, /gmi, ',') + .replace(/%} {%/gmi, '%}{%') + .replace(/ %}/gmi, '%}') + .replace(/{% /gmi, '{%') + .replace(/> <') + .replace(/}} {{/gmi, '>{{') + .replace(/{{ /gmi, '{{') + .replace(/ }}/gmi, '}}') + .trim(); + } + templates[i] = template; + } + return templates; +}; + +module.exports = build; diff --git a/hydro/development.js b/hydro/development.js index 424bafae7..fce3f29c4 100644 --- a/hydro/development.js +++ b/hydro/development.js @@ -9,46 +9,7 @@ process.stdin.on('data', async (input) => { } }); -global.Hydro = { - handler: {}, - template: {}, -}; - -require('./lib/i18n'); -require('./utils'); - -const bus = require('./service/bus'); -const loader = require('./lib/loader'); - -async function run() { - await loader.prepare(); - await new Promise((resolve) => { - const h = () => { - console.log('Database connected'); - bus.unsubscribe(['system_database_connected'], h); - resolve(); - }; - bus.subscribe(['system_database_connected'], h); - require('./service/db'); - }); - require('./service/gridfs'); - require('./service/queue'); - const server = require('./service/server'); - const builtinHandler = [ - 'home', 'problem', 'record', 'judge', 'user', - 'contest', 'training', 'discussion', 'manage', 'import', - ]; - for (const i of builtinHandler) require(`./handler/${i}`); - await loader.model(); - await loader.handler(); - for (const i in global.Hydro.handler) { - await global.Hydro.handler[i].apply(); - } - const notfound = require('./handler/notfound'); - await notfound.apply(); - server.start(); -} -run().catch((e) => { +require('./loader')().catch((e) => { console.error(e); process.exit(1); }); diff --git a/hydro/handler/home.js b/hydro/handler/home.js index 16e33ddbf..b15503ced 100644 --- a/hydro/handler/home.js +++ b/hydro/handler/home.js @@ -4,9 +4,13 @@ const { } = require('../error'); const options = require('../options'); const bus = require('../service/bus'); -const { Route, Handler } = require('../service/server'); +const { + Route, Connection, Handler, ConnectionHandler, +} = require('../service/server'); +const misc = require('../lib/misc'); const md5 = require('../lib/md5'); const contest = require('../model/contest'); +const message = require('../model/message'); const user = require('../model/user'); const setting = require('../model/setting'); const discussion = require('../model/discussion'); @@ -114,6 +118,7 @@ class HomeSecurityHandler extends Handler { const sessions = await token.getSessionListByUid(this.user._id); for (const session in sessions) { if (tokenDigest === md5(session._id)) { + // eslint-disable-next-line no-await-in-loop await token.delete(session._id, token.TYPE_SESSION); return this.back(); } @@ -175,7 +180,7 @@ class HomeMessagesHandler extends Handler { udoc(udict, key) { // eslint-disable-line class-methods-use-this const udoc = udict[key]; if (!udoc) return; - const gravatar_url = misc.gravatar_url(udoc.gravatar); + const gravatar_url = misc.gravatar(udoc.gravatar); if (udoc.gravatar) udict[key] = { ...udoc, gravatar_url, gravatar: '' }; } @@ -192,11 +197,11 @@ class HomeMessagesHandler extends Handler { ]); // TODO(twd2): improve here: for (const mdoc of mdocs) { - this.modify_udoc(udict, mdoc.from); - this.modify_udoc(udict, mdoc.to); + this.udoc(udict, mdoc.from); + this.udoc(udict, mdoc.to); } this.response.body = { messages: mdocs, udict }; - this.response.template = 'home_message.html'; + this.response.template = 'home_messages.html'; } async postSendMessage({ uid, content }) { @@ -204,10 +209,10 @@ class HomeMessagesHandler extends Handler { const mdoc = await message.add(this.user._id, udoc._id, content); // TODO(twd2): improve here: // projection - mdoc.from = this.user; - this.modify_udoc(mdoc, 'from'); - mdoc.to = udoc; - this.modify_udoc(mdoc, 'to'); + mdoc.from_udoc = this.user; + this.udoc(mdoc, 'from'); + mdoc.to_udoc = udoc; + this.udoc(mdoc, 'to'); if (this.user._id !== uid) { await bus.publish(`user_message-${uid}`, { type: 'new', data: mdoc }); } @@ -231,32 +236,40 @@ class HomeMessagesHandler extends Handler { } } +class HomeMessagesConnectionHandler extends ConnectionHandler { + async prepare() { + bus.subscribe([`message_received-${this.user._id}`], this.onMessageReceived); + } + + async onMessageReceived(e) { + this.send(...e.value); + } + + async clearup() { + bus.unsubscribe(this.onMessageReceived); + } +} + async function apply() { Route('/', module.exports.HomeHandler); Route('/home/security', module.exports.HomeSecurityHandler); Route('/home/changeMail/:code', module.exports.UserChangemailWithCodeHandler); Route('/home/settings/:category', module.exports.HomeSettingsHandler); + Route('/home/messages', module.exports.HomeMessagesHandler); + Connection('/home/messages-conn', module.exports.HomeMessagesConnectionHandler); } global.Hydro.handler.home = module.exports = { - HomeHandler, HomeSecurityHandler, HomeSettingsHandler, UserChangemailWithCodeHandler, apply, + HomeHandler, + HomeSecurityHandler, + HomeSettingsHandler, + UserChangemailWithCodeHandler, + HomeMessagesHandler, + HomeMessagesConnectionHandler, + apply, }; /* -@app.connection_route('/home/messages-conn', 'home_messages-conn', global_route=True) -class HomeMessagesConnection(base.Connection): - @base.require_priv(builtin.PRIV_USER_PROFILE) - async def on_open(this): - await super(HomeMessagesConnection, this).on_open() - bus.subscribe(this.on_message_received, ['message_received-' + str(this.user['_id'])]) - - async def on_message_received(this, e): - this.send(**e['value']) - - async def on_close(this): - bus.unsubscribe(this.on_message_received) - - @app.route('/home/file', 'home_file', global_route=True) class HomeFileHandler(base.OperationHandler): def file_url(this, fdoc): diff --git a/hydro/handler/notfound.js b/hydro/handler/notfound.js index b1467deb1..c186d153f 100644 --- a/hydro/handler/notfound.js +++ b/hydro/handler/notfound.js @@ -2,8 +2,8 @@ const { Route, Handler } = require('../service/server'); const { NotFoundError } = require('../error'); class NotFoundHandler extends Handler { - async prepare() { - throw new NotFoundError(); // eslint-disable-line class-methods-use-this + async prepare() { // eslint-disable-line class-methods-use-this + throw new NotFoundError(); } } diff --git a/hydro/handler/user.js b/hydro/handler/user.js index 496a0da1f..c47d44c7a 100644 --- a/hydro/handler/user.js +++ b/hydro/handler/user.js @@ -169,7 +169,7 @@ class UserSearchHandler extends Handler { } for (const i in udocs) { if (udocs[i].gravatar) { - udocs[i].gravatar_url = misc.gravatar_url(udocs[i].gravatar); + udocs[i].gravatar_url = misc.gravatar(udocs[i].gravatar); } } this.response.body = udocs; diff --git a/hydro/install.js b/hydro/install.js index 2177fe5c7..4180457eb 100644 --- a/hydro/install.js +++ b/hydro/install.js @@ -10,7 +10,10 @@ async function run() { let mongourl = 'mongodb://'; if (options.db.username) mongourl += `${options.db.username}:${options.db.password}@`; mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`; - const Database = await Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }); + const Database = await Mongo.MongoClient.connect( + mongourl, + { useNewUrlParser: true, useUnifiedTopology: true }, + ); const db = Database.db(options.db.name); const collUser = db.collection('user'); const collRole = db.collection('role'); @@ -48,8 +51,6 @@ async function run() { }, udoc), ]); } - await collUser.createIndex('unameLower', { unique: true }); - await collUser.createIndex('mailLower', { sparse: true }); await collRole.insertMany(builtin.BUILTIN_ROLES); await collBlacklist.createIndex('expireAt', { expireAfterSeconds: 0 }); await collToken.createIndex([{ uid: 1 }, { tokenType: 1 }, { updateAt: -1 }], { sparse: true }); diff --git a/hydro/lib/i18n.js b/hydro/lib/i18n.js index acedbcb80..04626397b 100644 --- a/hydro/lib/i18n.js +++ b/hydro/lib/i18n.js @@ -21,13 +21,23 @@ String.prototype.format = function formatStr(...args) { } return result; }; + String.prototype.rawformat = function rawFormat(object) { const res = this.split('{@}'); return [res[0], object, res[1]]; }; + String.prototype.translate = function translate(language = 'zh_CN') { if (locales[language]) return locales[language][this] || this; return this; }; -module.exports = locales; +function load(data) { + for (const i in data) { + if (!locales[i]) locales[i] = data[i]; + else locales[i] = Object.assign(locales[i], data[i]); + } +} + +global.Hydro.locales = locales; +global.Hydro.lib.i18n = module.exports = load; diff --git a/hydro/lib/loader.js b/hydro/lib/loader.js deleted file mode 100644 index 6840ec43a..000000000 --- a/hydro/lib/loader.js +++ /dev/null @@ -1,49 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -let installed; -function root(name) { - return path.resolve(process.cwd(), name); -} -function exist(name) { - try { - fs.statSync(root(name)); - } catch (e) { - return false; - } - return true; -} - -const superRequire = (name) => { - let m; - try { - m = require(root(name)); // eslint-disable-line import/no-dynamic-require - } catch (e) { - m = __non_webpack_require__(root(name)); // eslint-disable-line no-undef - } - return m; -}; - -async function prepare() { - installed = fs.readdirSync(root('.build/module')); -} - -async function handler() { - for (const i of installed) { - if (exist(`.build/module/${i}/handler.js`)) { - superRequire(`.build/module/${i}/handler.js`); - console.log(`Handler init: ${i}`); - } - } -} - -async function model() { - for (const i of installed) { - if (exist(`.build/module/${i}/model.js`)) { - superRequire(`.build/module/${i}/model.js`); - console.log(`Model init: ${i}`); - } - } -} - -global.Hydro['lib.loader'] = module.exports = { prepare, handler, model }; diff --git a/hydro/lib/markdown.js b/hydro/lib/markdown.js index 4875892ad..b3e8ccfc0 100644 --- a/hydro/lib/markdown.js +++ b/hydro/lib/markdown.js @@ -1,15 +1,19 @@ -const hljs = require('highlight.js'); const MarkdownIt = require('markdown-it'); const katex = require('markdown-it-katex'); +const Prism = require('prismjs'); +const loadLanguages = require('prismjs/components/'); + +// FIXME doesnt work in webpack +loadLanguages(['js', 'javascript', 'go', 'c', 'pascal', 'hs', 'java', 'cs', 'cpp', 'py', 'ruby']); class Markdown extends MarkdownIt { constructor() { super({ linkify: true, highlight(str, lang) { - if (lang && hljs.getLanguage(lang)) { + if (lang && Prism.languages[lang]) { try { - return hljs.highlight(lang, str).value; + return Prism.highlight(str, Prism.languages[lang], lang); } catch (__) { } // eslint-disable-line no-empty } return ''; diff --git a/hydro/lib/misc.js b/hydro/lib/misc.js index f0a59a1c6..f76e0677d 100644 --- a/hydro/lib/misc.js +++ b/hydro/lib/misc.js @@ -1,6 +1,6 @@ const md5 = require('./md5'); -exports.gravatar_url = function gravatarUrl(email, size = 32) { +exports.gravatar = function gravatar(email, size = 32) { return `//gravatar.loli.net/avatar/${md5((email || '').toString().trim().toLowerCase())}?d=mm&s=${size}`; }; diff --git a/hydro/lib/nav.js b/hydro/lib/nav.js new file mode 100644 index 000000000..a8f562ec6 --- /dev/null +++ b/hydro/lib/nav.js @@ -0,0 +1,18 @@ +const permission = require('../permission'); + +global.Hydro.ui.nav = []; + +const Item = (path, name, prefix, perm) => { + global.Hydro.ui.nav.push({ + path, name, prefix, perm, + }); +}; + +Item('/', 'domain_main', 'domain_main'); +Item('/p', 'problem_main', 'problem', permission.PERM_VIEW_PROBLEM); +Item('/t', 'training_main', 'training', permission.PERM_VIEW_TRAINING); +Item('/discuss', 'discussion_main', 'discussion', permission.PERM_VIEW_DISCUSSION); +Item('/c', 'contest_main', 'contest', permission.PERM_VIEW_CONTEST); +Item('/manage', 'domain_manage', 'domain_manage', permission.PERM_MANAGE); + +global.Hydro.lib.nav = module.exports = Item; diff --git a/hydro/lib/template.js b/hydro/lib/template.js index 65d72477c..87551a283 100644 --- a/hydro/lib/template.js +++ b/hydro/lib/template.js @@ -1,35 +1,49 @@ -const path = require('path'); const nunjucks = require('nunjucks'); const markdown = require('./markdown'); -const options = require('../options'); const perm = require('../permission'); const builtin = require('../model/builtin'); const model = require('../model'); const misc = require('./misc'); +function Loader() { } +Loader.prototype.getSource = function getSource(name) { + if (!global.Hydro.template[name]) throw new Error(); + return { + src: global.Hydro.template[name], + path: name, + }; +}; + class Nunjucks extends nunjucks.Environment { constructor() { - super( - new nunjucks.FileSystemLoader(path.resolve(options.template.path)), - { autoescape: true, trimBlocks: true }, - ); + super(new Loader(), { autoescape: true, trimBlocks: true }); this.addFilter('json', (self) => JSON.stringify(self), false); this.addFilter('assign', (self, data) => Object.assign(self, data)); this.addFilter('markdown', (self) => markdown.render(self)); this.addFilter('base64_encode', (s) => Buffer.from(s).toString('base64')); - this.addFilter('gravatar_url', misc.gravatar_url); - this.addFilter('format_size', misc.format_size); - this.addGlobal('typeof', (o) => typeof o); - this.addGlobal('console', console); - this.addGlobal('static_url', (str) => `/${str}`); - this.addGlobal('reverse_url', (str) => str); - this.addGlobal('datetime_span', misc.datetime_span); - this.addGlobal('paginate', misc.paginate); - this.addGlobal('perm', perm); - this.addGlobal('status', builtin.STATUS); - this.addGlobal('model', model); } } const env = new Nunjucks(); -module.exports = env; +module.exports = { + render(name, state) { + Object.assign(state, { + typeof: (o) => typeof o, + static_url: (str) => `/${str}`, + datetime_span: misc.datetime_span, + paginate: misc.paginate, + perm, + status: builtin.status, + size: misc.size, + gravatar: misc.gravatar, + model, + Context: global.Hydro.ui, + }); + return new Promise((resolve, reject) => { + env.render(name, state, (err, res) => { + if (err) reject(err); + else resolve(res); + }); + }); + }, +}; diff --git a/hydro/loader.js b/hydro/loader.js new file mode 100644 index 000000000..ec63046e4 --- /dev/null +++ b/hydro/loader.js @@ -0,0 +1,134 @@ +/* eslint-disable import/no-dynamic-require */ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +function root(name) { + return path.resolve(process.cwd(), name); +} +const installed = fs.readdirSync(root('.build/module')); +function exist(name) { + try { + fs.statSync(root(name)); + } catch (e) { + return false; + } + return true; +} +const superRequire = (name) => { + let m; + try { + m = require(root(name)); + } catch (e) { + m = __non_webpack_require__(root(name)); // eslint-disable-line no-undef + } + return m; +}; + +async function handler() { + for (const i of installed) { + if (exist(`.build/module/${i}/handler.js`)) { + superRequire(`.build/module/${i}/handler.js`); + console.log(`Handler init: ${i}`); + } + } +} + +async function locale() { + if (exist('.build/locale.js')) { + global.Hydro.lib.i18n(superRequire('.build/locale.js')); + console.log('Locale init: builtin'); + } + for (const i of installed) { + if (exist(`.build/module/${i}/locale.js`)) { + global.Hydro.lib.i18n(superRequire(`.build/module/${i}/locale.js`)); + console.log(`Locale init: ${i}`); + } + } +} + +async function template() { + if (exist('.build/template.yaml')) { + const file = fs.readFileSync(root('.build/template.yaml')).toString(); + Object.assign(global.Hydro.template, yaml.safeLoad(file)); + console.log('Template init: builtin'); + } + for (const i of installed) { + if (exist(`.build/module/${i}/locale.js`)) { + const file = fs.readFileSync(root(`.build/module/${i}/template.yaml`)).toString(); + Object.assign(global.Hydro.template, yaml.safeLoad(file)); + console.log(`Template init: ${i}`); + } + } +} + +async function model() { + for (const i of installed) { + if (exist(`.build/module/${i}/model.js`)) { + const m = superRequire(`.build/module/${i}/model.js`); + if (m.index) await m.index(); // eslint-disable-line no-await-in-loop + console.log(`Model init: ${i}`); + } + } +} + +async function load() { + global.Hydro = { + handler: {}, + service: {}, + model: {}, + lib: {}, + template: {}, + ui: {}, + }; + await template(); + require('./lib/i18n'); + require('./utils'); + require('./error'); + require('./permission'); + require('./options'); + const bus = require('./service/bus'); + await new Promise((resolve) => { + const h = () => { + console.log('Database connected'); + bus.unsubscribe(['system_database_connected'], h); + resolve(); + }; + bus.subscribe(['system_database_connected'], h); + require('./service/db'); + }); + const builtinLib = [ + 'axios', 'download', 'i18n', 'mail', 'markdown', + 'md5', 'misc', 'paginate', 'pwhash', 'rank', + 'template', 'validator', 'nav', + ]; + for (const i of builtinLib) require(`./lib/${i}`); + await locale(); + require('./service/gridfs'); + require('./service/queue'); + const server = require('./service/server'); + const builtinModel = [ + 'blacklist', 'builtin', 'contest', 'discussion', 'message', + 'opcount', 'problem', 'record', 'setting', 'solution', + 'system', 'token', 'training', 'user', + ]; + for (const i of builtinModel) { + const m = require(`./model/${i}`); + if (m.index) await m.index(); // eslint-disable-line no-await-in-loop + } + const builtinHandler = [ + 'home', 'problem', 'record', 'judge', 'user', + 'contest', 'training', 'discussion', 'manage', 'import', + ]; + for (const i of builtinHandler) require(`./handler/${i}`); + await model(); + await handler(); + for (const i in global.Hydro.handler) { + await global.Hydro.handler[i].apply(); // eslint-disable-line no-await-in-loop + } + const notfound = require('./handler/notfound'); + await notfound.apply(); + server.start(); +} + +module.exports = load; diff --git a/hydro/model/discussion.js b/hydro/model/discussion.js index 59385ec6b..96341b8ef 100644 --- a/hydro/model/discussion.js +++ b/hydro/model/discussion.js @@ -1,5 +1,7 @@ const { ObjectID } = require('bson'); const problem = require('./problem'); +const contest = require('./contest'); +const { PERM_VIEW_PROBLEM_HIDDEN } = require('../permission'); const { DocumentNotFoundError } = require('../error'); const db = require('../service/db.js'); @@ -8,7 +10,7 @@ const collReply = db.collection('discussion.reply'); const collStatus = db.collection('discussion.status'); async function add(parentType, parentId, owner, title, content, ip = null, highlight = false) { - let res = await coll.insertOne({ + const res = await coll.insertOne({ owner, title, content, @@ -145,7 +147,8 @@ async function getVnode(ddoc, handler) { async function getListVnodes(ddocs, handler) { const res = {}; for (const ddoc of ddocs) { - res[ddoc._id] = await getVnode(ddoc, handler); + // FIXME no-await-in-loop + res[ddoc._id] = await getVnode(ddoc, handler); // eslint-disable-line no-await-in-loop } return res; } diff --git a/hydro/model/message.js b/hydro/model/message.js new file mode 100644 index 000000000..a719d2743 --- /dev/null +++ b/hydro/model/message.js @@ -0,0 +1,72 @@ +const { MessageNotFoundError } = require('../error'); +const db = require('../service/db.js'); + +const coll = db.collection('message'); + +async function add(from, to, content) { + const res = await coll.insertOne({ + from, + to, + fromUnread: false, + toUnread: true, + reply: [ + { + from, unread: true, content, at: new Date(), + }, + ], + }); + return await coll.findOne({ _id: res.insertedId }); // eslint-disable-line no-return-await +} + +async function get(_id) { + const doc = await coll.findOne({ _id }); + if (!doc) throw new MessageNotFoundError(); + return doc; +} + +function getMany(query, sort, page, limit) { + return coll.find(query).sort(sort) + .skip((page - 1) * limit).limit(limit) + .toArray(); +} + +function del(_id) { + return coll.deleteOne({ _id }); +} + +function count(query) { + return coll.find(query).count(); +} + +function getMulti(uid) { + return coll.find({ $or: [{ from: uid }, { to: uid }] }); +} + +async function addReply(_id, from, content) { + const reply = { + from, + content, + unread: true, + at: new Date(), + }; + await coll.updateOne({ _id }, { $push: { reply } }); + return await coll.findOne({ _id }); // eslint-disable-line no-return-await +} + +function index() { + return Promise.all([ + coll.createIndex({ to: 1, _id: -1 }), + coll.createIndex({ from: 1, _id: -1 }), + ]); +} + +global.Hydro.model.message = module.exports = { + count, + add, + get, + del, + getMany, + getMulti, + addReply, + index, +}; diff --git a/hydro/model/user.js b/hydro/model/user.js index 2ea86aa02..5394174e3 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.js @@ -26,8 +26,8 @@ class USER { this.nSubmit = user.nSubmit || 0; } - hasPerm(perm) { - return this.perm.includes(perm); + hasPerm(p) { + return this.perm.includes(p); } checkPassword(password) { @@ -144,7 +144,7 @@ async function getPrefixList(prefix, limit = 50) { async function setRole(uid, role) { const udoc = await getById(uid); - return await Promise.all([ + return await Promise.all([ // eslint-disable-line no-return-await coll.findOneAndUpdate({ _id: uid }, { $set: { role } }), collRole.updateOne({ _id: udoc.role }, { $inc: { count: -1 } }), collRole.updateOne({ _id: role }, { $inc: { count: 1 } }), @@ -159,8 +159,8 @@ function getRole(name) { return collRole.findOne({ _id: name }); } -function addRole(name, perm) { - return collRole.insertOne({ _id: name, perm, count: 0 }); +function addRole(name, permission) { + return collRole.insertOne({ _id: name, perm: permission, count: 0 }); } function deleteRoles(roles) { @@ -170,8 +170,12 @@ function deleteRoles(roles) { ]); } -function init() { - return collRole.update({ _id: 'root' }, { $set: { perm: perm.PERM_ALL } }); +function index() { + return Promise.all([ + collRole.updateOne({ _id: 'root' }, { $set: { perm: perm.PERM_ALL } }), + coll.createIndex('unameLower', { unique: true }), + coll.createIndex('mailLower', { sparse: true }), + ]); } module.exports = { @@ -192,5 +196,5 @@ module.exports = { getRoles, addRole, deleteRoles, - init, + index, }; diff --git a/hydro/permission.js b/hydro/permission.js index 969e22098..82965b2b3 100644 --- a/hydro/permission.js +++ b/hydro/permission.js @@ -114,4 +114,4 @@ perm.PERM_DEFAULT = perm.PERM_BASIC perm.PERM_ADMIN = perm.PERM_ALL; -module.exports = perm; +global.Hydro.permission = module.exports = perm; diff --git a/hydro/service/cron.js b/hydro/service/cron.js new file mode 100644 index 000000000..733645a49 --- /dev/null +++ b/hydro/service/cron.js @@ -0,0 +1,33 @@ +class Cron { + constructor() { + this.tasks = []; + } + + plan(time, task) { + const now = new Date(); + if (!(time instanceof Date)) time = new Date(time); + this.tasks.push({ + id: setTimeout(() => { + for (const i in this.tasks) { + if (this.tasks[i].func === task) { + this.tasks.splice(i, 1); + break; + } + } + task(); + }, time - now), + func: task, + }); + } + + interval(time, task) { + const now = new Date(); + if (!(time instanceof Date)) time = new Date(time); + this.tasks.push({ + id: setTimeout(() => { task(); }, time - now), + func: task, + }); + } +} + +module.exports = Cron; diff --git a/hydro/service/db.js b/hydro/service/db.js index e353c1bbb..61359006c 100644 --- a/hydro/service/db.js +++ b/hydro/service/db.js @@ -16,6 +16,6 @@ Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: bus.publish('system_database_connected', null); }); -module.exports = { +global.Hydro.service.db = module.exports = { collection: (c) => db.collection(c), }; diff --git a/hydro/service/server.js b/hydro/service/server.js index 57c5848d4..609efa1b8 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -65,20 +65,16 @@ class Handler { this.session = {}; } - renderHTML(name, context) { + async renderHTML(name, context) { console.time(name); this.hasPerm = (perm) => this.user.hasPerm(perm); - return new Promise((resolve, reject) => { - template.render(name, Object.assign(context, { - handler: this, - _: (str) => (str ? str.toString().translate(this.user.language) : ''), - user: this.user, - }), (error, res) => { - console.timeEnd(name); - if (error) reject(error); - else resolve(res); - }); - }); + const res = await template.render(name, Object.assign(context, { + handler: this, + _: (str) => (str ? str.toString().translate(this.user.language) : ''), + user: this.user, + })); + console.timeEnd(name); + return res; } async render(name, context) { @@ -320,20 +316,16 @@ class ConnectionHandler { for (const i in p) this.request.params[p[i][0]] = decodeURIComponent(p[i][1]); } - renderHTML(name, context) { + async renderHTML(name, context) { console.time(name); this.hasPerm = (perm) => this.user.hasPerm(perm); - return new Promise((resolve, reject) => { - template.render(name, Object.assign(context, { - handler: this, - _: (str) => (str ? str.toString().translate(this.user.language) : ''), - user: this.user, - }), (error, res) => { - console.timeEnd(name); - if (error) reject(error); - else resolve(res); - }); - }); + const res = await template.render(name, Object.assign(context, { + handler: this, + _: (str) => (str ? str.toString().translate(this.user.language) : ''), + user: this.user, + })); + console.timeEnd(name); + return res; } send(data) { @@ -401,14 +393,14 @@ function Connection(prefix, RouteConnHandler) { sock.installHandlers(server); } -exports.Handler = Handler; -exports.ConnectionHandler = ConnectionHandler; -exports.Route = Route; -exports.Connection = Connection; -exports.start = function start() { +function start() { app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); app.use(router.routes()).use(router.allowedMethods()); Route('*', Handler); server.listen(options.listen.port); console.log('Server listening at: %s', options.listen.port); +} + +global.Hydro.service.server = module.exports = { + Handler, ConnectionHandler, Route, Connection, start, }; diff --git a/package.json b/package.json index e6d676b8f..752cc4fec 100644 --- a/package.json +++ b/package.json @@ -8,19 +8,19 @@ "dependencies": { "axios": "^0.19.0", "bson": "^4.0.2", - "highlight.js": "^9.18.1", "js-yaml": "^3.13.1", "koa": "^2.11.0", "koa-body": "^4.1.1", "koa-morgan": "^1.0.1", "koa-router": "^8.0.8", - "koa-static-cache": "^5.1.2", + "koa-static-cache": "^5.1.3", "lodash": "^4.17.15", "markdown-it": "^10.0.0", "markdown-it-katex": "^2.0.3", - "mongodb": "^3.5.5", + "mongodb": "^3.5.7", "nodemailer": "^6.3.1", "nunjucks": "^3.2.1", + "prismjs": "^1.20.0", "sockjs": "^0.3.20" }, "devDependencies": { @@ -47,4 +47,4 @@ "templates/*" ] } -} \ No newline at end of file +} diff --git a/templates/components/comments_discussion.html b/templates/components/comments_discussion.html index 958416f4a..942419b43 100644 --- a/templates/components/comments_discussion.html +++ b/templates/components/comments_discussion.html @@ -3,7 +3,7 @@
  • - +
    {% if not mode_create %} @@ -14,7 +14,7 @@
    {{ reply_operations(doc, rdoc) }}
    -
    +
    {{ rdoc['content']|markdown|safe }}
    @@ -75,7 +75,7 @@
  • - +
    @@ -107,7 +107,7 @@
  • - +
    @@ -133,7 +133,7 @@ {% endif %}
    -
    +
    {{ doc['content']|markdown|safe }}
    diff --git a/templates/components/user.html b/templates/components/user.html index c93daaafe..be6203157 100644 --- a/templates/components/user.html +++ b/templates/components/user.html @@ -6,7 +6,7 @@ ) %}
  • diff --git a/templates/partials/nav.html b/templates/partials/nav.html index e2228918f..e40b43662 100644 --- a/templates/partials/nav.html +++ b/templates/partials/nav.html @@ -10,22 +10,15 @@