diff --git a/hydro/handler/base.js b/hydro/handler/base.js index 4d4734d5c..78037b903 100644 --- a/hydro/handler/base.js +++ b/hydro/handler/base.js @@ -69,4 +69,4 @@ MIDDLEWARE(async (ctx, next) => { console.error(e.stack); ctx.body = { error: { message: e.message, params: e.params, stack: e.stack } }; } -}); \ No newline at end of file +}, true); \ No newline at end of file diff --git a/hydro/handler/judge.js b/hydro/handler/judge.js index 1356ebb7b..d8e7af38c 100644 --- a/hydro/handler/judge.js +++ b/hydro/handler/judge.js @@ -1,9 +1,10 @@ const - { GET, POST } = require('../service/server'), - queue = require('../service/queue'), - record = require('../model/record'), { requirePerm } = require('./tools'), - { PERM_JUDGE } = require('../permission'); + { PERM_JUDGE } = require('../permission'), + record = require('../model/record'), + bus = require('../service/bus'), + queue = require('../service/queue'), + { GET, POST } = require('../service/server'); queue.assert('judge'); @@ -46,7 +47,8 @@ POST('/judge/next', requirePerm(PERM_JUDGE), async ctx => { if (body.score) $set.score = body.score; if (body.time_ms) $set.time = body.time_ms; if (body.memory_kb) $set.memory = body.memory_kb; - await record.update(body.rid, $set); + rdoc = await record.update(body.rid, $set); + bus.publish('record_change', rdoc); ctx.body = {}; }); POST('/judge/end', requirePerm(PERM_JUDGE), async ctx => { @@ -71,6 +73,7 @@ POST('/judge/end', requirePerm(PERM_JUDGE), async ctx => { if (body.memory_kb) $set.memory = body.memory_kb; $set.judgeAt = new Date(); $set.judger = ctx.state.user._id; - await record.update(body.rid, $set); + rdoc = await record.update(body.rid, $set); + bus.publish('record_change', rdoc); ctx.body = {}; }); diff --git a/hydro/handler/record.js b/hydro/handler/record.js index 84cff6582..89cb5d585 100644 --- a/hydro/handler/record.js +++ b/hydro/handler/record.js @@ -1,26 +1,49 @@ const bson = require('bson'), { constants } = require('../options'), - { GET, POST } = require('../service/server'), + { PERM_READ_RECORD_CODE, PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD, + PERM_REJUDGE, PERM_VIEW_PROBLEM_HIDDEN } = require('../permission'), { requirePerm } = require('../handler/tools'), - queue = require('../service/queue'), + problem = require('../model/problem'), record = require('../model/record'), user = require('../model/user'), - problem = require('../model/problem'), - { PERM_READ_RECORD_CODE, PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD, PERM_REJUDGE } = require('../permission'); + bus = require('../service/bus'), + queue = require('../service/queue'), + { GET, POST, SOCKET } = require('../service/server'); GET('/r', async ctx => { ctx.templateName = 'record_main.html'; let q = {}, page = ctx.query.page || 1; let rdocs = await record.getMany(q, { rid: 1 }, page, constants.RECORD_PER_PAGE); - let pdict, udict; - for (let rdoc in rdocs) { + let pdict = {}, udict = {}; + for (let rdoc of rdocs) { udict[rdoc.uid] = await user.getById(rdoc.uid); pdict[rdoc.pid] = await problem.get({ pid: rdoc.pid, uid: ctx.state.user._id }); } ctx.body = { page, rdocs, pdict, udict }; }); +SOCKET('/record-conn', class RecordConnHandler { + constructor(conn) { + this.tid = null; + this.h = async data => { + let rdoc = data.value; + if (rdoc.tid && rdoc.tid != this.tid.toString()) return; + let [udoc, pdoc] = await Promise.all([user.getById(rdoc.uid), problem.get({ pid: rdoc.pid })]); + if (pdoc.hidden && !conn.state.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null; + conn.send({ html: await conn.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc }) }); + }; + } + onOpen() { + bus.subscribe(['record_change'], this.h); + } + onMessage(data) { + if (data.tid) this.tid = data.tid; + } + onClose() { + bus.unsubscribe(['record_change'], this.h); + } +}); GET('/r/:rid', async ctx => { ctx.templateName = 'record_detail.html'; let uid = ctx.state.user._id, rid = new bson.ObjectID(ctx.params.rid); @@ -29,6 +52,27 @@ GET('/r/:rid', async ctx => { if (rdoc.uid != uid && !ctx.state.user.hasPerm(PERM_READ_RECORD_CODE)) rdoc.code = null; ctx.body = { rdoc, show_status: true }; }); +SOCKET('/record-detail-conn', class RecordDetailConnHandler { + constructor(conn) { + this.rid = conn.params.rid; + this.h = async data => { + let rdoc = data.value; + if (rdoc.rid.toString() != this.rid) return; + let [udoc, pdoc] = await Promise.all([user.getById(rdoc.uid), problem.get({ pid: rdoc.pid })]); + if (pdoc.hidden && !conn.state.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null; + conn.send({ html: await conn.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc }) }); + }; + } + async onOpen() { + bus.subscribe(['record_change'], this.h); + } + onMessage(data) { + if (data.rid) this.rid = data.rid; + } + onClose() { + bus.unsubscribe(['record_change'], this.h); + } +}); POST('/r/:rid/rejudge', requirePerm(PERM_REJUDGE), async ctx => { ctx.templateName = 'record_detail.html'; let uid = ctx.state.user._id, rid = new bson.ObjectID(ctx.params.rid); diff --git a/hydro/handler/ui.js b/hydro/handler/ui.js index d100959d3..bf614d7a7 100644 --- a/hydro/handler/ui.js +++ b/hydro/handler/ui.js @@ -30,10 +30,11 @@ class Markdown extends MarkdownIt { } const md = new Markdown(); function datetime_span(dt, { relative = true } = {}) { - dt = new Date(dt); + if (dt.generationTime) dt = new Date(dt.generationTime * 1000); + else if (typeof dt == 'number' || typeof dt == 'string') dt = new Date(dt); return '{2}'.format( relative ? ' relative' : '', - dt.getTime(), + dt.getTime() / 1000, dt.toLocaleString() ); } @@ -93,14 +94,14 @@ MIDDLEWARE(async (ctx, next) => { url_prefix: '/' }; ctx.preferJson = (ctx.request.headers['accept'] || '').includes('application/json'); - ctx.render = async (name, context) => { + ctx.renderHTML = (name, context) => { ctx.user = ctx.state.user; ctx.translate = str => { if (!str) return ''; return str.toString().translate(ctx.state.user.language); }; ctx.has_perm = perm => ctx.state.user.hasPerm(perm); - ctx.body = await new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { env.render(name, Object.assign(ctx.state, context, { handler: ctx, _: ctx.translate, @@ -110,6 +111,9 @@ MIDDLEWARE(async (ctx, next) => { else resolve(res); }); }); + }; + ctx.render = async (name, context) => { + ctx.body = await ctx.renderHTML(name, context); ctx.response.type = 'text/html'; }; try { @@ -125,8 +129,11 @@ MIDDLEWARE(async (ctx, next) => { ctx.response.type = 'application/octet-stream'; ctx.redirect(ctx.setRedirect); } - } else throw new NotFoundError(); + } else { + throw new NotFoundError(); + } } catch (error) { + if (error instanceof NotFoundError) ctx.status = 404; if (error.toString().startsWith('NotFoundError')) console.log(error); if (error.toString().startsWith('Template render error')) throw error; if (ctx.preferJson) ctx.body = { error }; @@ -136,4 +143,4 @@ MIDDLEWARE(async (ctx, next) => { if (ctx.preferJson) ctx.body = { error }; else await ctx.render('bsod.html', { error }); } -}); \ No newline at end of file +}, true); \ No newline at end of file diff --git a/hydro/model/user.js b/hydro/model/user.js index d58299fce..d08902b54 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.js @@ -14,6 +14,9 @@ class USER { this.salt = user.salt; this.hash = user.hash; this.perm = user.perm; + this.lang = user.language || 'zh_CN'; + this.codeLang = user.codeLang || 'c'; + this.codeTemplate = user.codeTemplate || ''; } hasPerm(perm) { return this.perm == '-' || (this.perm || '').includes(perm); diff --git a/hydro/service/bus.js b/hydro/service/bus.js new file mode 100644 index 000000000..d675fc90d --- /dev/null +++ b/hydro/service/bus.js @@ -0,0 +1,15 @@ +const EventEmitter = require('events').EventEmitter; +const bus = new EventEmitter(); +module.exports = { + subscribe(events, handler) { + for (let event of events) + bus.on(event, handler); + }, + unsubscribe(events, handler) { + for (let event of events) + bus.off(event, handler); + }, + publish(event, data) { + bus.emit(event, { value: data, event }); + } +}; \ No newline at end of file diff --git a/hydro/service/server.js b/hydro/service/server.js index d2d17b145..6ad784296 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -3,14 +3,13 @@ const morgan = require('koa-morgan'), Body = require('koa-body'), Router = require('koa-router'), - SocketIO = require('socket.io'); + sockjs = require('sockjs'); const options = require('../options'); let http = options.listen.https ? require('https') : require('http'); let app = new Koa(); let server = http.createServer(app.callback()); -let io = SocketIO(server); app.keys = options.session.keys; app.use(Body({ multipart: true, @@ -19,12 +18,14 @@ app.use(Body({ } })); let router = new Router(); - +let m = []; /** * @param {import('koa').Middleware} middleware + * @param {boolean} installForSocket */ -function MIDDLEWARE(middleware) { +function MIDDLEWARE(middleware, installForSocket = false) { app.use(middleware); + if (installForSocket) m.push(middleware); } /** * @param {string} route @@ -44,13 +45,57 @@ function POST(route, ...handler) { * @callback SocketIOHandler * @param {import('socket.io').Socket} socket */ +/** + * @callback SockJSHandler + * @param {import('sockjs').Connection} conn + */ /** * @param {string} prefix * @param {SocketIOHandler} handler */ function SOCKET(prefix, handler) { - io.of(prefix).on('connection', handler); + const sock = sockjs.createServer({ prefix }); + sock.on('connection', async conn => { + conn.cookies = { + get(name) { + return conn.cookies[name]; + }, + set() { } + }; + conn.state = {}; + conn.params = {}; + conn.request = { + headers: conn.headers + }; + let p = conn.url.split('?')[1].split('&'); + for (let i in p) p[i] = p[i].split('='); + for (let i in p) conn.params[p[i][0]] = decodeURIComponent(p[i][1]); + await new Promise((resolve, reject) => { + conn.once('data', msg => { + for (let i of msg.split(';')) { + i = i.trim().split('='); + conn.cookies[i[0]] = i[1]; + } + resolve(); + }); + setTimeout(reject, 5000); + }); + for (let i of m) { + await new Promise((resolve, reject) => { + i(conn, resolve).catch(reject); + }); + } + conn.send = data => { + conn.write(JSON.stringify(data)); + }; + let h = new handler(conn); + if (h.onOpen) conn.on('open', h.onOpen); + if (h.onClose) conn.on('close', h.onClose); + if (h.onMessage) conn.on('data', h.onMessage); + }); + sock.installHandlers(server); } + exports.MIDDLEWARE = MIDDLEWARE; exports.GET = GET; exports.POST = POST; @@ -58,6 +103,6 @@ exports.SOCKET = SOCKET; exports.start = function start() { app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); app.use(router.routes()).use(router.allowedMethods()); - app.listen(options.listen.port); + server.listen(options.listen.port); console.log('Server listening at: %s', options.listen.port); }; diff --git a/package.json b/package.json index b76d37e6e..e90a34e83 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "mongodb": "^3.5.5", "nodemailer": "^6.3.1", "nunjucks": "^3.2.1", - "socket.io": "^2.3.0" + "sockjs": "^0.3.20" }, "devDependencies": { "eslint": "^6.6.0" diff --git a/templates/components/record.html b/templates/components/record.html index 5bbec2420..fb9843bb3 100644 --- a/templates/components/record.html +++ b/templates/components/record.html @@ -3,7 +3,7 @@
{% if rdoc['status'] == status.STATUS_JUDGING %} diff --git a/templates/layout/html5.html b/templates/layout/html5.html index 910b3cea9..b1d866da2 100644 --- a/templates/layout/html5.html +++ b/templates/layout/html5.html @@ -40,7 +40,7 @@ - +