diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 52549ed29..f97ca5b05 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.15.16", + "version": "2.15.17", "bin": "bin/hydrooj.js", "main": "dist/loader.js", "typings": "dist/loader.d.ts", diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index 47e01377c..7e6c98f62 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -73,16 +73,14 @@ class RecordDetailHandler extends RecordHandler { const tdoc = await contest.get(domainId, rdoc.contest.tid, rdoc.contest.type); if (!this.canShowRecord(tdoc, true)) throw new PermissionError(rid); } - if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM.PERM_READ_RECORD_CODE)) { - rdoc.code = null; - } + if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM.PERM_READ_RECORD_CODE)) rdoc.code = null; // eslint-disable-next-line prefer-const let [pdoc, udoc] = await Promise.all([ problem.get(domainId, rdoc.pid), user.getById(domainId, rdoc.uid), ]); if (!rdoc.contest && pdoc.hidden && pdoc.owner !== this.user._id) { - if (this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) { + if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) { pdoc = problem.Pdoc.create(pdoc.docId, pdoc.pid); } } @@ -130,22 +128,6 @@ class RecordDetailHandler extends RecordHandler { } } -/** - * @deprecated - * use RecordDetailHandler.postRejudge instead. - */ -class RecordRejudgeHandler extends Handler { - @param('rid', Types.ObjectID) - async post(domainId: string, rid: ObjectID) { - const rdoc = await record.get(domainId, rid); - if (rdoc) { - await record.reset(domainId, rid, true); - await record.judge(domainId, rid, 0); - } - this.back(); - } -} - const RecordConnectionHandler = contest.ContestHandlerMixin(ConnectionHandler); class RecordMainConnectionHandler extends RecordConnectionHandler { @@ -230,7 +212,6 @@ class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(Connecti export async function apply() { Route('record_main', '/record', RecordListHandler); Route('record_detail', '/record/:rid', RecordDetailHandler); - Route('record_rejudge', '/record/:rid/rejudge', RecordRejudgeHandler); Connection('record_conn', '/record-conn', RecordMainConnectionHandler); Connection('record_detail_conn', '/record-detail-conn', RecordDetailConnectionHandler); } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index a5c5ad6b8..1c182f225 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -33,6 +33,7 @@ export interface SystemKeys { 'server.worker': number, 'server.port': number, 'server.language': string, + 'problem.categories': string, 'session.keys': string[], 'session.secure': boolean, 'session.expire_seconds': number, diff --git a/packages/hydrooj/src/model/blacklist.ts b/packages/hydrooj/src/model/blacklist.ts index 7164f2c55..011dbb040 100644 --- a/packages/hydrooj/src/model/blacklist.ts +++ b/packages/hydrooj/src/model/blacklist.ts @@ -4,11 +4,6 @@ import * as bus from '../service/bus'; const coll = db.collection('blacklist'); export async function add(ip: string) { - /** - * Add a ip into blacklist. - * @param {string} ip - * @returns {Promise} - */ const expireAt = new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000); const res = await coll.findOneAndUpdate( { _id: ip }, @@ -18,21 +13,16 @@ export async function add(ip: string) { return res.value; } -export function get(ip: string) { - /** - * Get a ip, return null if not. - * @param {string} ip - * @returns {Promise} - */ - return coll.findOne({ _id: ip }); +export async function get(ip: string) { + return await coll.findOne({ _id: ip }); } -export function del(ip: string) { - return coll.deleteOne({ _id: ip }); +export async function del(ip: string) { + return await coll.deleteOne({ _id: ip }); } -function ensureIndexes() { - return coll.createIndex('expireAt', { expireAfterSeconds: 0 }); +async function ensureIndexes() { + return await coll.createIndex('expireAt', { expireAfterSeconds: 0 }); } bus.once('app/started', ensureIndexes); diff --git a/packages/hydrooj/src/model/discussion.ts b/packages/hydrooj/src/model/discussion.ts index 2d93a7df6..23b6813e0 100644 --- a/packages/hydrooj/src/model/discussion.ts +++ b/packages/hydrooj/src/model/discussion.ts @@ -5,6 +5,7 @@ import * as training from './training'; import * as document from './document'; import { DocumentNotFoundError } from '../error'; import { Ddoc, Drdoc, Drrdoc } from '../interface'; +import * as bus from '../service/bus'; export const typeDisplay = { [document.TYPE_PROBLEM]: 'problem', @@ -215,6 +216,22 @@ export async function getListVnodes(domainId: string, ddocs: any, getHidden: boo return res; } +bus.on('problem/delete', async (domainId, docId) => { + const dids = await document.getMulti( + domainId, document.TYPE_DISCUSSION, + { parentType: document.TYPE_PROBLEM, parentId: docId }, + ).project({ docId: 1 }).map((ddoc) => ddoc.docId).toArray(); + const drids = await document.getMulti( + domainId, document.TYPE_DISCUSSION_REPLY, + { parentType: document.TYPE_DISCUSSION, parentId: { $in: dids } }, + ).project({ docId: 1 }).map((drdoc) => drdoc.docId).toArray(); + return await Promise.all([ + document.deleteMultiStatus(domainId, document.TYPE_DISCUSSION, { docId: { $in: dids } }), + document.deleteMulti(domainId, document.TYPE_DISCUSSION, { docId: { $in: dids } }), + document.deleteMulti(domainId, document.TYPE_DISCUSSION_REPLY, { docId: { $in: drids } }), + ]); +}); + global.Hydro.model.discussion = { typeDisplay, add, diff --git a/packages/hydrooj/src/model/document.ts b/packages/hydrooj/src/model/document.ts index f4528eed3..79bee756b 100644 --- a/packages/hydrooj/src/model/document.ts +++ b/packages/hydrooj/src/model/document.ts @@ -25,7 +25,6 @@ export const TYPE_DISCUSSION = 21; export const TYPE_DISCUSSION_REPLY = 22; export const TYPE_CONTEST = 30; export const TYPE_TRAINING = 40; -export const TYPE_FILE = 50; export const TYPE_HOMEWORK = 60; export interface DocType { @@ -37,7 +36,6 @@ export interface DocType { [TYPE_DISCUSSION_REPLY]: Drdoc, [TYPE_CONTEST]: Tdoc, [TYPE_TRAINING]: TrainingDoc, - [TYPE_FILE]: any, [TYPE_HOMEWORK]: Tdoc, } @@ -457,6 +455,5 @@ global.Hydro.model.document = { TYPE_PROBLEM, TYPE_PROBLEM_LIST, TYPE_PROBLEM_SOLUTION, - TYPE_FILE, TYPE_TRAINING, }; diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index 1f68f8811..ba4adab03 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -9,6 +9,7 @@ import { Content } from '../loader'; import { ArrayKeys, NumberKeys, Projection } from '../typeutils'; import { ProblemNotFoundError } from '../error'; import storage from '../service/storage'; +import * as bus from '../service/bus'; export interface Pdoc { _id: ObjectID @@ -207,6 +208,11 @@ export function setStar(domainId: string, pid: number, uid: number, star: boolea return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star }); } +bus.on('problem/delete', (domainId, docId) => Promise.all([ + document.deleteOne(domainId, document.TYPE_PROBLEM, docId), + document.deleteMultiStatus(domainId, document.TYPE_PROBLEM, { docId }), +])); + global.Hydro.model.problem = { Pdoc, diff --git a/packages/hydrooj/src/model/record.ts b/packages/hydrooj/src/model/record.ts index 4de171bf8..28a6d1eb4 100644 --- a/packages/hydrooj/src/model/record.ts +++ b/packages/hydrooj/src/model/record.ts @@ -2,12 +2,15 @@ import { ObjectID, Collection, UpdateQuery, PushOperator, MatchKeysAndValues, OnlyFieldsOfType, } from 'mongodb'; import { Dictionary } from 'lodash'; +import moment from 'moment'; import { STATUS } from './builtin'; import * as task from './task'; import * as problem from './problem'; import { Rdoc, TestCase, RunConfig, ContestInfo, ProblemConfig, } from '../interface'; +import { Time } from '../utils'; +import * as bus from '../service/bus'; import db from '../service/db'; import storage from '../service/storage'; @@ -55,6 +58,19 @@ export async function get(domainId: string, _id: ObjectID): Promise return null; } +export async function stat(domainId?: string) { + const [d5min, d1h, d1d, d1m, total] = await Promise.all([ + coll.find({ _id: { $gte: Time.getObjectID(moment().add(-5, 'minutes')) }, ...domainId ? { domainId } : {} }).count(), + coll.find({ _id: { $gte: Time.getObjectID(moment().add(-1, 'hour')) }, ...domainId ? { domainId } : {} }).count(), + coll.find({ _id: { $gte: Time.getObjectID(moment().add(-1, 'day')) }, ...domainId ? { domainId } : {} }).count(), + coll.find({ _id: { $gte: Time.getObjectID(moment().add(-1, 'month')) }, ...domainId ? { domainId } : {} }).count(), + coll.find(domainId ? { domainId } : {}).count(), + ]); + return { + d5min, d1h, d1d, d1m, total, + }; +} + export async function judge(domainId: string, rid: ObjectID, priority = 1) { const rdoc = await get(domainId, rid); let config: RunConfig | ProblemConfig = rdoc.config; @@ -186,11 +202,14 @@ export function getByUid(domainId: string, uid: number, limit: number): Promise< return coll.find({ domainId, uid }).limit(limit).toArray(); } +bus.on('problem/delete', (domainId, docId) => coll.deleteMany({ domainId, pid: docId })); + global.Hydro.model.record = { coll, add, get, getMulti, + stat, update, count, reset, diff --git a/packages/hydrooj/src/model/solution.ts b/packages/hydrooj/src/model/solution.ts index 052f412f5..d4bb726af 100644 --- a/packages/hydrooj/src/model/solution.ts +++ b/packages/hydrooj/src/model/solution.ts @@ -1,6 +1,7 @@ import { ObjectID } from 'mongodb'; import * as document from './document'; import { SolutionNotFoundError } from '../error'; +import * as bus from '../service/bus'; export function add(domainId: string, pid: number, owner: number, content: string) { return document.add( @@ -26,8 +27,11 @@ export function edit(domainId: string, psid: ObjectID, content: string) { return document.set(domainId, document.TYPE_PROBLEM_SOLUTION, psid, { content }); } -export function del(domainId: string, psid: ObjectID) { - return document.deleteOne(domainId, document.TYPE_PROBLEM_SOLUTION, psid); +export async function del(domainId: string, psid: ObjectID) { + return await Promise.all([ + document.deleteOne(domainId, document.TYPE_PROBLEM_SOLUTION, psid), + document.deleteMultiStatus(domainId, document.TYPE_PROBLEM_SOLUTION, { docId: psid }), + ]); } export function count(domainId: string, query: any) { @@ -75,6 +79,17 @@ export async function getListStatus(domainId: string, psids: ObjectID[], uid: nu return result; } +bus.on('problem/delete', async (domainId, docId) => { + const psids = await document.getMulti( + domainId, document.TYPE_PROBLEM_SOLUTION, + { parentType: document.TYPE_PROBLEM, parentId: docId }, + ).project({ docId: 1 }).map((psdoc) => psdoc.docId).toArray(); + return await Promise.all([ + document.deleteMulti(domainId, document.TYPE_PROBLEM_SOLUTION, { docId: { $in: psids } }), + document.deleteMultiStatus(domainId, document.TYPE_PROBLEM_SOLUTION, { docId: { $in: psids } }), + ]); +}); + global.Hydro.model.solution = { count, add, diff --git a/packages/hydrooj/src/service/bus.ts b/packages/hydrooj/src/service/bus.ts index 9f9123c12..7e7c0279c 100644 --- a/packages/hydrooj/src/service/bus.ts +++ b/packages/hydrooj/src/service/bus.ts @@ -17,7 +17,7 @@ function isBailed(value: any) { } export type Disposable = () => void -export type VoidReturn = Promise | void +export type VoidReturn = Promise | any export interface EventMap { 'app/started': () => void @@ -57,6 +57,7 @@ export interface EventMap { 'problem/list': (query: FilterQuery, handler: any) => VoidReturn 'problem/setting': (update: Partial, handler: any) => VoidReturn 'problem/get': (doc: Pdoc, handler: any) => VoidReturn + 'problem/delete': (domainId: string, docId: number) => VoidReturn 'training/list': (query: FilterQuery, handler: any) => VoidReturn 'training/get': (tdoc: TrainingDoc, handler: any) => VoidReturn diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index 4e0b39173..ce8069393 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -293,13 +293,68 @@ export async function prepare() { })); } -export class Handler { - UIContext: any; - - args: any; - +export class HandlerCommon { domainId: string; + domain: DomainDoc; + user: User; + session: Record; + request: Record; + async limitRate(op: string, periodSecs: number, maxOperations: number) { + await opcount.inc(op, this.request.ip, periodSecs, maxOperations); + } + + translate(str: string) { + if (!str) return ''; + return str.toString().translate(this.user.viewLang, this.session.viewLang); + } + + renderTitle(str: string) { + return `${this.translate(str)} - ${system.get('server.name')}`; + } + + checkPerm(...args: bigint[]) { + // @ts-ignore + if (!this.user.hasPerm(...args)) throw new PermissionError(...args); + } + + checkPriv(...args: number[]) { + // @ts-ignore + if (!this.user.hasPriv(...args)) throw new PrivilegeError(...args); + } + + url(name: string, kwargs: any = {}) { + let res = '#'; + const args: any = {}; + const query: any = {}; + for (const key in kwargs) { + if (kwargs[key] instanceof ObjectID) args[key] = kwargs[key].toHexString(); + else args[key] = kwargs[key].toString(); + } + for (const key in kwargs.query || {}) { + if (query[key] instanceof ObjectID) query[key] = kwargs.query[key].toHexString(); + else query[key] = kwargs.query[key].toString(); + } + try { + if (this.domainId !== 'system' || args.domainId) { + name += '_with_domainId'; + args.domainId = args.domainId || this.domainId; + } + const { anchor } = args; + res = router.url(name, args, { query }); + if (anchor) return `${res}#${anchor}`; + } catch (e) { + logger.warn(e.message); + logger.info('%s %o', name, args); + logger.info('%s', e.stack); + } + return res; + } +} + +export class Handler extends HandlerCommon { + UIContext: any; + args: any; ctx: Koa.Context; request: { @@ -327,19 +382,12 @@ export class Handler { attachment: (name: string, stream?: any) => void, }; - session: any; - csrfToken: string; - - user: User; - - domain: DomainDoc; - loginMethods: any; - __param: Record; constructor(ctx: Koa.Context) { + super(); this.ctx = ctx; this.request = { host: ctx.request.host, @@ -405,57 +453,6 @@ export class Handler { return res; } - async limitRate(op: string, periodSecs: number, maxOperations: number) { - await opcount.inc(op, this.request.ip, periodSecs, maxOperations); - } - - translate(str: string) { - if (!str) return ''; - return str.toString().translate(this.user.viewLang, this.session.viewLang); - } - - renderTitle(str: string) { - return `${this.translate(str)} - ${system.get('server.name')}`; - } - - checkPerm(...args: bigint[]) { - // @ts-ignore - if (!this.user.hasPerm(...args)) throw new PermissionError(...args); - } - - checkPriv(...args: number[]) { - // @ts-ignore - if (!this.user.hasPriv(...args)) throw new PrivilegeError(...args); - } - - url(name: string, kwargs: any = {}) { - let res = '#'; - const args: any = {}; - const query: any = {}; - for (const key in kwargs) { - if (kwargs[key] instanceof ObjectID) args[key] = kwargs[key].toHexString(); - else args[key] = kwargs[key].toString(); - } - for (const key in kwargs.query || {}) { - if (query[key] instanceof ObjectID) query[key] = kwargs.query[key].toHexString(); - else query[key] = kwargs.query[key].toString(); - } - try { - if (this.domainId !== 'system' || args.domainId) { - name += '_with_domainId'; - args.domainId = args.domainId || this.domainId; - } - const { anchor } = args; - res = router.url(name, args, { query }); - if (anchor) return `${res}#${anchor}`; - } catch (e) { - logger.warn(e.message); - logger.info('%s %o', name, args); - logger.info('%s', e.stack); - } - return res; - } - async render(name: string, context: any) { this.response.body = await this.renderHTML(name, context); this.response.type = 'text/html'; @@ -584,8 +581,8 @@ export class Handler { async saveCookie() { const expireSeconds = this.session.save - ? system.get('session.expire_seconds') as number - : system.get('session.unsaved_expire_seconds') as number; + ? system.get('session.expire_seconds') + : system.get('session.unsaved_expire_seconds'); if (this.session._id) { await token.update( this.session._id, @@ -614,10 +611,6 @@ export class Handler { secure: !!system.get('session.secure'), httpOnly: false, }; - if (this.session.save) { - cookie.expires = this.session.expireAt; - cookie.maxAge = expireSeconds; - } this.ctx.cookies.set('sid', this.session._id, cookie); } @@ -717,26 +710,17 @@ export function Route(name: string, path: string, RouteHandler: any, ...permPriv router.all(`${name}_with_domainId`, `/d/:domainId${path}`, (ctx) => handle(ctx, RouteHandler, checker)); } -export class ConnectionHandler { +export class ConnectionHandler extends HandlerCommon { conn: sockjs.Connection; - + args: any; request: { params: any; headers: any; ip: string; } - domainId: string; - - session: any; - - args: any; - - user: User; - - domain: DomainDoc; - constructor(conn: sockjs.Connection) { + super(); this.conn = conn; this.request = { params: {}, @@ -758,54 +742,6 @@ export class ConnectionHandler { return res; } - async limitRate(op: string, periodSecs: number, maxOperations: number) { - await opcount.inc(op, this.request.ip, periodSecs, maxOperations); - } - - translate(str: string) { - return str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''; - } - - renderTitle(str: string) { - return `${this.translate(str)} - ${system.get('server.name')}`; - } - - checkPerm(...args: bigint[]) { - if (!this.user.hasPerm(...args)) throw new PermissionError(...args); - } - - checkPriv(...args: number[]) { - // @ts-ignore - if (!this.user.hasPriv(...args)) throw new PrivilegeError(...args); - } - - url(name: string, kwargs: any = {}) { - let res = '#'; - const args: any = {}; - const query: any = {}; - for (const key in kwargs) { - if (kwargs[key] instanceof ObjectID) args[key] = kwargs[key].toHexString(); - else args[key] = kwargs[key].toString(); - } - for (const key in kwargs.query || {}) { - if (query[key] instanceof ObjectID) query[key] = kwargs.query[key].toHexString(); - else query[key] = kwargs.query[key].toString(); - } - try { - if (this.args.domainId !== 'system' || args.domainId) { - name += '_with_domainId'; - args.domainId = args.domainId || this.args.domainId; - } - const { anchor } = args; - res = router.url(name, args, { query }); - if (anchor) return `${res}#${anchor}`; - } catch (e) { - logger.error(e.message); - logger.info(name, args); - } - return res; - } - send(data: any) { this.conn.write(JSON.stringify(data)); } @@ -934,6 +870,7 @@ global.Hydro.service.server = { route, param, requireCsrfToken, + HandlerCommon, Handler, ConnectionHandler, Route, diff --git a/packages/hydrooj/src/utils.ts b/packages/hydrooj/src/utils.ts index e2afd3d4a..58d9e13a0 100644 --- a/packages/hydrooj/src/utils.ts +++ b/packages/hydrooj/src/utils.ts @@ -4,6 +4,9 @@ import cluster from 'cluster'; import path from 'path'; import serialize from 'serialize-javascript'; import Lru from 'lru-cache'; +import { ObjectID } from 'mongodb'; +import { isMoment } from 'moment'; +import { Moment } from 'moment-timezone'; declare global { interface StringConstructor { @@ -240,4 +243,14 @@ export namespace Time { } return `${ms}ms`; } + + export function getObjectID(timestamp: string | Date | Moment) { + let _timestamp: number; + if (typeof timestamp === 'string') _timestamp = new Date(timestamp).getTime(); + else if (isMoment(timestamp)) _timestamp = timestamp.toDate().getTime(); + else _timestamp = timestamp.getTime(); + const hexSeconds = Math.floor(_timestamp / 1000).toString(16); + const constructedObjectId = new ObjectID(`${hexSeconds}0000000000000000`); + return constructedObjectId; + } }