diff --git a/hydro/handler/misc.ts b/hydro/handler/misc.ts index d11efba9d..0f97be20a 100644 --- a/hydro/handler/misc.ts +++ b/hydro/handler/misc.ts @@ -5,7 +5,6 @@ import { PRIV } from '../model/builtin'; import * as system from '../model/system'; import * as file from '../model/file'; import * as user from '../model/user'; -import * as markdown from '../lib/markdown'; import * as db from '../service/db'; import { Route, Handler, Types, param, multipart, @@ -103,15 +102,6 @@ class SwitchLanguageHandler extends Handler { } } -class MarkdownHandler extends Handler { - async post({ text, html = false, inline = false }) { - this.response.body = inline - ? markdown.renderInline(text, html) - : markdown.render(text, html); - this.response.type = 'text/html'; - } -} - class SockToken extends Handler { async get() { this.response.body = { token: this.UIContext.token }; @@ -153,7 +143,6 @@ export async function apply() { Route('status', '/status', StatusHandler); Route('status_update', '/status/update', StatusUpdateHandler); Route('switch_language', '/language/:lang', SwitchLanguageHandler); - Route('markdown', '/markdown', MarkdownHandler); Route('token', '/token', SockToken); Route('ui', '/extra.css', UiSettingsHandler); } diff --git a/hydro/handler/user.ts b/hydro/handler/user.ts index 33dc965c8..e412fe200 100644 --- a/hydro/handler/user.ts +++ b/hydro/handler/user.ts @@ -238,7 +238,9 @@ class OauthHandler extends Handler { } class OauthCallbackHandler extends Handler { - async github({ state, code }) { + @param('state', Types.String) + @param('code', Types.String) + async github(domainId: string, state: string, code: string) { const [appid, secret, url, proxy, s] = await Promise.all([ system.get('oauth.githubappid'), system.get('oauth.githubsecret'), @@ -272,13 +274,14 @@ class OauthCallbackHandler extends Handler { uname: [userInfo.body.name, userInfo.body.login], }; this.response.redirect = s.redirect; - await token.del(s, token.TYPE_OAUTH); + await token.del(s._id, token.TYPE_OAUTH); return ret; } - async google({ - code, error, state, - }) { + @param('state', Types.String) + @param('code', Types.String) + @param('error', Types.String) + async google(domainId: string, state: string, code: string, error: string) { if (error) throw new UserFacingError(error); const [ [appid, secret, url, proxy], @@ -308,9 +311,11 @@ class OauthCallbackHandler extends Handler { }; } - async get(args) { + async get(args: any) { let r; + // @ts-ignore if (args.type === 'github') r = await this.github(args); + // @ts-ignore else if (args.type === 'google') r = await this.google(args); else throw new UserFacingError('Oauth type'); const udoc = await user.getByEmail('system', r.email); diff --git a/hydro/interface.ts b/hydro/interface.ts index ae60af992..bd8cf5113 100644 --- a/hydro/interface.ts +++ b/hydro/interface.ts @@ -277,6 +277,15 @@ export interface Drrdoc { ip: string, } +export interface TokenDoc { + _id: string, + tokenType: number, + createAt: Date, + updateAt: Date, + expireAt: Date, + [key: string]: any, +} + export interface ContestStat extends Dictionary { detail: any, } @@ -372,7 +381,6 @@ declare global { jwt: typeof import('./lib/jwt'), logger: typeof import('./lib/logger').default, mail: typeof import('./lib/mail'), - markdown: typeof import('./lib/markdown'), md5: typeof import('./lib/md5').default, misc: typeof import('./lib/misc'), nav: typeof import('./lib/nav').default, @@ -382,7 +390,6 @@ declare global { testdataConfig: typeof import('./lib/testdataConfig'), sha1: typeof import('./lib/sha1').default, sysinfo: typeof import('./lib/sysinfo'), - template: typeof import('./lib/template'), 'testdata.convert.ini': typeof import('./lib/testdata.convert.ini').default, useragent: typeof import('./lib/useragent'), validator: typeof import('./lib/validator'), diff --git a/hydro/lib/markdown.ts b/hydro/lib/markdown.ts deleted file mode 100644 index b6f4ffc9e..000000000 --- a/hydro/lib/markdown.ts +++ /dev/null @@ -1,67 +0,0 @@ -import MarkdownIt from 'markdown-it'; -import Prism from 'prismjs'; - -// For math: $a\times b\eq 10$ -// Specific image size: ![image](image.png =100x100) -import Imsize from 'markdown-it-imsize'; -/* Footnote support. -Here is a footnote reference,[^1] and another.[^longnote] - -[^1]: Here is the footnote. - -[^longnote]: Here's one with multiple blocks. - - Subsequent paragraphs are indented to show that they -belong to the previous footnote. -*/ -import Footnote from 'markdown-it-footnote'; -// ==Highlight== -import Mark from 'markdown-it-mark'; -import TOC from 'markdown-it-table-of-contents'; -import Anchor from 'markdown-it-anchor'; - -require('prismjs/components/index'); - -type Plugin = MarkdownIt.PluginSimple | MarkdownIt.PluginWithOptions | MarkdownIt.PluginWithParams; - -class Markdown extends MarkdownIt { - constructor() { - super({ - linkify: true, - highlight(str, lang) { - if (lang && Prism.languages[lang]) { - try { - return Prism.highlight(str, Prism.languages[lang], lang); - } catch (__) { } // eslint-disable-line no-empty - } - return ''; - }, - }); - this.linkify.tlds('.py', false); - this.use(Imsize); - this.use(Footnote); - this.use(Mark); - this.use(Anchor); - this.use(TOC); - } -} - -export const md = new Markdown(); - -export function plugin(func: Plugin, ...args: any[]) { - md.use(func, ...args); -} - -export function render(text: string, html = false) { - md.set({ html: !!html }); - return md.render(text); -} - -export function renderInline(text: string, html = false) { - md.set({ html: !!html }); - return md.renderInline(text); -} - -global.Hydro.lib.markdown = { - md, plugin, render, renderInline, -}; diff --git a/hydro/lib/paginate.ts b/hydro/lib/paginate.ts index 8cb8f5a76..54a5a6397 100644 --- a/hydro/lib/paginate.ts +++ b/hydro/lib/paginate.ts @@ -3,7 +3,7 @@ import { ValidationError } from '../error'; export default async function paginate( cursor: Cursor, page: number, pageSize: number, -): Promise<[any[], number, number]> { +): Promise<[any[]/* docs */, number/* numPages */, number/* count */]> { if (page <= 0) throw new ValidationError('page'); const [count, pageDocs] = await Promise.all([ cursor.count(), diff --git a/hydro/lib/template.ts b/hydro/lib/template.ts deleted file mode 100644 index 19a8359b1..000000000 --- a/hydro/lib/template.ts +++ /dev/null @@ -1,78 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import serialize from 'serialize-javascript'; -import nunjucks from 'nunjucks'; -import html2text from 'html-to-text'; -import { argv } from 'yargs'; -import * as markdown from './markdown'; -import * as misc from './misc'; - -class Loader extends nunjucks.Loader { - // eslint-disable-next-line class-methods-use-this - getSource(name: string) { - if (!argv.template) { - if (!global.Hydro.ui.template[name]) throw new Error(`Cannot get template ${name}`); - return { - src: global.Hydro.ui.template[name], - path: name, - noCache: false, - }; - } - let fullpath = null; - const p = path.resolve(process.cwd(), argv.template as string, name); - if (fs.existsSync(p)) fullpath = p; - if (!fullpath) throw new Error(`Cannot get template ${name}`); - return { - src: fs.readFileSync(fullpath, 'utf-8').toString(), - path: fullpath, - noCache: true, - }; - } -} - -class Nunjucks extends nunjucks.Environment { - constructor() { - super(new Loader(), { autoescape: true, trimBlocks: true }); - this.addFilter('json', (self) => JSON.stringify(self), false); - this.addFilter('serialize', (self, ignoreFunction = true) => serialize(self, { ignoreFunction })); - this.addFilter('assign', (self, data) => Object.assign(self, data)); - this.addFilter('markdown', (self, html = false) => markdown.render(self, html)); - this.addFilter('ansi', (self) => misc.ansiToHtml(self)); - this.addFilter('inline', (self) => html2text.fromString(self).replace(/\n/gmi, ' ')); - this.addFilter('base64_encode', (s) => Buffer.from(s).toString('base64')); - this.addFilter('bitand', (self, val) => self & val); - } -} -const env = new Nunjucks(); - -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: (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: 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, - Context: global.Hydro.ui, - isIE: (str: string) => str.includes('MSIE') || str.includes('rv:11.0'), - }, (err, res) => { - if (err) reject(err); - else resolve(res); - }); - }); -} - -global.Hydro.lib.template = { render }; diff --git a/hydro/model/record.ts b/hydro/model/record.ts index 1548cb094..b1ae22cf4 100644 --- a/hydro/model/record.ts +++ b/hydro/model/record.ts @@ -6,7 +6,6 @@ import * as problem from './problem'; import { Rdoc, TestCase, PretestConfig, ContestInfo, ProblemConfig, } from '../interface'; -import { RecordNotFoundError } from '../error'; import * as db from '../service/db'; const coll = db.collection('record'); @@ -48,7 +47,9 @@ export interface JudgeTask { } export async function get(domainId: string, _id: ObjectID): Promise { - return await coll.findOne({ domainId, _id }); + const res = await coll.findOne({ _id }); + if (res && res.domainId === domainId) return res; + return null; } export async function judge(domainId: string, rid: ObjectID) { diff --git a/hydro/model/token.ts b/hydro/model/token.ts index 52965a604..93238bf1b 100644 --- a/hydro/model/token.ts +++ b/hydro/model/token.ts @@ -1,3 +1,4 @@ +import { TokenDoc } from '../interface'; import * as db from '../service/db'; const coll = db.collection('token'); @@ -16,7 +17,9 @@ function ensureIndexes() { ]); } -export async function add(tokenType: number, expireSeconds: number, data: any) { +export async function add( + tokenType: number, expireSeconds: number, data: any, +): Promise<[string, TokenDoc]> { const now = new Date(); const str = String.random(32); const res = await coll.insertOne({ @@ -30,9 +33,8 @@ export async function add(tokenType: number, expireSeconds: number, data: any) { return [str, res.ops[0]]; } -export async function get(tokenId: string, tokenType: number) { - const res = await coll.findOne({ _id: tokenId, tokenType }); - return res; +export async function get(tokenId: string, tokenType: number): Promise { + return await coll.findOne({ _id: tokenId, tokenType }); } export async function update( @@ -73,11 +75,11 @@ export async function createOrUpdate( } export function getSessionListByUid(uid: number) { - return coll.find({ uid, tokenType: this.TYPE_SESSION }).sort('updateAt', -1).toArray(); + return coll.find({ uid, tokenType: TYPE_SESSION }).sort('updateAt', -1).toArray(); } export function getMostRecentSessionByUid(uid: number) { - return coll.findOne({ uid, tokenType: this.TYPE_SESSION }, { sort: { updateAt: -1 } }); + return coll.findOne({ uid, tokenType: TYPE_SESSION }, { sort: { updateAt: -1 } }); } export function delByUid(uid: number) { diff --git a/hydro/model/user.ts b/hydro/model/user.ts index bc0889c5b..c0bfb351d 100644 --- a/hydro/model/user.ts +++ b/hydro/model/user.ts @@ -199,7 +199,7 @@ export async function create( return uid; } -export function getMulti(params) { +export function getMulti(params: any) { return coll.find(params); } @@ -210,7 +210,7 @@ export async function getPrefixList(prefix: string, limit = 50) { return udocs; } -export async function setPriv(uid: number, priv: number) { +export async function setPriv(uid: number, priv: number): Promise { const udoc = await coll.findOneAndUpdate( { _id: uid }, { $set: { priv } }, diff --git a/hydro/script/rating.ts b/hydro/script/rating.ts index 26884c6c8..30230b94f 100644 --- a/hydro/script/rating.ts +++ b/hydro/script/rating.ts @@ -1,5 +1,5 @@ /* eslint-disable no-await-in-loop */ -import { NumericDictionary, filter } from 'lodash'; +import { NumericDictionary } from 'lodash'; import { ObjectID } from 'mongodb'; import { Tdoc, Pdoc } from '../interface'; import * as domain from '../model/domain'; @@ -8,23 +8,12 @@ import * as problem from '../model/problem'; import * as record from '../model/record'; import { STATUS } from '../model/builtin'; import rating from '../lib/rating'; +import paginate from '../lib/paginate'; export const description = 'Calculate rp of a domain, or all domains'; type ND = NumericDictionary -function calc(udict: ND, rankedDocs: [number, number][]) { - const users = []; - for (const [rk, uid] of rankedDocs) { - users.push({ uid, rank: rk, old: udict[uid] || 1500 }); - } - // FIXME sum(rating.new) always less than sum(rating.old) - const rated = rating(users); - for (const udoc of rated) { - udict[udoc.uid] = udoc.new; - } -} - async function runProblem(pdoc: Pdoc, udict: ND): Promise async function runProblem(domainId: string, pid: number, udict: ND): Promise async function runProblem(...arg: any[]) { @@ -32,17 +21,31 @@ async function runProblem(...arg: any[]) { ? await problem.get(arg[0], arg[1], -1) : arg[0]; const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1]; - const psdocs = await problem.getMultiStatus( - pdoc.domainId, { docId: pdoc.docId, rid: { $ne: null } }, - ).toArray(); - if (!psdocs.length) return; - const rdict = await record.getList(pdoc.domainId, psdocs.map((psdoc) => psdoc.rid), true); - const nAccept = filter(psdocs, (psdoc) => psdoc.status === STATUS.STATUS_ACCEPTED).length; + const [, nPages] = await paginate( + problem.getMultiStatus( + pdoc.domainId, { docId: pdoc.docId, rid: { $ne: null } }, + ), + 1, + 100, + ); + const nAccept = await problem.getMultiStatus( + pdoc.domainId, { docId: pdoc.docId, status: STATUS.STATUS_ACCEPTED }, + ).count(); const p = (pdoc.difficulty || 5) / (Math.sqrt(Math.sqrt(nAccept)) + 1); - for (const psdoc of psdocs) { - if (rdict[psdoc.rid]) { - const rp = rdict[psdoc.rid].score * p; - udict[psdoc.uid] = (udict[psdoc.uid] || 1500) + rp; + for (let page = 1; page <= nPages; page++) { + const [psdocs] = await paginate( + problem.getMultiStatus( + pdoc.domainId, { docId: pdoc.docId, rid: { $ne: null } }, + ), + page, + 100, + ); + const rdict = await record.getList(pdoc.domainId, psdocs.map((psdoc) => psdoc.rid), true); + for (const psdoc of psdocs) { + if (rdict[psdoc.rid]) { + const rp = rdict[psdoc.rid].score * p; + udict[psdoc.uid] = (udict[psdoc.uid] || 1500) + rp; + } } } } @@ -62,11 +65,13 @@ async function runContest(...arg: any[]) { .sort(contest.RULES[tdoc.rule].statusSort).toArray(); if (!tsdocs.length) return; const rankedTsdocs = contest.RULES[tdoc.rule].rank(tsdocs); - const ranked = []; + const users = []; for (const result of rankedTsdocs) { - ranked.push([result[0], result[1].uid]); + users.push({ uid: result[1].uid, rank: result[0], old: udict[result[1].uid] || 1500 }); } - calc(udict, ranked); + // FIXME sum(rating.new) always less than sum(rating.old) + const rated = rating(users); + for (const udoc of rated) udict[udoc.uid] = udoc.new; await report({ case: { status: STATUS.STATUS_ACCEPTED, @@ -79,7 +84,6 @@ async function runContest(...arg: any[]) { } async function runInDomain(domainId: string, isSub: boolean, report: Function) { - await domain.setMultiUserInDomain(domainId, {}, { rp: 1500 }); const udict: ND = {}; const contests = await contest.getMulti(domainId, { rated: true }, -1).sort('endAt', -1).toArray(); await report({ message: `Found ${contests.length} contests in ${domainId}` }); @@ -104,6 +108,7 @@ async function runInDomain(domainId: string, isSub: boolean, report: Function) { }); } } + await domain.setMultiUserInDomain(domainId, {}, { rp: 1500 }); const tasks = []; for (const uid in udict) { tasks.push(domain.setUserInDomain(domainId, parseInt(uid, 10), { rp: udict[uid] })); diff --git a/hydro/script/setSuperadmin.ts b/hydro/script/setSuperadmin.ts index 10404b2fc..5eb05142f 100644 --- a/hydro/script/setSuperadmin.ts +++ b/hydro/script/setSuperadmin.ts @@ -1,3 +1,4 @@ +// TODO add a set_priv interface import * as user from '../model/user'; import { PRIV } from '../model/builtin'; diff --git a/hydro/service/server.ts b/hydro/service/server.ts index 1bd62808a..c84d2dfe2 100644 --- a/hydro/service/server.ts +++ b/hydro/service/server.ts @@ -22,7 +22,6 @@ import { CsrfTokenError, InvalidOperationError, MethodNotAllowedError, NotFoundError, HydroError, } from '../error'; -import { render } from '../lib/template'; import hash from '../lib/hash.hydro'; import * as misc from '../lib/misc'; import * as user from '../model/user'; @@ -276,7 +275,7 @@ export class Handler { gravatar: misc.gravatar(this.user.gravatar || '', 128), perm: this.user.perm.toString(), }; - const res = await render(name, { + const res = await global.Hydro.lib.template.render(name, { handler: this, UserContext, url: this.url.bind(this), @@ -615,7 +614,7 @@ export class ConnectionHandler { } async renderHTML(name: string, context: any): Promise { - const res = await render(name, Object.assign(context, { + const res = await global.Hydro.lib.template.render(name, Object.assign(context, { handler: this, url: this.url.bind(this), _: this.translate.bind(this), diff --git a/package.json b/package.json index 5ac87a50d..0c9d986a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.11.1", + "version": "2.11.2", "main": "dist/loader.js", "bin": "bin/hydrooj.js", "repository": "https://github.com/hydro-dev/Hydro.git",