diff --git a/TODO b/TODO index 24fedd493..887a0dd39 100644 --- a/TODO +++ b/TODO @@ -1,8 +1,9 @@ @requireCsrfToken 为model.system添加缓存 -bug修复 修复discussion前端权限节点显示异常 web端运行脚本似乎异常 讨论回复通知 比赛举办者发布通知 举报功能 +SETTINGS_PRIVACY.allowRegisteredUsers +model.setting.ui.name diff --git a/build/webpack.js b/build/webpack.js index 386c258bb..22a6d0df7 100644 --- a/build/webpack.js +++ b/build/webpack.js @@ -31,6 +31,9 @@ const build = async (type) => { }, ], }, + node: { + __dirname: false, + }, resolve: { extensions: ['.js', '.ts'] }, plugins: [ new webpack.ProgressPlugin(), diff --git a/hydro/entry/common.ts b/hydro/entry/common.ts index e2e815d5c..f946d566e 100644 --- a/hydro/entry/common.ts +++ b/hydro/entry/common.ts @@ -1,6 +1,5 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-eval */ -import os from 'os'; import fs from 'fs'; import path from 'path'; import yaml from 'js-yaml'; @@ -30,20 +29,18 @@ export const builtinScript = [ 'blacklist', 'setSuperadmin', 'deleteUser', ]; -function getFiles(folder, base = '') { +function getFiles(folder: string, base = ''): string[] { const files = []; const f = fs.readdirSync(folder); for (const i of f) { - if (!i.startsWith('.')) { - if (fs.statSync(path.join(folder, i)).isDirectory()) { - files.push(...getFiles(path.join(folder, i), path.join(base, i))); - } else files.push(path.join(base, i)); - } + if (fs.statSync(path.join(folder, i)).isDirectory()) { + files.push(...getFiles(path.join(folder, i), path.join(base, i))); + } else files.push(path.join(base, i)); } - return files; + return files.map((item) => item.replace(/\\/gmi, '/')); } -export async function handler(pending, fail) { +export async function handler(pending: string[], fail: string[]) { for (const i of pending) { const p = path.resolve(i, 'handler.js'); if (fs.existsSync(p) && !fail.includes(i)) { @@ -60,7 +57,7 @@ export async function handler(pending, fail) { } } -export async function locale(pending, fail) { +export async function locale(pending: string[], fail: string[]) { for (const i of pending) { let p = path.resolve(i, 'locales'); if (!fs.existsSync(p)) p = path.resolve(i, 'locale'); @@ -82,7 +79,7 @@ export async function locale(pending, fail) { } } -export async function setting(pending, fail, modelSetting: typeof import('../model/setting')) { +export async function setting(pending: string[], fail: string[], modelSetting: typeof import('../model/setting')) { const map = { system: modelSetting.SystemSetting, account: modelSetting.AccountSetting, @@ -109,7 +106,7 @@ export async function setting(pending, fail, modelSetting: typeof import('../mod } } -export async function template(pending, fail) { +export async function template(pending: string[], fail: string[]) { for (const i of pending) { let p = path.resolve(i, 'templates'); if (!fs.existsSync(p)) p = path.resolve(i, 'template'); @@ -117,7 +114,9 @@ export async function template(pending, fail) { try { const files = getFiles(p); for (const file of files) { - global.Hydro.template[file] = fs.readFileSync(path.resolve(p, file)).toString(); + global.Hydro.ui.template[file] = fs.readFileSync( + path.resolve(p, file), + ).toString(); } console.log(`Template init: ${i}`); } catch (e) { @@ -128,7 +127,20 @@ export async function template(pending, fail) { } } -export async function model(pending, fail) { +export async function uistatic(pending: string[], fail: string[]) { + for (const i of pending) { + const p = path.resolve(i, 'public', 'static-manifest.json'); + if (fs.existsSync(p) && fs.statSync(p).isFile() && !fail.includes(i)) { + try { + Object.assign(global.Hydro.ui.manifest, eval('require')(p)); + } catch (e) { + fail.push(i); + } + } + } +} + +export async function model(pending: string[], fail: string[]) { for (const i of pending) { const p = path.resolve(i, 'model.js'); if (fs.existsSync(p) && !fail.includes(i)) { @@ -145,7 +157,7 @@ export async function model(pending, fail) { } } -export async function lib(pending, fail) { +export async function lib(pending: string[], fail: string[]) { for (const i of pending) { const p = path.resolve(i, 'lib.js'); if (fs.existsSync(p) && !fail.includes(i)) { @@ -163,7 +175,7 @@ export async function lib(pending, fail) { } } -export async function service(pending, fail) { +export async function service(pending: string[], fail: string[]) { for (const i of pending) { const p = path.resolve(i, 'service.js'); if (fs.existsSync(p) && !fail.includes(i)) { @@ -181,7 +193,7 @@ export async function service(pending, fail) { } } -export async function script(pending, fail, active) { +export async function script(pending: string[], fail: string[], active: string[]) { for (const i of pending) { const p = path.resolve(i, 'script.js'); if (fs.existsSync(p) && !fail.includes(i)) { @@ -199,7 +211,7 @@ export async function script(pending, fail, active) { } } -export async function wiki(pending, fail) { +export async function wiki(pending: string[], fail: string[]) { for (const i of pending) { const p = path.resolve(i, 'wiki'); if (fs.existsSync(p) && fs.statSync(p).isDirectory() && !fail.includes(i)) { diff --git a/hydro/entry/unzip.ts b/hydro/entry/unzip.ts index dc41fbae5..132da7be0 100644 --- a/hydro/entry/unzip.ts +++ b/hydro/entry/unzip.ts @@ -4,7 +4,8 @@ import path from 'path'; import AdmZip from 'adm-zip'; import fs from 'fs-extra'; -const moduleRoots = Array.from(new Set([ +const moduleRoots = []; +const _moduleRoots = Array.from(new Set([ path.resolve(process.cwd(), 'node_modules', '@hydrooj'), path.resolve(process.cwd(), 'modules'), path.resolve(process.cwd(), 'module'), @@ -16,17 +17,26 @@ const moduleRoots = Array.from(new Set([ path.resolve(__dirname, '.build'), __dirname, path.resolve(os.homedir(), '.hydro', 'module'), + // TODO add npm global path + '/usr/local/share/.config/yarn/global/node_modules/@hydrooj', + path.resolve(os.homedir(), 'AppData', 'Local', 'Yarn', 'Data', 'global', 'node_modules', '@hydrooj'), ])); +for (const moduleRoot of _moduleRoots) { + if (fs.existsSync(moduleRoot)) moduleRoots.push(moduleRoot); +} + +if (process.env.debug) console.log('Module roots: ', moduleRoots); const moduleTemp = path.resolve(os.tmpdir(), 'hydro', 'module'); const publicTemp = path.resolve(os.tmpdir(), 'hydro', 'public'); const tmp = path.resolve(os.tmpdir(), 'hydro', '__'); +process.on('unhandledRejection', (e) => { + console.error(e); + process.exit(1); +}); + export async function load() { - process.on('unhandledRejection', (e) => { - console.error(e); - process.exit(1); - }); fs.ensureDirSync(moduleTemp); fs.ensureDirSync(publicTemp); for (const moduleRoot of moduleRoots) { diff --git a/hydro/entry/worker.ts b/hydro/entry/worker.ts index 589041e70..6e80ab353 100644 --- a/hydro/entry/worker.ts +++ b/hydro/entry/worker.ts @@ -1,7 +1,7 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable import/no-dynamic-require */ import { - locale, template, lib, service, model, handler, script, setting, + locale, template, lib, service, model, handler, script, setting, wiki, uistatic, builtinLib, builtinScript, builtinHandler, builtinModel, } from './common'; @@ -13,7 +13,12 @@ export async function load() { require('../utils'); require('../error'); require('../options'); - await Promise.all([locale(pending, fail), template(pending, fail)]); + await Promise.all([ + locale(pending, fail), + template(pending, fail), + uistatic(pending, fail), + wiki(pending, fail), + ]); const bus = require('../service/bus'); await new Promise((resolve) => { const h = () => { @@ -51,6 +56,7 @@ export async function load() { } } for (const i of builtinScript) require(`../script/${i}`); + await wiki(pending, fail); await script(pending, fail, active); for (const postInit of global.Hydro.postInit) { await postInit(); diff --git a/hydro/handler/contest.ts b/hydro/handler/contest.ts index c67179cdd..a840427db 100644 --- a/hydro/handler/contest.ts +++ b/hydro/handler/contest.ts @@ -280,7 +280,7 @@ class ContestProblemSubmitHandler extends ContestProblemHandler { ttype: document.TYPE_CONTEST, lang, code, - }); + }, true); await contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId); if (!this.canShowRecord(this.tdoc)) { this.response.body = { tid }; diff --git a/hydro/handler/home.ts b/hydro/handler/home.ts index 2be756280..edb030b71 100644 --- a/hydro/handler/home.ts +++ b/hydro/handler/home.ts @@ -1,9 +1,7 @@ import { ObjectID } from 'mongodb'; import { VerifyPasswordError, UserAlreadyExistError, InvalidTokenError, - NotFoundError, - UserNotFoundError, - PermissionError, + NotFoundError, UserNotFoundError, PermissionError, } from '../error'; import * as bus from '../service/bus'; import { @@ -321,7 +319,7 @@ class HomeMessagesHandler extends Handler { const udoc = await user.getById('system', uid); if (!udoc) throw new UserNotFoundError(uid); if (udoc.gravatar) udoc.gravatar = misc.gravatar(udoc.gravatar); - const mdoc = await message.send(this.user._id, uid, content); + const mdoc = await message.send(this.user._id, uid, content, message.FLAG_UNREAD); // TODO(twd2): improve here: projection if (this.user._id !== uid) { bus.publish(`user_message-${uid}`, { mdoc, udoc }); @@ -340,8 +338,9 @@ class HomeMessagesHandler extends Handler { @param('messageId', Types.ObjectID) async postRead(domainId: string, messageId: ObjectID) { const msg = await message.get(messageId); - if ([msg.from, msg.to].includes(this.user._id)) await message.setFlag(messageId, message.FLAG_UNREAD); - else throw new PermissionError(); + if ([msg.from, msg.to].includes(this.user._id)) { + await message.setFlag(messageId, message.FLAG_UNREAD); + } else throw new PermissionError(); this.back(); } } diff --git a/hydro/handler/homework.ts b/hydro/handler/homework.ts index 79f4e6a8d..0c6b7c3a9 100644 --- a/hydro/handler/homework.ts +++ b/hydro/handler/homework.ts @@ -182,7 +182,7 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler { if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid); const rid = await record.add(domainId, { pid, lang, code, uid: this.user._id, tid, hidden: true, ttype: document.TYPE_HOMEWORK, - }); + }, true); await contest.updateStatus(domainId, tid, this.user._id, rid, pid, false, 0, document.TYPE_HOMEWORK); this.response.body = { tid, rid }; diff --git a/hydro/handler/judge.ts b/hydro/handler/judge.ts index e798eaf76..73e8cda3d 100644 --- a/hydro/handler/judge.ts +++ b/hydro/handler/judge.ts @@ -126,7 +126,7 @@ class PretestHandler extends Handler { lang, code, hidden: true, - }); + }, true); this.response.body = { rid }; this.response.redirect = this.url('record_detail', { rid }); } diff --git a/hydro/handler/manage.ts b/hydro/handler/manage.ts index cb43d23c2..59b4788b9 100644 --- a/hydro/handler/manage.ts +++ b/hydro/handler/manage.ts @@ -6,7 +6,7 @@ import * as system from '../model/system'; import { STATUS, PRIV } from '../model/builtin'; import * as record from '../model/record'; import { - Route, Connection, Handler, ConnectionHandler, + Route, Connection, Handler, ConnectionHandler, param, Types, } from '../service/server'; import { validate } from '../lib/validator'; import * as hpm from '../lib/hpm'; @@ -101,18 +101,21 @@ class SystemScriptHandler extends SystemHandler { this.response.body.path.push(['manage_script', null]); } - async post({ domainId, id, args = '{}' }) { + @param('id', Types.String) + @param('args', Types.String, true) + async post(domainId: string, id: string, args = '{}') { if (!global.Hydro.script[id]) throw new ValidationError('id'); args = JSON.parse(args); validate(global.Hydro.script[id].validate, args); const rid = await record.add(domainId, { - pid: id, + pid: 1, uid: this.user._id, lang: null, code: null, status: STATUS.STATUS_JUDGING, hidden: true, }, false); + judge.next({ domainId, rid, message: `Running script: ${id}` }); async function report(data) { judge.next({ domainId, rid, ...data }); } @@ -125,13 +128,13 @@ class SystemScriptHandler extends SystemHandler { domainId, rid, status: STATUS.STATUS_ACCEPTED, - judge_text: ret, + judge_text: ret.toString(), judger: 1, time_ms: time, memory_kb: 0, }); }) - .catch((err) => { + .catch((err: Error) => { const time = new Date().getTime() - start; judge.end({ domainId, diff --git a/hydro/handler/problem.ts b/hydro/handler/problem.ts index 2ab89783d..27691891e 100644 --- a/hydro/handler/problem.ts +++ b/hydro/handler/problem.ts @@ -234,7 +234,7 @@ class ProblemPretestHandler extends ProblemDetailHandler { this.limitRate('add_record', 60, 100); const rid = await record.add(domainId, { uid: this.user._id, lang, code, pid: this.pdoc.docId, input, - }); + }, true); await record.judge(domainId, rid); this.response.body = { rid }; } diff --git a/hydro/interface.ts b/hydro/interface.ts index baa458952..fed14cbc8 100644 --- a/hydro/interface.ts +++ b/hydro/interface.ts @@ -259,8 +259,11 @@ declare global { }, stat: any, wiki: { [category: string]: { [page: string]: any } }, - template: { [key: string]: string }, - ui: any, + ui: { + manifest: Dict, + nav: any, + template: { [key: string]: string }, + }, error: typeof import('./error'), locales: Dict>, postInit: Function[], diff --git a/hydro/lib/logger.ts b/hydro/lib/logger.ts index cdfd582c7..0d7baff62 100644 --- a/hydro/lib/logger.ts +++ b/hydro/lib/logger.ts @@ -1,4 +1,4 @@ -import yaml from 'js-yaml'; +import serialize from 'serialize-javascript'; function wrap(func: Function, type: string) { return (...args: any) => { @@ -12,7 +12,7 @@ function wrap(func: Function, type: string) { args[i] = `${args[i].message}\n${args[i].stack}`; } } - global.Hydro.model.message.send(1, 1, yaml.dump({ time, type, args }, {})); + global.Hydro.model.message.send(1, 1, serialize({ time, type, args }), 0); } catch (e) { func(e.message); } diff --git a/hydro/lib/template.ts b/hydro/lib/template.ts index 6dbfbbee4..14df5af46 100644 --- a/hydro/lib/template.ts +++ b/hydro/lib/template.ts @@ -3,16 +3,15 @@ import path from 'path'; import serialize from 'serialize-javascript'; import nunjucks from 'nunjucks'; import * as markdown from './markdown'; -import { STATUS, PERM, PRIV } from '../model/builtin'; import * as misc from './misc'; class Loader extends nunjucks.Loader { // eslint-disable-next-line class-methods-use-this getSource(name: string) { if (!process.env.debug) { - if (!global.Hydro.template[name]) throw new Error(`Cannot get template ${name}`); + if (!global.Hydro.ui.template[name]) throw new Error(`Cannot get template ${name}`); return { - src: global.Hydro.template[name], + src: global.Hydro.ui.template[name], path: name, noCache: false, }; @@ -47,18 +46,24 @@ class Nunjucks extends nunjucks.Environment { } const env = new Nunjucks(); -export function render(name: string, state: any): Promise { - return new Promise((resolve, reject) => { +export async function render(name: string, state: any): Promise { + const cdnPrefix = await global.Hydro.model.system.get('server.cdn'); + return await new Promise((resolve, reject) => { env.render(name, { page_name: name.split('.')[0], ...state, typeof: (o) => typeof o, - static_url: (str) => `/${str}`, + static_url: (assetName: string) => { + if (global.Hydro.ui.manifest[assetName]) { + return `${cdnPrefix}${global.Hydro.ui.manifest[assetName]}`; + } + return `${cdnPrefix}${assetName}`; + }, datetimeSpan: misc.datetimeSpan, paginate: misc.paginate, - perm: PERM, - PRIV, - STATUS, + perm: global.Hydro.model.builtin.PERM, + PRIV: global.Hydro.model.builtin.PRIV, + STATUS: global.Hydro.model.builtin.STATUS, size: misc.size, gravatar: misc.gravatar, model: global.Hydro.model, diff --git a/hydro/loader.ts b/hydro/loader.ts index e853e50a7..a1859658c 100644 --- a/hydro/loader.ts +++ b/hydro/loader.ts @@ -19,8 +19,11 @@ global.Hydro = { // @ts-ignore lib: {}, wiki: {}, - template: {}, - ui: {}, + ui: { + manifest: {}, + nav: [], + template: {}, + }, // @ts-ignore error: {}, locales: {}, diff --git a/hydro/model/message.ts b/hydro/model/message.ts index 00c1fbc3b..cbd6ff90b 100644 --- a/hydro/model/message.ts +++ b/hydro/model/message.ts @@ -5,14 +5,17 @@ import * as db from '../service/db'; const coll = db.collection('message'); export const FLAG_UNREAD = 1; -export const FLAG_NOTIFICATION = 2; +export const FLAG_ALERT = 2; -export async function send(from: number, to: number, content: string, flag: number): Promise { +export async function send( + from: number, to: number, + content: string, flag: number = FLAG_UNREAD, +): Promise { const res = await coll.insertOne({ - from, to, content, flag: FLAG_UNREAD | flag, + from, to, content, flag, }); return { - from, to, content, _id: res.insertedId, flag: FLAG_UNREAD | flag, + from, to, content, _id: res.insertedId, flag, }; } @@ -60,7 +63,7 @@ export function ensureIndexes() { global.Hydro.model.message = { FLAG_UNREAD, - FLAG_NOTIFICATION, + FLAG_ALERT, count, get, diff --git a/hydro/model/record.ts b/hydro/model/record.ts index 45db48d0b..80e8c8fe7 100644 --- a/hydro/model/record.ts +++ b/hydro/model/record.ts @@ -46,7 +46,7 @@ export interface JudgeTask { type?: string, } -export async function add(domainId: string, data: RdocBase, addTask = true) { +export async function add(domainId: string, data: RdocBase, addTask: boolean) { _.defaults(data, { status: STATUS.STATUS_WAITING, type: 'judge', diff --git a/hydro/model/setting.ts b/hydro/model/setting.ts index cfbe55c4f..aa510ac32 100644 --- a/hydro/model/setting.ts +++ b/hydro/model/setting.ts @@ -117,13 +117,15 @@ SystemSetting( Setting('setting_server', 'server.hostname', null, null, 'text', 'Server Hostname'), Setting('setting_server', 'server.host', null, null, 'text', 'Server Host'), Setting('setting_server', 'server.url', null, null, 'text', 'Server BaseURL'), + Setting('setting_server', 'server.cdn', null, '/', 'text', 'CDN Prefix', 'Ends with /'), Setting('setting_server', 'server.port', null, 8888, 'number', 'Server Port'), - Setting('setting_server', 'server.xff', null, null, 'text', 'IP Header'), + Setting('setting_server', 'server.xff', null, null, 'text', 'IP Header', 'e.g. x-forwarded-for (lowercase)'), Setting('setting_server', 'server.log', null, false, 'boolean', 'Disable Access Log'), Setting('setting_session', 'session.keys', null, [String.random(32)], 'text', 'session.keys', null, FLAG_DISABLED | FLAG_HIDDEN), Setting('setting_session', 'session.secure', null, false, 'boolean', 'session.secure'), Setting('setting_session', 'session.saved_expire_seconds', null, 3600 * 24 * 30, 'number', 'Saved session expire seconds'), Setting('setting_session', 'session.unsaved_expire_seconds', null, 3600 * 3, 'number', 'Unsaved session expire seconds'), + Setting('setting_ui', 'ui.name', null, 'Hydro', 'text', 'Site Name'), Setting('setting_ui', 'ui.header_logo', null, '/components/header/header-logo.png', 'text', 'Header Logo'), Setting('setting_ui', 'ui.header_logo_2x', null, '/components/header/header-logo@2x.png', 'text', 'Header Logo@2x'), Setting('setting_ui', 'ui.header_background', null, '/components/header/header-background.png', 'text', 'Header Background'), diff --git a/hydro/model/system.ts b/hydro/model/system.ts index 6e04d0364..14a47af16 100644 --- a/hydro/model/system.ts +++ b/hydro/model/system.ts @@ -1,15 +1,31 @@ -import { FindOneAndUpdateOption } from 'mongodb'; +import Lru from 'lru-cache'; import * as db from '../service/db'; const coll = db.collection('system'); +const cache = new Lru({ + maxAge: 5000, +}); export async function get(_id: string) { + const res = cache.get(_id); + if (res !== undefined) return res; const doc = await coll.findOne({ _id }); - if (doc) return doc.value; + if (doc) { + cache.set(_id, doc.value); + return doc.value; + } return null; } export async function getMany(keys: string[]): Promise { + const r = []; + let success = true; + for (const key of keys) { + const res = cache.get(key); + if (res !== undefined) r.push(res); + else success = false; + } + if (success) return r; const docs = await coll.find({ _id: { $in: keys } }).toArray(); const dict = {}; const res = []; @@ -22,24 +38,29 @@ export async function getMany(keys: string[]): Promise { return res; } -export async function update(_id: string, operation: any, config: FindOneAndUpdateOption) { - await coll.findOneAndUpdate({ _id }, operation, config); - return get(_id); -} - export async function set(_id: string, value: any) { - await coll.findOneAndUpdate({ _id }, { $set: { value } }, { upsert: true }); - return get(_id); + cache.set(_id, value); + const res = await coll.findOneAndUpdate( + { _id }, + { $set: { value } }, + { upsert: true, returnOriginal: false }, + ); + return res.value.value; } -export function inc(field: string) { - return update(field, { $inc: { value: 1 } }, { upsert: true }); +export async function inc(_id: string) { + const res = await coll.findOneAndUpdate( + { _id }, + { $inc: { value: 1 } }, + { upsert: true, returnOriginal: false }, + ); + cache.set(_id, res.value.value); + return res.value.value; } global.Hydro.model.system = { get, getMany, - update, inc, set, }; diff --git a/locales/zh_CN.yaml b/locales/zh_CN.yaml index d0c376d67..29994bf50 100644 --- a/locales/zh_CN.yaml +++ b/locales/zh_CN.yaml @@ -653,6 +653,7 @@ Solve: 解决 Solved Problems: 解决题目 Sorry, there is no problem in the problem set: 题库中没有题目 Sorry, there is no training plan.: 目前没有训练计划。 +Sorry, there is no user in this domain.: 对不起,此域中没有用户。 Sorry: 对不起 Sort by: 排序 Source type: 来源类型 diff --git a/package.json b/package.json index e06d35a78..77fba1b79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.8.0", + "version": "2.8.1", "main": "hydro/loader.ts", "repository": "https://github.com/hydro-dev/Hydro.git", "author": "masnn", @@ -10,7 +10,6 @@ }, "dependencies": { "@hydrooj/markdown-it-katex": "^2.1.1", - "@types/lru-cache": "^5.1.0", "adm-zip": "^0.4.16", "ansi_up": "^4.0.4", "detect-browser": "^5.1.1", @@ -51,12 +50,14 @@ "@types/koa-router": "^7.4.1", "@types/koa-static-cache": "^5.1.0", "@types/lodash": "^4.14.157", + "@types/lru-cache": "^5.1.0", "@types/markdown-it": "^10.0.1", "@types/mongodb": "^3.5.25", "@types/node": "^14.0.22", "@types/nodemailer": "^6.4.0", "@types/nunjucks": "^3.1.3", "@types/prismjs": "^1.16.1", + "@types/serialize-javascript": "^4.0.0", "@types/sockjs": "^0.3.32", "@types/superagent": "^4.1.8", "@types/superagent-proxy": "^2.0.0", @@ -93,10 +94,5 @@ ".build/module/**" ] }, - "snyk": true, - "optionalDependencies": { - "@hydrooj/geoip": "^1.1.1", - "@hydrooj/migrate-vijos": "^1.1.0", - "@hydrooj/pastebin": "^1.0.0" - } + "snyk": true } diff --git a/tool/checkTranslation.js b/tool/checkTranslation.js index 249067689..e690a868e 100644 --- a/tool/checkTranslation.js +++ b/tool/checkTranslation.js @@ -1,3 +1,4 @@ +/* eslint-disable no-eval */ const fs = require('fs'); const path = require('path'); const yaml = require('js-yaml'); @@ -8,42 +9,77 @@ const IGNORE_CHECK = ['en']; const IGNORE_MISSING = [ '?', 'AC', 'Gravatar', 'ID', 'MD5', 'URL', ]; -const RE_TEXT = /_\(['"]([\s\S])*?['"]\)/gmi; +const RE_TEMPLATE = /_\(['"]([\s\S])*?['"]\)/gmi; +const RE_UI = /i18n\(['"]([\s\S])*?['"](,.*?)?\)/gmi; const TEMPLATE_ROOT = path.resolve(__dirname, '..', 'templates'); const LOCALE_ROOT = path.resolve(__dirname, '..', 'locales'); - +const UI_ROOT = path.resolve(__dirname, '..', 'ui'); +const IGNORE_NAME = ['node_modules']; const texts = {}; +const result = {}; +const locales = fs.readdirSync(LOCALE_ROOT); let currentFile = ''; // eslint-disable-next-line @typescript-eslint/no-unused-vars -function _(str) { +function _(str, format) { if (!texts[str]) texts[str] = [currentFile]; else texts[str].push(currentFile); } +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const i18n = _; -function scan(folder, relative = '') { +function scanTemplate(folder, relative = 'templates') { const files = fs.readdirSync(folder); for (const file of files) { - const p = path.join(folder, file); - if (fs.statSync(p).isDirectory()) { - scan(p, path.join(relative, file)); - } else { - currentFile = path.join(relative, file); - const f = fs.readFileSync(p).toString(); - f.replace(RE_TEXT, (substr) => { - try { - // eslint-disable-next-line no-eval - eval(substr); - } catch (e) { - console.error('Cannot parse: ', substr, ' in file ', p); - } - }); + if (!IGNORE_NAME.includes(file)) { + const p = path.join(folder, file); + if (fs.statSync(p).isDirectory()) { + scanTemplate(p, path.join(relative, file)); + } else { + currentFile = path.join(relative, file); + const f = fs.readFileSync(p).toString(); + f.replace(RE_TEMPLATE, (substr) => { + try { + // eslint-disable-next-line no-eval + eval(substr); + } catch (e) { + console.error('Cannot parse: ', substr, ' in file ', p); + } + }); + } } } } -scan(TEMPLATE_ROOT); -const result = {}; -const locales = fs.readdirSync(LOCALE_ROOT); +function scanUi(folder, relative = 'ui') { + const files = fs.readdirSync(folder); + for (const file of files) { + if (!IGNORE_NAME.includes(file)) { + const p = path.join(folder, file); + if (fs.statSync(p).isDirectory()) { + scanUi(p, path.join(relative, file)); + } else { + currentFile = path.join(relative, file); + const f = fs.readFileSync(p).toString(); + f.replace(RE_UI, (substr) => { + try { + eval(substr); + } catch (e) { + if (e.message.endsWith('is not defined')) { + global[e.message.split(' ')[0]] = undefined; + try { + eval(substr); + } catch (err) { + console.error('Cannot parse: ', substr, ' in file ', p); + } + } else console.error('Cannot parse: ', substr, ' in file ', p); + } + }); + } + } + } +} +scanTemplate(TEMPLATE_ROOT); +scanUi(UI_ROOT); if (!name) { for (const locale of locales) { if (!IGNORE_CHECK.includes(locale.split('.')[0])) { diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index c8fde399a..4ae338472 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -1,100 +1,101 @@ const path = require('path'); module.exports = { - "root": true, - "parser": "babel-eslint", - "plugins": [ - "babel", - "react" + root: true, + parser: 'babel-eslint', + plugins: [ + 'babel', + 'react', ], - "env": { - "browser": true, - "es6": true, - "jquery": true, - "commonjs": true + env: { + browser: true, + es6: true, + jquery: true, + commonjs: true, }, - "extends": [ - "airbnb" + extends: [ + 'airbnb', ], - "parserOptions": { - "sourceType": "module", - "ecmaVersion": 7, - "ecmaFeatures": { - "impliedStrict": true, - "experimentalObjectRestSpread": true, - "jsx": true, - "legacyDecorators": true - } + parserOptions: { + sourceType: 'module', + ecmaVersion: 7, + ecmaFeatures: { + impliedStrict: true, + experimentalObjectRestSpread: true, + jsx: true, + legacyDecorators: true, + }, }, - "settings": { - "import/resolver": { - "webpack": { - "config": { - "resolve": { - "alias": { - "vj": path.resolve(__dirname), - } - } - } - } - } + settings: { + 'import/resolver': { + webpack: { + config: { + resolve: { + alias: { + vj: path.resolve(__dirname), + }, + }, + }, + }, + }, }, - "globals": { - "__webpack_public_path__": true, - "__webpack_require__": true, - "UiContext": true, - "UserContext": true, - "Context": true, - "LOCALES": true, - "window": true, + globals: { + __webpack_public_path__: true, + __webpack_require__: true, + UiContext: true, + UserContext: true, + Context: true, + LOCALES: true, + window: true, }, - "rules": { - "comma-dangle": [ - "error", - "always-multiline" + rules: { + 'comma-dangle': [ + 'error', + 'always-multiline', ], - "indent": [ - "error", + indent: [ + 'error', 2, { - "SwitchCase": 0 - } + SwitchCase: 0, + }, ], - "max-len": [ - "error", - 150 + 'max-len': [ + 'error', + 150, ], - "quotes": "warn", - "func-names": "off", - "class-methods-use-this": "off", - "consistent-return": "warn", - "no-restricted-syntax": "warn", - "no-unused-vars": "warn", - "no-console": "off", - "no-continue": "off", - "no-mixed-operators": "off", - "no-plusplus": "off", - "no-underscore-dangle": "off", - "no-await-in-loop": "off", - "no-lonely-if": "off", - "import/first": "off", - "react/prefer-stateless-function": "off", - "react/self-closing-comp": "off", - "react/prop-types": "off", - "react/jsx-filename-extension": "off", - "react/jsx-one-expression-per-line": "off", - "react/no-string-refs": "off", - "react/require-default-props": "off", - "react/react-in-jsx-scope": "off", - "react/destructuring-assignment": "off", - "react/button-has-type": "off", - "react/forbid-prop-types": "off", - "jsx-a11y/no-static-element-interactions": "off", - "jsx-a11y/anchor-is-valid": "off", - "jsx-a11y/click-events-have-key-events": "off", - "max-classes-per-file": "off", - "react/jsx-props-no-spreading": "off", - "react/static-property-placement": "off", - "no-param-reassign": "off" - } + quotes: 'warn', + 'func-names': 'off', + 'class-methods-use-this': 'off', + 'consistent-return': 'warn', + 'no-restricted-syntax': 'warn', + 'no-unused-vars': 'warn', + 'no-console': 'off', + 'no-continue': 'off', + 'no-mixed-operators': 'off', + 'no-plusplus': 'off', + 'no-underscore-dangle': 'off', + 'no-await-in-loop': 'off', + 'no-lonely-if': 'off', + 'no-param-reassign': 'off', + 'no-bitwise': 'off', + 'import/first': 'off', + 'react/prefer-stateless-function': 'off', + 'react/self-closing-comp': 'off', + 'react/prop-types': 'off', + 'react/jsx-filename-extension': 'off', + 'react/jsx-one-expression-per-line': 'off', + 'react/no-string-refs': 'off', + 'react/require-default-props': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/destructuring-assignment': 'off', + 'react/button-has-type': 'off', + 'react/forbid-prop-types': 'off', + 'jsx-a11y/no-static-element-interactions': 'off', + 'jsx-a11y/anchor-is-valid': 'off', + 'jsx-a11y/click-events-have-key-events': 'off', + 'max-classes-per-file': 'off', + 'react/jsx-props-no-spreading': 'off', + 'react/static-property-placement': 'off', + }, }; diff --git a/ui/components/message/index.page.js b/ui/components/message/index.page.js index 2c91d82df..6601b4434 100644 --- a/ui/components/message/index.page.js +++ b/ui/components/message/index.page.js @@ -1,5 +1,9 @@ import { AutoloadPage } from 'vj/misc/PageLoader'; +import { InfoDialog } from 'vj/components/dialog'; import Notification from 'vj/components/notification'; +import { FLAG_ALERT } from 'vj/constant/message'; +import i18n from 'vj/utils/i18n'; +import tpl from 'vj/utils/tpl'; const messagePage = new AutoloadPage('messagePage', () => { async function init() { @@ -8,12 +12,23 @@ const messagePage = new AutoloadPage('messagePage', () => { const sock = new SockJs(`/home/messages-conn?token=${UiContext.token}`); sock.onmessage = (message) => { const msg = JSON.parse(message.data); - Notification.show({ - title: msg.udoc.uname, - avatar: msg.udoc.gravatar, - message: msg.mdoc.content, - duration: 0, - }); + if (msg.mdoc.flag | FLAG_ALERT) { + // Is alert + const dialog = new InfoDialog({ + $body: tpl` +
+

${i18n(msg.udoc.content)}

+
`, + }); + dialog.open(); + } else { + // Is message + Notification.show({ + title: msg.udoc.uname, + avatar: msg.udoc.gravatar, + message: msg.mdoc.content, + }); + } }; } diff --git a/ui/components/messagepad/MessagePadDialogueContentContainer.js b/ui/components/messagepad/MessagePadDialogueContentContainer.js index 7a4edff9b..ac16f459b 100644 --- a/ui/components/messagepad/MessagePadDialogueContentContainer.js +++ b/ui/components/messagepad/MessagePadDialogueContentContainer.js @@ -13,7 +13,7 @@ import Message from './MessageComponent'; const mapStateToProps = (state) => ({ activeId: state.activeId, item: state.activeId !== null - ? state.dialogues[state.activeId] + ? state.ues[state.activeId] : null, }); diff --git a/ui/components/notification/index.js b/ui/components/notification/index.js index 08db305d9..de1bd6351 100644 --- a/ui/components/notification/index.js +++ b/ui/components/notification/index.js @@ -5,6 +5,35 @@ let lastNotification = null; let autoHideTimer = null; export default class Notification { + constructor({ + avatar, title, message, type = '', duration = 3000, action, + }) { + this.type = type; + if (avatar) this.type += ' avatar'; + if (title) this.type += ' title'; + this.action = (() => { }) || action; + this.$dom = $(tpl`
`); + if (avatar) $(tpl``).appendTo(this.$dom); + if (title) $(tpl`

${title}

`).appendTo(this.$dom); + $(tpl`

${message}

`).appendTo(this.$dom); + this.$dom.on('clink', this.handleClick.bind(this)); + this.$n = this.$dom + .css('z-index', zIndexManager.getNext()) + .appendTo('body'); + this.$n.width(); // force reflow + this.$n.removeClass('hide'); + this.autoHideTimer = setTimeout(this.hide.bind(this), duration); + } + + handleClick() { + this.action(); + } + + hide() { + this.$n.addClass('hide'); + setTimeout(() => this.$n.remove(), 200); + } + static success(message, duration) { return Notification.show({ type: 'success', message, duration }); } @@ -22,49 +51,37 @@ export default class Notification { } static show({ - avatar, title, message, type = '', duration = 3000, - actionButtons = [], + avatar, title, message, type = '', duration = 3000, action, }) { - if (duration && lastNotification) { - Notification.hide(); - } + if (lastNotification) Notification.hide(); if (avatar) type += ' avatar'; if (title) type += ' title'; const $dom = $(tpl`
`); if (avatar) $(tpl``).appendTo($dom); if (title) $(tpl`

${title}

`).appendTo($dom); $(tpl`

${message}

`).appendTo($dom); - actionButtons.forEach((button) => { - $(tpl``).appendTo($dom); - }); + $dom.on('clink', action); const $n = $dom .css('z-index', zIndexManager.getNext()) .appendTo('body'); $n.width(); // force reflow $n.removeClass('hide'); - if (duration) { - lastNotification = $n; - autoHideTimer = setTimeout(Notification.hide, duration); - } + lastNotification = $n; + autoHideTimer = setTimeout(Notification.hide, duration); return $n; } - static hide($node) { - if (!$node) { - if (!lastNotification) { - return; - } - if (autoHideTimer) { - clearTimeout(autoHideTimer); - autoHideTimer = null; - } - const $n = lastNotification; - $n.addClass('hide'); - setTimeout(() => $n.remove(), 200); - lastNotification = null; - } else { - $node.addClass('hide'); - setTimeout(() => $node.remove(), 200); + static hide() { + if (!lastNotification) { + return; } + if (autoHideTimer) { + clearTimeout(autoHideTimer); + autoHideTimer = null; + } + const $n = lastNotification; + $n.addClass('hide'); + setTimeout(() => $n.remove(), 200); + lastNotification = null; } } diff --git a/ui/constant/message.js b/ui/constant/message.js new file mode 100644 index 000000000..15ecb3b06 --- /dev/null +++ b/ui/constant/message.js @@ -0,0 +1,2 @@ +export const FLAG_UNREAD = 1; +export const FLAG_ALERT = 2; diff --git a/ui/constant/setting.js b/ui/constant/setting.js index c551bb8fe..a2a919d65 100644 --- a/ui/constant/setting.js +++ b/ui/constant/setting.js @@ -15,28 +15,3 @@ export const FUNCTION_RANGE = { 1: 'Enabled', }; attachObjectMeta(FUNCTION_RANGE, 'intKey', true); - -export const BACKGROUND_RANGE = { - 1: 'Bg1', - 2: 'Bg2', - 3: 'Bg3', - 4: 'Bg4', - 5: 'Bg5', - 6: 'Bg6', - 7: 'Bg7', - 8: 'Bg8', - 9: 'Bg9', - 10: 'Bg10', - 11: 'Bg11', - 12: 'Bg12', - 13: 'Bg13', - 14: 'Bg14', - 15: 'Bg15', - 16: 'Bg16', - 17: 'Bg17', - 18: 'Bg18', - 19: 'Bg19', - 20: 'Bg20', - 21: 'Bg21', -}; -attachObjectMeta(BACKGROUND_RANGE, 'intKey', true); diff --git a/yarn.lock b/yarn.lock index 0020cb1eb..f1fec7627 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,13 +30,6 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@hydrooj/geoip@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@hydrooj/geoip/-/geoip-1.1.2.tgz#1dfa02ce06c2aa2690bbce6aea089731eb91d137" - integrity sha512-a8OefZxUd+AUlv4w1RweVXqtcb7gxNaTk0oce5/WWDN1CogFbDUK790BPHRvfB3wiSWLm1h0GyjJOFQL9hypwA== - dependencies: - maxmind "^4.1.3" - "@hydrooj/markdown-it-katex@^2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@hydrooj/markdown-it-katex/-/markdown-it-katex-2.1.1.tgz#0b656483b912593e2a927360592462770e329f25" @@ -44,16 +37,6 @@ dependencies: katex "^0.11.1" -"@hydrooj/migrate-vijos@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@hydrooj/migrate-vijos/-/migrate-vijos-1.1.0.tgz#1929ec44accd47be58ff792eda69f4576883e807" - integrity sha512-3OqrNyMFQjkIx7LQDzHF0Z/HUzYqWaGPJnRO7JkhblCnZ4wBEhXRzC0ZI4Yil1s/w9+E+POb+5HrzwYPrnV7mw== - -"@hydrooj/pastebin@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@hydrooj/pastebin/-/pastebin-1.0.0.tgz#5b3b86097451dfd056bea84d51b9b7c28b00c895" - integrity sha512-AFDgqNTR/luBA1zIlP+4tBbcGC35Nq74nzaHtJ/ZCqZL2RZfpP4p90wH3U3Gi0+YD45VXIvDVEVCgruQ4naFvA== - "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -648,6 +631,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== +"@types/serialize-javascript@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/serialize-javascript/-/serialize-javascript-4.0.0.tgz#ab9c47edf71f6a4590221118d1dffc37b50d71a2" + integrity sha512-y9UO8ozDGF30EVRizmsXuCdZzAxHmYpEEiL+/SQjX+7bnxslkvZQU8rLydixJv7yVae6bWyrNDFHsh6fYHkFMA== + "@types/serve-static@*": version "1.13.4" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.4.tgz#6662a93583e5a6cabca1b23592eb91e12fa80e7c" @@ -4319,13 +4307,6 @@ markdown-it@^11.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" -maxmind@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/maxmind/-/maxmind-4.1.3.tgz#ed1060f45085b7ae2dc95cf5c1672f593d7fa076" - integrity sha512-6foBs2wNAfZ7Hu3Ag3nFiIs0gVlHtP1UioHHOuYBi2fH24Ngc5XujUi2GLk0r2HJLjRAaYgaSSTnRoIPs/RWNw== - dependencies: - tiny-lru "7.0.6" - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -6681,11 +6662,6 @@ tiny-glob@^0.2.6: globalyzer "^0.1.0" globrex "^0.1.1" -tiny-lru@7.0.6: - version "7.0.6" - resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz#b0c3cdede1e5882aa2d1ae21cb2ceccf2a331f24" - integrity sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow== - tmp@0.0.33, tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"