文件结构更改

This commit is contained in:
undefined committed 2020-08-04 13:42:42 +08:00
1 parent b3af28dc43
commit 1f1d12ad73
109 files changed
+544 -3296

No files matched your search

+420
View File
@@ -0,0 +1,420 @@
import moment from 'moment-timezone';
import { ObjectID } from 'mongodb';
import AdmZip from 'adm-zip';
import { isSafeInteger } from 'lodash';
import {
ContestNotLiveError, ValidationError, ProblemNotFoundError,
ContestNotAttendedError, PermissionError,
} from '../error';
import { isContent, isTitle } from '../lib/validator';
import paginate from '../lib/paginate';
import { PERM, PRIV, CONSTANT } from '../model/builtin';
import * as contest from '../model/contest';
import * as document from '../model/document';
import * as problem from '../model/problem';
import * as record from '../model/record';
import * as user from '../model/user';
import * as message from '../model/message';
import {
Route, Handler, Types, param,
} from '../service/server';
import * as bus from '../service/bus';
const ContestHandler = contest.ContestHandlerMixin(Handler);
class ContestListHandler extends ContestHandler {
@param('rule', Types.String, true)
@param('page', Types.UnsignedInt, true)
async get(domainId: string, rule = '', page = 1) {
this.response.template = 'contest_main.html';
let tdocs;
let qs;
let tpcount;
if (!rule) {
tdocs = contest.getMulti(domainId).sort({ beginAt: -1 });
qs = '';
} else {
if (!contest.RULES[rule]) throw new ValidationError('rule');
tdocs = contest.getMulti(domainId, { rule }).sort({ beginAt: -1 });
qs = `rule=${rule}`;
}
// eslint-disable-next-line prefer-const
[tdocs, tpcount] = await paginate(tdocs, page, CONSTANT.CONTEST_PER_PAGE);
const tids = [];
for (const tdoc of tdocs) tids.push(tdoc.docId);
const tsdict = await contest.getListStatus(domainId, this.user._id, tids);
const path = [
['Hydro', 'homepage'],
['contest_main', null],
];
this.response.body = {
page, tpcount, qs, rule, tdocs, tsdict, path,
};
}
}
class ContestDetailHandler extends ContestHandler {
@param('tid', Types.ObjectID)
@param('page', Types.UnsignedInt, true)
async get(domainId: string, tid: ObjectID, page = 1) {
const tdoc = await contest.get(domainId, tid);
this.response.template = 'contest_detail.html';
const [tsdoc, pdict] = await Promise.all([
contest.getStatus(domainId, tdoc.docId, this.user._id),
problem.getList(domainId, tdoc.pids, true),
]);
const psdict = {};
let rdict = {};
let attended: boolean;
if (tsdoc) {
attended = tsdoc.attend === 1;
for (const pdetail of tsdoc.journal || []) psdict[pdetail.pid] = pdetail;
if (this.canShowRecord(tdoc)) {
const q = [];
for (const i in psdict) q.push(psdict[i].rid);
rdict = await record.getList(domainId, q, true);
} else {
for (const i in psdict) rdict[psdict[i].rid] = { _id: psdict[i].rid };
}
} else attended = false;
const udict = await user.getList(domainId, [tdoc.owner]);
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
[tdoc.title, null, null, true],
];
this.response.body = {
path, tdoc, tsdoc, attended, udict, pdict, psdict, rdict, page,
};
}
@param('tid', Types.ObjectID)
async postAttend(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid);
if (contest.isDone(tdoc)) throw new ContestNotLiveError(tid);
await contest.attend(domainId, tid, this.user._id);
this.back();
}
}
class ContestBoardcastHandler extends ContestHandler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid);
if (tdoc.owner !== this.user._id) throw new PermissionError('Boardcast Message');
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
[tdoc.title, 'contest_detail', { tid }, true],
['contest_boardcast'],
];
this.response.template = 'contest_boardcast.html';
this.response.body = path;
}
@param('tid', Types.ObjectID)
@param('content', Types.String, isContent)
async post(domainId: string, tid: ObjectID, content: string) {
const tdoc = await contest.get(domainId, tid);
if (tdoc.owner !== this.user._id) throw new PermissionError('Boardcast Message');
const tsdocs = await contest.getMultiStatus(domainId, { docId: tid }).toArray();
const uids = Array.from(new Set(tsdocs.map((tsdoc) => tsdoc.uid)));
await Promise.all(
uids.map((uid) => message.send(this.user._id, uid, content, message.FLAG_ALERT)),
);
this.response.redirect = this.url('contest_detail', { tid });
}
}
class ContestScoreboardHandler extends ContestHandler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
const [tdoc, rows, udict] = await this.getScoreboard(domainId, tid);
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
[tdoc.title, 'contest_detail', { tid }, true],
['contest_scoreboard', null],
];
this.response.template = 'contest_scoreboard.html';
this.response.body = {
tdoc, rows, path, udict,
};
}
}
class ContestScoreboardDownloadHandler extends ContestHandler {
@param('tid', Types.ObjectID)
@param('ext', Types.String)
async get(domainId: string, tid: ObjectID, ext: string) {
const getContent = {
csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`,
html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }),
};
if (!getContent[ext]) throw new ValidationError('ext');
const [, rows] = await this.getScoreboard(domainId, tid, true);
this.binary(await getContent[ext](rows), `${this.tdoc.title}.${ext}`);
}
}
class ContestEditHandler extends ContestHandler {
@param('tid', Types.ObjectID)
async prepare(domainId: string, tid: ObjectID) {
this.tdoc = await contest.get(domainId, tid);
if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_CONTEST);
else this.checkPerm(PERM.PERM_EDIT_CONTEST_SELF);
}
async get() {
this.response.template = 'contest_edit.html';
const rules = {};
for (const i in contest.RULES) {
rules[i] = contest.RULES[i].TEXT;
}
const duration = (this.tdoc.endAt.getTime() - this.tdoc.beginAt.getTime()) / 3600 / 1000;
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
[this.tdoc.title, 'contest_detail', { tid: this.tdoc.docId }, true],
['contest_edit', null],
];
const dt = this.tdoc.beginAt;
this.response.body = {
rules,
tdoc: this.tdoc,
duration,
path,
pids: this.tdoc.pids.join(','),
date_text: dt.format('%Y-%m-%d'),
time_text: dt.format('%H:%M'),
page_name: 'contest_edit',
};
}
@param('beginAtDate', Types.Date)
@param('beginAtTime', Types.Time)
@param('duration', Types.Float)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('rule', Types.String)
@param('pids', Types.String)
@param('rated', Types.Boolean)
async post(
domainId: string, beginAtDate: string, beginAtTime: string, duration: number,
title: string, content: string, rule: string, _pids: string, rated = false,
) {
let pids = _pids.split(',').map((i) => {
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
return i;
});
const beginAtMoment = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
if (!beginAtMoment.isValid()) {
throw new ValidationError('beginAtDate', 'beginAtTime');
}
const endAt = beginAtMoment.clone().add(duration, 'hours').toDate();
if (beginAtMoment.isSameOrAfter(endAt)) throw new ValidationError('duration');
const beginAt = beginAtMoment.toDate();
pids = await this.verifyProblems(domainId, pids);
await contest.edit(domainId, this.tdoc.docId, {
title, content, rule, beginAt, endAt, pids, rated,
});
if (this.tdoc.beginAt !== beginAt || this.tdoc.endAt !== endAt
|| Array.isDiff(this.tdoc.pids, pids) || this.tdoc.rule !== rule) {
await contest.recalcStatus(domainId, this.tdoc.docId);
}
this.response.body = { tid: this.tdoc.docId };
this.response.redirect = this.url('contest_detail', { tid: this.tdoc.docId });
}
}
class ContestProblemHandler extends ContestHandler {
@param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt)
async _prepare(domainId: string, tid: ObjectID, pid: number) {
[this.tdoc, this.pdoc] = await Promise.all([
contest.get(domainId, tid),
problem.get(domainId, pid, this.user._id),
]);
[this.tsdoc, this.udoc] = await Promise.all([
contest.getStatus(domainId, this.tdoc.docId, this.user._id),
user.getById(domainId, this.tdoc.owner),
]);
this.attended = this.tsdoc && this.tsdoc.attend === 1;
this.response.template = 'problem_detail.html';
if (contest.isDone(this.tdoc)) {
if (!this.attended) throw new ContestNotAttendedError(this.tdoc.docId);
if (!contest.isOngoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc.docId);
}
if (!this.tdoc.pids.map((s) => s.toString()).includes(this.pdoc.docId.toString())) {
throw new ProblemNotFoundError(pid, this.tdoc.docId);
}
}
async get() {
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
[this.tdoc.title, 'contest_detail', { tid: this.tdoc.docId }, true],
[this.pdoc.title, null, null, true],
];
this.response.body = {
tdoc: this.tdoc,
pdoc: this.pdoc,
tsdoc: this.tsdoc,
udoc: this.udoc,
attended: this.attended,
path,
};
}
}
class ContestProblemSubmitHandler extends ContestProblemHandler {
@param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt)
async prepare(domainId: string, tid: ObjectID, pid: number) {
let rdocs = [];
if (this.canShowRecord(this.tdoc)) {
rdocs = await record.getUserInProblemMulti(
domainId, this.user._id,
this.pdoc.docId, true,
).sort({ _id: -1 }).limit(10).toArray();
}
this.response.template = 'problem_submit.html';
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
[this.tdoc.title, 'contest_detail', { tid }, true],
[this.pdoc.title, 'contest_detail_problem', { tid, pid }, true],
['contest_detail_problem_submit', null],
];
this.response.body = {
tdoc: this.tdoc,
pdoc: this.pdoc,
tsdoc: this.tsdoc,
udoc: this.udoc,
attended: this.attend,
path,
rdocs,
};
}
@param('tid', Types.ObjectID)
@param('lang', Types.String)
@param('code', Types.String)
async post(domainId: string, tid: ObjectID, lang: string, code: string) {
await this.limitRate('add_record', 60, 100);
const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true, {
type: document.TYPE_CONTEST,
tid,
});
const [rdoc] = await Promise.all([
record.get(domainId, rid),
contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId),
]);
bus.publish('record_change', rdoc);
if (!this.canShowRecord(this.tdoc)) {
this.response.body = { tid };
this.response.redirect = this.url('contest_detail', { tid });
} else {
this.response.body = { rid };
this.response.redirect = this.url('record_detail', { rid });
}
}
}
class ContestCodeHandler extends ContestHandler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
if (!this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) {
this.checkPerm(PERM.PERM_READ_RECORD_CODE);
}
this.limitRate('homework_code', 3600, 60);
const [tdoc, tsdocs] = await contest.getAndListStatus(domainId, tid);
const rnames = {};
for (const tsdoc of tsdocs) {
for (const pdetail of tsdoc.detail || []) {
rnames[pdetail.rid] = `U${tsdoc.uid}_P${pdetail.pid}_R${pdetail.rid}`;
}
}
const zip = new AdmZip();
const rdocs = await record.getMulti(domainId, {
_id: {
$in: Array.from(Object.keys(rnames)).map((id) => new ObjectID(id)),
},
}).toArray();
for (const rdoc of rdocs) {
zip.addFile(`${rnames[rdoc._id]}.${rdoc.lang}`, rdoc.code);
}
await this.binary(zip.toBuffer(), `${tdoc.title}.zip`);
}
}
class ContestCreateHandler extends ContestHandler {
async get() {
this.response.template = 'contest_edit.html';
const rules = {};
for (const i in contest.RULES) { rules[i] = contest.RULES[i].TEXT; }
const now = new Date();
let ts = now.getTime();
ts = ts - (ts % (15 * 60 * 1000)) + 15 * 60 * 1000;
const dt = new Date(ts);
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
['contest_create', null],
];
this.response.body = {
rules,
path,
page_name: 'contest_create',
date_text: dt.format('%Y-%m-%d'),
time_text: dt.format('%M:%S'),
pids: '1000, 1001',
};
}
@param('beginAtDate', Types.Date)
@param('beginAtTime', Types.Time)
@param('duration', Types.Float)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('rule', Types.String)
@param('pids', Types.String)
@param('rated', Types.Boolean)
async post(
domainId: string, beginAtDate: string, beginAtTime: string, duration: number,
title: string, content: string, rule: string, _pids: string, rated = false,
) {
let pids = _pids.split(',').map((i) => {
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
return i;
});
const beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
if (!beginAt.isValid()) {
throw new ValidationError('beginAtDate', 'beginAtTime');
}
const endAt = beginAt.clone().add(duration, 'hours');
pids = await this.verifyProblems(domainId, pids);
const tid = await contest.add(
domainId, title, content,
this.user._id, rule, beginAt.toDate(), endAt.toDate(), pids, rated,
);
this.response.body = { tid };
this.response.redirect = this.url('contest_detail', { tid });
}
}
export async function apply() {
Route('contest_create', '/contest/create', ContestCreateHandler, PERM.PERM_CREATE_CONTEST);
Route('contest_main', '/contest', ContestListHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_boardcast', '/contest/:tid/boardcast', ContestBoardcastHandler);
Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_scoreboard_download', '/contest/:tid/export/:ext', ContestScoreboardDownloadHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_detail_problem', '/contest/:tid/p/:pid', ContestProblemHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_detail_problem_submit', '/contest/:tid/p/:pid/submit', ContestProblemSubmitHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST);
}
global.Hydro.handler.contest = apply;
+372
View File
@@ -0,0 +1,372 @@
import { ObjectID } from 'mongodb';
import { isSafeInteger } from 'lodash';
import { DiscussionNotFoundError, DocumentNotFoundError } from '../error';
import {
Mdoc, Drdoc, Ddoc, Drrdoc,
} from '../interface';
import paginate from '../lib/paginate';
import * as user from '../model/user';
import * as message from '../model/message';
import * as discussion from '../model/discussion';
import * as document from '../model/document';
import { PERM, PRIV, CONSTANT } from '../model/builtin';
import {
Route, Handler, Types, param,
} from '../service/server';
import { isTitle, isContent } from '../lib/validator';
export const typeMapper = {
problem: document.TYPE_PROBLEM,
contest: document.TYPE_CONTEST,
node: document.TYPE_DISCUSSION_NODE,
training: document.TYPE_TRAINING,
homework: document.TYPE_HOMEWORK,
};
class DiscussionHandler extends Handler {
ddoc?: Ddoc;
drdoc?: Drdoc;
drrdoc?: Drrdoc;
vnode?: any;
@param('type', Types.String, true)
@param('name', Types.String, true)
@param('did', Types.ObjectID, true)
@param('drid', Types.ObjectID, true)
@param('drrid', Types.ObjectID, true)
async _prepare(
domainId: string, type: string, name: string,
did: ObjectID, drid: ObjectID, drrid: ObjectID,
) {
this.checkPerm(PERM.PERM_VIEW_DISCUSSION);
if (did) {
this.ddoc = await discussion.get(domainId, did);
if (!this.ddoc) throw new DiscussionNotFoundError(did);
type = discussion.typeDisplay[this.ddoc.parentType];
name = this.ddoc.parentId.toString();
if (drrid) {
[this.drdoc, this.drrdoc] = await discussion.getTailReply(domainId, drid, drrid);
if (!this.drrdoc) throw new DiscussionNotFoundError(drrid);
} else if (drid) {
this.drdoc = await discussion.getReply(domainId, drid);
if (!this.drdoc) throw new DiscussionNotFoundError(drid);
if (!this.drdoc.parentId.equals(this.ddoc._id)) {
throw new DocumentNotFoundError(drid);
}
}
}
// TODO(twd2): do more visibility check eg. contest
// TODO(twd2): exclude problem/contest discussions?
// TODO(iceboy): continuation based pagination.
this.vnode = await discussion.getVnode(domainId, typeMapper[type], name);
if (this.vnode.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
if (this.ddoc) {
this.ddoc.parentType = this.ddoc.parentType || this.vnode.type;
this.ddoc.parentId = this.ddoc.parentId || this.vnode.id;
}
}
}
class DiscussionMainHandler extends Handler {
@param('page', Types.UnsignedInt, true)
async get(domainId: string, page = 1) {
const [ddocs, dpcount] = await paginate(
discussion.getMulti(domainId),
page,
CONSTANT.DISCUSSION_PER_PAGE,
);
const udict = await user.getList(domainId, ddocs.map((ddoc) => ddoc.owner));
const path = [
['Hydro', 'homepage'],
['discussion_main', null],
];
const [vndict, vnodes] = await Promise.all([
discussion.getListVnodes(
domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN),
),
discussion.getNodes(domainId),
]);
this.response.template = 'discussion_main_or_node.html';
this.response.body = {
ddocs, dpcount, udict, page, page_name: 'discussion_main', vndict, vnode: {}, path, vnodes,
};
}
}
class DiscussionNodeHandler extends DiscussionHandler {
@param('type', Types.String)
@param('name', Types.String)
@param('page', Types.UnsignedInt, true)
async get(domainId: string, type: string, _name: string, page = 1) {
let name: ObjectID | string | number;
if (ObjectID.isValid(_name)) name = new ObjectID(_name);
else if (isSafeInteger(parseInt(_name, 10))) name = parseInt(_name, 10);
else name = _name;
const [ddocs, dpcount] = await paginate(
discussion.getMulti(domainId, { parentType: typeMapper[type], parentId: name }),
page,
CONSTANT.DISCUSSION_PER_PAGE,
);
const uids = ddocs.map((ddoc) => ddoc.owner);
uids.push(this.vnode.owner);
const [udict, vnodes] = await Promise.all([
user.getList(domainId, uids),
discussion.getNodes(domainId),
]);
const path = [
['Hydro', 'homepage'],
['discussion_main', 'discussion_main'],
[this.vnode.title, null, true],
];
const vndict = { [typeMapper[type]]: { [name.toString()]: this.vnode } };
this.response.template = 'discussion_main_or_node.html';
this.response.body = {
ddocs,
dpcount,
udict,
path,
page,
vndict,
vnode: this.vnode,
page_name: 'discussion_node',
vnodes,
};
}
}
class DiscussionCreateHandler extends DiscussionHandler {
async get({ type, name }) {
const path = [
['Hydro', 'homepage'],
['discussion_main', 'discussion_main'],
[this.vnode.title, 'discussion_node', { type, name }, true],
['discussion_create', null],
];
this.response.template = 'discussion_create.html';
this.response.body = { path, vnode: this.vnode };
}
@param('type', Types.String)
@param('name', Types.String)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('highlight', Types.Boolean)
@param('pin', Types.Boolean)
async post(
domainId: string, type: string, _name: string,
title: string, content: string, highlight = false, pin = false,
) {
this.limitRate('add_discussion', 3600, 30);
let name: ObjectID | string | number;
if (ObjectID.isValid(_name)) name = new ObjectID(_name);
else if (isSafeInteger(parseInt(_name, 10))) name = parseInt(_name, 10);
else name = _name;
if (highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION);
if (pin) this.checkPerm(PERM.PERM_PIN_DISCUSSION);
const did = await discussion.add(
domainId, typeMapper[type], name, this.user._id,
title, content, this.request.ip, highlight, pin,
);
this.response.body = { did };
this.response.redirect = this.url('discussion_detail', { did });
}
}
class DiscussionDetailHandler extends DiscussionHandler {
@param('did', Types.ObjectID)
@param('page', Types.UnsignedInt, true)
async get(domainId: string, did: ObjectID, page = 1) {
const dsdoc = this.user.hasPriv(PRIV.PRIV_USER_PROFILE)
? await discussion.getStatus(domainId, did, this.user._id)
: null;
const [drdocs, pcount, drcount] = await paginate(
discussion.getMultiReply(domainId, did),
page,
CONSTANT.REPLY_PER_PAGE,
);
const uids = drdocs.map((drdoc) => drdoc.owner);
uids.push(this.ddoc.owner);
for (const drdoc of drdocs) {
if (drdoc.reply) {
drdocs.forEach((drrdoc) => uids.push(drrdoc.owner));
}
}
const udict = await user.getList(domainId, uids);
const path = [
['Hydro', 'homepage'],
['discussion_main', 'discussion_main'],
[this.vnode.title, 'discussion_node', { type: discussion.typeDisplay[this.ddoc.parentType], name: this.ddoc.parentId }, true],
[this.ddoc.title, null, null, true],
];
this.response.template = 'discussion_detail.html';
this.response.body = {
path, ddoc: this.ddoc, dsdoc, drdocs, page, pcount, drcount, udict, vnode: this.vnode,
};
}
async post() {
this.checkPriv(PRIV.PRIV_USER_PROFILE);
}
@param('did', Types.ObjectID)
@param('content', Types.String, isContent)
async postReply(domainId: string, did: ObjectID, content: string) {
this.checkPerm(PERM.PERM_REPLY_DISCUSSION);
this.limitRate('add_discussion', 3600, 30);
// Notify related users
const replies: Drdoc[] = await discussion.getMultiReply(domainId, did).toArray();
const uids = Array.from(new Set(replies.map((drdoc) => drdoc.owner)));
const tasks = uids.map((uid) => {
if (uid !== this.user._id) {
return message.send(1, uid, `Discussion update: ${domainId} ${did}`);
}
return Promise.resolve(null as Mdoc);
});
await Promise.all(tasks);
await discussion.addReply(domainId, did, this.user._id, content, this.request.ip);
this.back();
}
@param('drid', Types.ObjectID)
@param('content', Types.String, isContent)
async postTailReply(domainId: string, drid: ObjectID, content: string) {
this.checkPerm(PERM.PERM_REPLY_DISCUSSION);
this.limitRate('add_discussion', 3600, 30);
await discussion.addTailReply(domainId, drid, this.user._id, content, this.request.ip);
this.back();
}
@param('drid', Types.ObjectID)
@param('content', Types.String, isContent)
async postEditReply(domainId: string, drid: ObjectID, content: string) {
if (!(this.ddoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION))) {
if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY);
else this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
}
await discussion.editReply(domainId, drid, content);
this.back();
}
@param('drid', Types.ObjectID)
async postDeleteReply(domainId: string, drid: ObjectID) {
if (!(this.ddoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION))) {
if (this.drdoc.owner !== this.user._id) {
this.checkPerm(PERM.PERM_DELETE_DISCUSSION_REPLY);
} else this.checkPerm(PERM.PERM_DELETE_DISCUSSION_SELF);
}
await discussion.delReply(domainId, drid);
this.back();
}
@param('drid', Types.ObjectID)
@param('drrid', Types.ObjectID)
@param('content', Types.String, isContent)
async postEditTailReply(domainId: string, drid: ObjectID, drrid: ObjectID, content: string) {
if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY);
await discussion.editTailReply(domainId, drid, drrid, content);
this.back();
}
@param('drid', Types.ObjectID)
@param('drrid', Types.ObjectID)
async postDeleteTailReply(domainId: string, drid: ObjectID, drrid: ObjectID) {
if (this.drrdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_DISCUSSION_REPLY);
await discussion.delTailReply(domainId, drid, drrid);
this.back();
}
@param('did', Types.ObjectID)
async postStar(domainId: string, did: ObjectID) {
await discussion.setStar(domainId, did, this.user._id, true);
this.back({ star: true });
}
@param('did', Types.ObjectID)
async postUnstar(domainId: string, did: ObjectID) {
await discussion.setStar(domainId, did, this.user._id, false);
this.back({ star: false });
}
}
class DiscussionDetailRawHandler extends DiscussionHandler {
async get() {
this.response.type = 'text/markdown';
this.response.body = this.ddoc.content;
}
}
class DiscussionReplyRawHandler extends DiscussionHandler {
async get() {
this.response.type = 'text/markdown';
this.response.body = this.drdoc.content;
}
}
class DiscussionTailReplyRawHandler extends DiscussionHandler {
async get() {
this.response.type = 'text/markdown';
this.response.body = this.drrdoc.content;
}
}
class DiscussionEditHandler extends DiscussionHandler {
async get() {
const path = [
['Hydro', 'homepage'],
['discussion_main', 'discussion_main'],
[this.vnode.title, 'discussion_node', { type: discussion.typeDisplay[this.ddoc.parentType], name: this.ddoc.parentId }, true],
[this.ddoc.title, 'discussion_detail', { did: this.ddoc.docId }, true],
['discussion_edit', null],
];
this.response.template = 'discussion_edit.html';
this.response.body = { ddoc: this.ddoc, path };
}
@param('did', Types.ObjectID)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('highlight', Types.Boolean)
@param('pin', Types.Boolean)
async postUpdate(
domainId: string, did: ObjectID, title: string, content: string,
highlight = false, pin = false,
) {
if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION);
else this.checkPerm(PERM.PERM_EDIT_DISCUSSION_SELF);
if (highlight !== this.ddoc.highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION);
if (pin !== this.ddoc.pin) this.checkPerm(PERM.PERM_PIN_DISCUSSION);
await discussion.edit(domainId, did, title, content, highlight, pin);
this.response.body = { did };
this.response.redirect = this.url('discussion_detail', { did });
}
@param('did', Types.ObjectID)
async postDelete(domainId: string, did: ObjectID) {
if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_DISCUSSION);
else this.checkPerm(PERM.PERM_DELETE_DISCUSSION_SELF);
await discussion.del(domainId, did);
this.response.body = { type: this.ddoc.parentType, parent: this.ddoc.parentId };
this.response.redirect = this.url('discussion_node', {
type: discussion.typeDisplay[this.ddoc.parentType],
name: this.ddoc.parentId,
});
}
}
export async function apply() {
Route('discussion_main', '/discuss', DiscussionMainHandler);
Route('discussion_detail', '/discuss/:did', DiscussionDetailHandler);
Route('discussion_edit', '/discuss/:did/edit', DiscussionEditHandler);
Route('discussion_detail', '/discuss/:did/raw', DiscussionDetailRawHandler);
Route('discussion_reply_raw', '/discuss/:did/:drid/raw', DiscussionReplyRawHandler);
Route('discussion_tail_reply_raw', '/discuss/:did/:drid/:drrid/raw', DiscussionTailReplyRawHandler);
Route('discussion_node', '/discuss/:type/:name', DiscussionNodeHandler);
Route('discussion_create', '/discuss/:type/:name/create', DiscussionCreateHandler, PRIV.PRIV_USER_PROFILE, PERM.PERM_CREATE_DISCUSSION);
}
global.Hydro.handler.discussion = apply;
+208
View File
@@ -0,0 +1,208 @@
import { RoleAlreadyExistError, ValidationError } from '../error';
import * as user from '../model/user';
import * as domain from '../model/domain';
import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting';
import { PERM, PERMS_BY_FAMILY, PRIV } from '../model/builtin';
import { gravatar } from '../lib/misc';
import paginate from '../lib/paginate';
import {
Route, Handler, Types, param,
} from '../service/server';
class DomainRankHandler extends Handler {
@param('page', Types.UnsignedInt, true)
async get(domainId: string, page = 1) {
const [dudocs, upcount, ucount] = await paginate(
domain.getMultiInDomain(domainId).sort({ rp: -1 }),
page,
100,
);
let udocs = [];
for (const dudoc of dudocs) {
udocs.push(user.getById(domainId, dudoc.uid));
}
udocs = await Promise.all(udocs);
const path = [
['Hydro', 'homepage'],
['ranking', null],
];
this.response.template = 'ranking.html';
this.response.body = {
udocs, upcount, ucount, page, path,
};
}
}
class ManageHandler extends Handler {
domain: any;
async prepare({ domainId }) {
this.checkPerm(PERM.PERM_EDIT_DOMAIN);
this.domain = await domain.get(domainId);
}
}
class DomainEditHandler extends ManageHandler {
async get() {
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_edit', null],
];
this.response.template = 'domain_edit.html';
this.response.body = { current: this.domain, settings: DOMAIN_SETTINGS, path };
}
async post(args) {
const $set = {};
for (const key in args) {
if (DOMAIN_SETTINGS_BY_KEY[key]) $set[key] = args[key];
}
await domain.edit(args.domainId, $set);
this.response.redirect = this.url('domain_dashboard');
}
}
class DomainDashboardHandler extends ManageHandler {
async get() {
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_dashboard', null],
];
this.response.template = 'domain_dashboard.html';
this.response.body = { domain: this.domain, path };
}
}
class DomainUserHandler extends ManageHandler {
async get({ domainId }) {
const rudocs = {};
const [dudocs, roles] = await Promise.all([
domain.getMultiInDomain(domainId, {
$and: [
{ role: { $ne: 'default' } },
{ role: { $ne: null } },
],
}).toArray(),
domain.getRoles(domainId),
]);
const uids = dudocs.map((dudoc) => dudoc.uid);
const udict = await user.getList(domainId, uids);
for (const role of roles) rudocs[role._id] = [];
for (const dudoc of dudocs) {
const ud = udict[dudoc.uid];
rudocs[ud.role].push(ud);
}
const rolesSelect = roles.map((role) => [role._id, role._id]);
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_user', null],
];
this.response.template = 'domain_user.html';
this.response.body = {
roles, rolesSelect, rudocs, udict, path, domain: this.domain,
};
}
@param('uid', Types.Int)
@param('role', Types.String)
async postSetUser(domainId: string, uid: number, role: string) {
await domain.setUserRole(domainId, uid, role);
this.back();
}
}
class DomainPermissionHandler extends ManageHandler {
async get({ domainId }) {
const roles = await domain.getRoles(domainId);
const log2 = (val: bigint) => {
// @ts-ignore
for (let i = 0n; ; i++) {
// @ts-ignore
if ((val >> i) === 0n) return parseInt(i.toString(), 10) - 1;
}
};
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_permission', null],
];
this.response.template = 'domain_permission.html';
this.response.body = {
roles, PERMS_BY_FAMILY, domain: this.domain, path, log2,
};
}
async post({ domainId }) {
const roles = {};
for (const role in this.request.body) {
if (this.request.body[role] instanceof Array) {
const perms = this.request.body[role];
// @ts-ignore
roles[role] = 0n;
// @ts-ignore
for (const r of perms) roles[role] += 1n << BigInt(r);
}
}
await domain.setRoles(domainId, roles);
this.back();
}
}
class DomainRoleHandler extends ManageHandler {
async get({ domainId }) {
const roles = await domain.getRoles(domainId);
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_role', null],
];
this.response.template = 'domain_role.html';
this.response.body = { roles, domain: this.domain, path };
}
@param('role', Types.String)
async postAdd(domainId: string, role: string) {
const roles = await domain.getRoles(this.domain);
const rdict: any = {};
for (const r of roles) rdict[r._id] = r.perm;
if (rdict[role]) throw new RoleAlreadyExistError(role);
await domain.addRole(domainId, role, rdict.default.perm);
this.back();
}
async postDelete({ domainId, roles }) {
for (const role of roles) {
if (['root', 'default', 'guest'].includes(role)) {
throw new ValidationError('role');
}
}
await domain.deleteRoles(domainId, roles);
this.back();
}
}
class DomainSearchHandler extends Handler {
@param('q', Types.String)
async get(domainId: string, q: string) {
const ddocs = await domain.getPrefixSearch(q, 20);
for (let i = 0; i < ddocs.length; i++) {
ddocs[i].gravatar = ddocs[i].gravatar ? gravatar(ddocs[i].gravatar) : '/img/team_avatar.png';
}
this.response.body = ddocs;
}
}
export async function apply() {
Route('ranking', '/ranking', DomainRankHandler, PERM.PERM_VIEW_RANKING);
Route('domain_dashboard', '/domain/dashboard', DomainDashboardHandler);
Route('domain_edit', '/domain/edit', DomainEditHandler);
Route('domain_user', '/domain/user', DomainUserHandler);
Route('domain_permission', '/domain/permission', DomainPermissionHandler);
Route('domain_role', '/domain/role', DomainRoleHandler);
Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE);
}
global.Hydro.handler.domain = apply;
+410
View File
@@ -0,0 +1,410 @@
import { ObjectID } from 'mongodb';
import {
VerifyPasswordError, UserAlreadyExistError, InvalidTokenError,
NotFoundError, UserNotFoundError, PermissionError,
} from '../error';
import * as bus from '../service/bus';
import {
Route, Connection, Handler, ConnectionHandler, param, Types,
} from '../service/server';
import * as misc from '../lib/misc';
import md5 from '../lib/md5';
import * as mail from '../lib/mail';
import * as contest from '../model/contest';
import * as message from '../model/message';
import * as document from '../model/document';
import * as system from '../model/system';
import * as user from '../model/user';
import * as file from '../model/file';
import * as setting from '../model/setting';
import * as domain from '../model/domain';
import * as discussion from '../model/discussion';
import * as token from '../model/token';
import * as training from '../model/training';
import { PERM, PRIV, CONSTANT } from '../model/builtin';
import {
isContent, isPassword, isEmail, isTitle,
} from '../lib/validator';
const { geoip, useragent } = global.Hydro.lib;
class HomeHandler extends Handler {
async homework(domainId: string) {
if (this.user.hasPerm(PERM.PERM_VIEW_HOMEWORK)) {
const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK)
.sort('beginAt', -1)
.limit(CONSTANT.HOMEWORK_ON_MAIN)
.toArray();
const tsdict = await contest.getListStatus(
domainId, this.user._id,
tdocs.map((tdoc) => tdoc.docId), document.TYPE_HOMEWORK,
);
return [tdocs, tsdict];
}
return [[], {}];
}
async contest(domainId: string) {
if (this.user.hasPerm(PERM.PERM_VIEW_CONTEST)) {
const tdocs = await contest.getMulti(domainId)
.sort('beginAt', -1)
.limit(CONSTANT.CONTEST_ON_MAIN)
.toArray();
const tsdict = await contest.getListStatus(
domainId, this.user._id, tdocs.map((tdoc) => tdoc.docId),
);
return [tdocs, tsdict];
}
return [[], {}];
}
async training(domainId: string) {
if (this.user.hasPerm(PERM.PERM_VIEW_TRAINING)) {
const tdocs = await training.getMulti(domainId)
.sort('_id', 1)
.limit(CONSTANT.TRAINING_ON_MAIN)
.toArray();
const tsdict = await training.getListStatus(
domainId, this.user._id, tdocs.map((tdoc) => tdoc.docId),
);
return [tdocs, tsdict];
}
return [[], {}];
}
async discussion(domainId: string): Promise<[any[], any]> {
if (this.user.hasPerm(PERM.PERM_VIEW_DISCUSSION)) {
const ddocs = await discussion.getMulti(domainId)
.limit(CONSTANT.DISCUSSION_ON_MAIN)
.toArray();
const vndict = await discussion.getListVnodes(
domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN),
);
return [ddocs, vndict];
}
return [[], {}];
}
async get({ domainId }) {
const [
[htdocs, htsdict], [tdocs, tsdict],
[trdocs, trsdict], [ddocs, vndict],
] = await Promise.all([
this.homework(domainId), this.contest(domainId),
this.training(domainId), this.discussion(domainId),
]);
const [udict, dodoc, vnodes] = await Promise.all([
user.getList(domainId, ddocs.map((ddoc) => ddoc.owner)),
domain.get(domainId),
discussion.getNodes(domainId),
]);
this.response.template = 'main.html';
this.response.body = {
htdocs,
htsdict,
tdocs,
tsdict,
trdocs,
trsdict,
ddocs,
vndict,
udict,
domain: dodoc,
vnodes,
};
}
}
class HomeSecurityHandler extends Handler {
async get() {
// TODO(iceboy): pagination? or limit session count for uid?
const sessions = await token.getSessionListByUid(this.user._id);
for (const session of sessions) {
session.isCurrent = session._id === this.session._id;
session._id = md5(session._id);
if (useragent) session.updateUa = useragent.parse(session.updateUa || session.createUa || '');
if (geoip) {
session.updateGeoip = geoip.lookup(
session.updateIp || session.createIp,
this.translate('geoip_locale'),
);
}
}
const path = [
['Hydro', 'homepage'],
['home_security', null],
];
this.response.template = 'home_security.html';
this.response.body = { sessions, geoipProvider: geoip?.provider, path };
if (useragent) this.response.body.icon = useragent.icon;
}
@param('current', Types.String)
@param('password', Types.String, isPassword)
@param('verifyPassword', Types.String)
async postChangePassword(_: string, current: string, password: string, verify: string) {
if (password !== verify) throw new VerifyPasswordError();
this.user.checkPassword(current);
await user.setPassword(this.user._id, password);
await token.delByUid(this.user._id);
this.response.redirect = this.url('user_login');
}
@param('currentPassword', Types.String)
@param('mail', Types.String, isEmail)
async postChangeMail(domainId: string, current: string, email: string) {
this.limitRate('send_mail', 3600, 30);
this.user.checkPassword(current);
const udoc = await user.getByEmail(domainId, email);
if (udoc) throw new UserAlreadyExistError(email);
const [code] = await token.add(
token.TYPE_CHANGEMAIL,
await system.get('changemail_token_expire_seconds'),
{ uid: this.user._id, email },
);
const m = await this.renderHTML('user_changemail_mail.html', {
path: `home/changeMail/${code}`,
uname: this.user.uname,
url_prefix: await system.get('server.url'),
});
await mail.sendMail(email, 'Change Email', 'user_changemail_mail', m);
this.response.template = 'user_changemail_mail_sent.html';
}
@param('tokenDigest', Types.String)
async postDeleteToken(domainId: string, tokenDigest: string) {
const sessions = await token.getSessionListByUid(this.user._id);
for (const session of sessions) {
if (tokenDigest === md5(session._id)) {
// eslint-disable-next-line no-await-in-loop
await token.del(session._id, token.TYPE_SESSION);
return this.back();
}
}
throw new InvalidTokenError(tokenDigest);
}
async postDeleteAllTokens() {
await token.delByUid(this.user._id);
this.response.redirect = this.url('user_login');
}
}
class HomeSettingsHandler extends Handler {
@param('category', Types.String)
async get(domainId: string, category: string) {
const path = [
['Hydro', 'homepage'],
[`home_${category}`, null],
];
this.response.template = 'home_settings.html';
this.response.body = {
category,
page_name: `home_${category}`,
current: this.user,
path,
};
if (category === 'preference') {
this.response.body.settings = setting.PREFERENCE_SETTINGS;
} else if (category === 'account') {
this.response.body.settings = setting.ACCOUNT_SETTINGS;
} else if (category === 'domain') {
this.response.body.settings = setting.DOMAIN_USER_SETTINGS;
} else throw new NotFoundError();
}
async post(args: any) {
const $set = {};
if (args.category === 'domain') {
for (const key in args) {
if (setting.DOMAIN_USER_SETTINGS_BY_KEY[key]
&& !(setting.DOMAIN_USER_SETTINGS_BY_KEY[key].flag & setting.FLAG_DISABLED)) {
$set[key] = args[key];
}
}
await domain.setUserInDomain(args.domainId, this.user._id, $set);
} else {
for (const key in args) {
if (setting.SETTINGS_BY_KEY[key]
&& !(setting.SETTINGS_BY_KEY[key].flag & setting.FLAG_DISABLED)) {
$set[key] = args[key];
}
}
await user.setById(this.user._id, $set);
}
this.back();
}
}
class UserChangemailWithCodeHandler extends Handler {
@param('code', Types.String)
async get(domainId: string, code: string) {
const tdoc = await token.get(code, token.TYPE_CHANGEMAIL);
if (!tdoc || tdoc.uid !== this.user._id) {
throw new InvalidTokenError(code);
}
const udoc = await user.getByEmail(domainId, tdoc.email);
if (udoc) throw new UserAlreadyExistError(tdoc.email);
await Promise.all([
user.setEmail(this.user._id, tdoc.email),
token.del(code, token.TYPE_CHANGEMAIL),
]);
this.response.redirect = this.url('home_security');
}
}
class HomeDomainHandler extends Handler {
async get() {
const path = [
['Hydro', 'homepage'],
['home_domain', null],
];
const dudict = await domain.getDictUserByDomainId(this.user._id);
const dids = Object.keys(dudict);
const ddocs = await domain.getMulti({ _id: { $in: dids } }).toArray();
const canManage = {};
for (const ddoc of ddocs) {
// eslint-disable-next-line no-await-in-loop
const udoc = await user.getById(ddoc._id, this.user._id);
canManage[ddoc._id] = udoc.hasPerm(PERM.PERM_EDIT_DOMAIN)
|| udoc.hasPriv(PRIV.PRIV_MANAGE_ALL_DOMAIN);
}
this.response.template = 'home_domain.html';
this.response.body = {
ddocs, dudict, canManage, path,
};
}
}
class HomeDomainCreateHandler extends Handler {
async get() {
this.response.body = {
path: [
['Hydro', 'homepage'],
['domain_create', null],
],
};
this.response.template = 'domain_create.html';
}
@param('id', Types.String)
@param('name', Types.String, isTitle)
@param('bulletin', Types.String, isContent)
@param('gravatar', Types.String, true, isEmail)
async post(_: string, id: string, name: string, bulletin: string, gravatar: string) {
gravatar = gravatar || this.user.gravatar || this.user.mail || 'guest@hydro.local';
const domainId = await domain.add(id, this.user._id, name, bulletin);
await domain.edit(domainId, { gravatar });
this.response.redirect = this.url('domain_manage', { domainId });
this.response.body = { domainId };
}
}
class HomeMessagesHandler extends Handler {
async get() {
// TODO(iceboy): projection, pagination.
const messages = await message.getByUser(this.user._id);
const udict = await user.getList('system', [
...messages.map((mdoc) => mdoc.from),
...messages.map((mdoc) => mdoc.to),
]);
// TODO(twd2): improve here:
const parsed = {};
for (const m of messages) {
const target = m.from === this.user._id ? m.to : m.from;
if (!parsed[target]) {
parsed[target] = {
_id: target,
udoc: { ...udict[target], gravatar: misc.gravatar(udict[target].gravatar) },
messages: [],
};
}
parsed[target].messages.push(m);
}
const path = [
['Hydro', 'homepage'],
['home_messages', null],
];
this.response.body = { messages: parsed, path };
this.response.template = 'home_messages.html';
}
@param('uid', Types.Int)
@param('content', Types.String, isContent)
async postSend(domainId: string, uid: number, content: string) {
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, message.FLAG_UNREAD);
this.back({ mdoc, udoc });
}
@param('messageId', Types.ObjectID)
async postDeleteMessage(domainId: string, messageId: ObjectID) {
const msg = await message.get(messageId);
if ([msg.from, msg.to].includes(this.user._id)) await message.del(messageId);
else throw new PermissionError();
this.back();
}
@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();
this.back();
}
}
class HomeMessagesConnectionHandler extends ConnectionHandler {
id: string;
async prepare() {
bus.subscribe([`user_message-${this.user._id}`], this.onMessageReceived.bind(this));
}
async onMessageReceived(e: any) {
this.send(e.value);
}
async cleanup() {
bus.unsubscribe([`user_message-${this.user._id}`], this.id);
}
}
class HomeFileHandler extends Handler {
async get() {
const path = [
['Hydro', 'homepage'],
['domain_file', null],
];
const ufdocs = await file.getMulti({ owner: this.user._id }).toArray();
this.response.template = 'home_file.html';
this.response.body = { ufdocs, path };
}
@param('ufid', Types.ObjectID)
async postDelete(domainId: string, ufid: ObjectID) {
const ufdoc = await file.getMeta(ufid);
if (ufdoc.owner !== this.user._id) this.checkPriv(PRIV.PRIV_DELETE_FILE);
else this.checkPriv(PRIV.PRIV_DELETE_FILE_SELF);
const result = await file.del(ufdoc._id);
if (result) await user.inc(this.user._id, 'usage', -ufdoc.size);
this.back();
}
}
async function apply() {
Route('homepage', '/', HomeHandler);
Route('home_security', '/home/security', HomeSecurityHandler, PRIV.PRIV_USER_PROFILE);
Route('user_changemail_with_code', '/home/changeMail/:code', UserChangemailWithCodeHandler, PRIV.PRIV_USER_PROFILE);
Route('home_settings', '/home/settings/:category', HomeSettingsHandler, PRIV.PRIV_USER_PROFILE);
Route('home_domain', '/home/domain', HomeDomainHandler, PRIV.PRIV_USER_PROFILE);
Route('home_domain_create', '/home/domain/create', HomeDomainCreateHandler, PRIV.PRIV_CREATE_DOMAIN);
Route('home_messages', '/home/messages', HomeMessagesHandler, PRIV.PRIV_USER_PROFILE);
Route('home_file', '/home/file', HomeFileHandler, PRIV.PRIV_USER_PROFILE);
Connection('home_messages_conn', '/home/messages-conn', HomeMessagesConnectionHandler, PRIV.PRIV_USER_PROFILE);
}
global.Hydro.handler.home = module.exports = apply;
+416
View File
@@ -0,0 +1,416 @@
import yaml from 'js-yaml';
import moment from 'moment-timezone';
import { ObjectID } from 'mongodb';
import AdmZip from 'adm-zip';
import { isSafeInteger } from 'lodash';
import {
ValidationError, HomeworkNotLiveError, ProblemNotFoundError,
HomeworkNotAttendedError,
} from '../error';
import { PenaltyRules } from '../interface';
import {
Route, Handler, Types, param,
} from '../service/server';
import * as bus from '../service/bus';
import { PERM, CONSTANT } from '../model/builtin';
import * as user from '../model/user';
import * as contest from '../model/contest';
import * as discussion from '../model/discussion';
import * as problem from '../model/problem';
import * as record from '../model/record';
import * as document from '../model/document';
import paginate from '../lib/paginate';
import { isTitle, isContent } from '../lib/validator';
const validatePenaltyRules = (input: string) => {
const s = yaml.safeLoad(input);
return s;
};
const convertPenaltyRules = validatePenaltyRules;
const HomeworkHandler = contest.ContestHandlerMixin(Handler);
class HomeworkMainHandler extends HomeworkHandler {
async get({ domainId }) {
const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK).toArray();
const calendar = [];
for (const tdoc of tdocs) {
const cal = { ...tdoc, url: this.url('homework_detail', { tid: tdoc.docId }) };
if (contest.isExtended(tdoc) || contest.isDone(tdoc)) {
cal.endAt = tdoc.endAt;
cal.penaltySince = tdoc.penaltySince;
} else {
cal.endAt = tdoc.penaltySince;
}
calendar.push(cal);
}
const path = [
['Hydro', 'homepage'],
['homework_main', null],
];
this.response.body = { tdocs, calendar, path };
this.response.template = 'homework_main.html';
}
}
class HomeworkDetailHandler extends HomeworkHandler {
@param('tid', Types.ObjectID)
@param('page', Types.UnsignedInt, true)
async get(domainId: string, tid: ObjectID, page = 1) {
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
const [tsdoc, pdict] = await Promise.all([
contest.getStatus(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK),
problem.getList(domainId, tdoc.pids, true),
]);
const psdict = {};
let rdict = {};
let attended = false;
if (tsdoc) {
attended = tsdoc.attend === 1;
for (const pdetail of tsdoc.journal || []) {
psdict[pdetail.pid] = pdetail;
rdict[pdetail.rid] = { _id: pdetail.rid };
}
if (this.canShowRecord(tdoc) && tsdoc.journal) {
rdict = await record.getList(
domainId,
tsdoc.journal.map((pdetail) => pdetail.rid),
true,
);
}
}
// discussion
const [ddocs, dpcount, dcount] = await paginate(
discussion.getMulti(domainId, { parentType: tdoc.docType, parentId: tdoc.docId }),
page,
CONSTANT.DISCUSSION_PER_PAGE,
);
const uids = ddocs.map((ddoc) => ddoc.owner);
uids.push(tdoc.owner);
const udict = await user.getList(domainId, uids);
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
[tdoc.title, null, null, true],
];
this.response.template = 'homework_detail.html';
this.response.body = {
tdoc, tsdoc, attended, udict, pdict, psdict, rdict, ddocs, page, dpcount, dcount, path,
};
}
@param('tid', Types.ObjectID)
async postAttend(domainId: string, tid: ObjectID) {
this.checkPerm(PERM.PERM_ATTEND_HOMEWORK);
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
if (contest.isDone(tdoc)) throw new HomeworkNotLiveError(tdoc.docId);
await contest.attend(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK);
this.back();
}
}
class HomeworkDetailProblemHandler extends HomeworkHandler {
@param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt)
async prepare(domainId: string, tid: ObjectID, pid: number) {
this.checkPerm(PERM.PERM_VIEW_PROBLEM);
[this.tdoc, this.pdoc, this.tsdoc] = await Promise.all([
contest.get(domainId, tid, document.TYPE_HOMEWORK),
problem.get(domainId, pid, this.user._id),
contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK),
]);
}
@param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt)
async get(domainId: string, tid: ObjectID, pid: number) {
const udoc = await user.getById(domainId, this.tdoc.owner);
const attended = this.tsdoc && this.tsdoc.attend === 1;
if (!contest.isDone(this.tdoc)) {
if (!attended) throw new HomeworkNotAttendedError(tid);
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
}
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid);
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
[this.tdoc.title, 'homework_detail', { tid }, true],
[this.pdoc.title, null, null, true],
];
this.response.template = 'problem_detail.html';
this.response.body = {
tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc, udoc, attended, path,
};
}
}
class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
@param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt)
async get(domainId: string, tid: ObjectID, pid: number) {
const udoc = await user.getById(domainId, this.tdoc.owner);
const attended = this.tsdoc && this.tsdoc.attend === 1;
if (!attended) throw new HomeworkNotAttendedError(tid);
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid);
const rdocs = this.canShowRecord(this.tdoc)
? await record.getUserInProblemMulti(domainId, this.user._id, pid, true)
.sort('_id', -1).limit(10).toArray()
: [];
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
[this.tdoc.title, 'homework_detail', { tid }, true],
[this.pdoc.title, 'homework_detail_problem', { tid, pid }, true],
['homework_detail_problem_submit', null],
];
this.response.template = 'problem_submit.html';
this.response.body = {
tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc, udoc, attended, path, rdocs,
};
}
@param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt)
@param('code', Types.String)
@param('lang', Types.String)
async post(domainId: string, tid: ObjectID, pid: number, code: string, lang: string) {
this.limitRate('add_record', 3600, 100);
const tsdoc = await contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK);
if (!tsdoc.attend) throw new HomeworkNotAttendedError(tid);
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid);
const rid = await record.add(domainId, pid, this.user._id, lang, code, true, {
type: document.TYPE_HOMEWORK,
tid,
});
const [rdoc] = await Promise.all([
record.get(domainId, rid),
contest.updateStatus(domainId, tid, this.user._id,
rid, pid, false, 0, document.TYPE_HOMEWORK),
]);
bus.publish('record_change', rdoc);
this.response.body = { tid, rid };
if (this.canShowRecord(this.tdoc)) this.response.redirect = this.url('record_detail', { rid });
else this.response.redirect = this.url('homework_detail', { tid });
}
}
class HomeworkCreateHandler extends HomeworkHandler {
async get() {
const beginAt = moment().add(1, 'day');
console.log(beginAt, beginAt.tz(this.user.timeZone));
const penaltySince = beginAt.clone().add(7, 'days');
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
['homework_create', null],
];
this.response.template = 'homework_edit.html';
this.response.body = {
path,
dateBeginText: beginAt.tz(this.user.timeZone).format('YYYY-M-D'),
timeBeginText: '00:00',
datePenaltyText: penaltySince.tz(this.user.timeZone).format('YYYY-M-D'),
timePenaltyText: '23:59',
pids: '1000, 1001',
extensionDays: 1,
page_name: 'homework_create',
};
}
@param('beginAtDate', Types.Date)
@param('beginAtTime', Types.Time)
@param('penaltySinceDate', Types.Date)
@param('penaltySinceTime', Types.Time)
@param('extensionDays', Types.Float)
@param('penaltyRules', Types.String, validatePenaltyRules, convertPenaltyRules)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('pids', Types.String)
@param('rated', Types.Boolean)
async post(
domainId: string, beginAtDate: string, beginAtTime: string,
penaltySinceDate: string, penaltySinceTime: string, extensionDays: number,
penaltyRules: PenaltyRules, title: string, content: string, _pids: string, rated = false,
) {
const pids = _pids.split(',').map((i) => {
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
return i;
});
const beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
if (!beginAt.isValid()) throw new ValidationError('beginAtDate', 'beginAtTime');
const penaltySince = moment.tz(`${penaltySinceDate} ${penaltySinceTime}`, this.user.timeZone);
if (!penaltySince.isValid()) throw new ValidationError('endAtDate', 'endAtTime');
const endAt = penaltySince.clone().add(extensionDays, 'days');
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
if (penaltySince.isAfter(endAt)) throw new ValidationError('extensionDays');
await this.verifyProblems(domainId, pids);
const tid = await contest.add(domainId, title, content, this.user._id,
'homework', beginAt.toDate(), endAt.toDate(), pids, rated,
{ penaltySince: penaltySince.toDate(), penaltyRules }, document.TYPE_HOMEWORK);
this.response.body = { tid };
this.response.redirect = this.url('homework_detail', { tid });
}
}
class HomeworkEditHandler extends HomeworkHandler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
if (tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF);
const extensionDays = Math.round(
(tdoc.endAt.getTime() - tdoc.penaltySince.getTime()) / 36000 / 24,
) / 100;
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
[tdoc.title, 'homework_detail', { tid }, true],
['homework_edit', null],
];
const beginAt = moment(tdoc.beginAt).tz(this.user.timeZone);
const penaltySince = moment(tdoc.penaltySince).tz(this.user.timeZone);
this.response.template = 'homework_edit.html';
this.response.body = {
tdoc,
dateBeginText: beginAt.format('YYYY-M-D'),
timeBeginText: beginAt.format('hh:mm'),
datePenaltyText: penaltySince.format('YYYY-M-D'),
timePenaltyText: penaltySince.format('hh:mm'),
extensionDays,
penaltyRules: yaml.safeDump(tdoc.penaltyRules),
pids: tdoc.pids.join(','),
path,
};
}
@param('tid', Types.ObjectID)
@param('beginAtDate', Types.Date)
@param('beginAtTime', Types.Time)
@param('penaltySinceDate', Types.Date)
@param('penaltySinceTime', Types.Time)
@param('extensionDays', Types.Float)
@param('penaltyRules', Types.String, validatePenaltyRules, convertPenaltyRules)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('pids', Types.String)
@param('rated', Types.Boolean)
async post(
domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string,
penaltySinceDate: string, penaltySinceTime: string, extensionDays: number,
penaltyRules: string, title: string, content: string, _pids: string, rated = false,
) {
const pids = _pids.split(',').map((i) => {
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
return i;
});
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
if (tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF);
let beginAt;
let penaltySince;
try {
beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
} catch (e) {
throw new ValidationError('beginAtDate', 'beginAtTime');
}
try {
penaltySince = moment.tz(`${penaltySinceDate} ${penaltySinceTime}`, this.user.timeZone);
} catch (e) {
throw new ValidationError('endAtDate', 'endAtTime');
}
let endAt = penaltySince.clone().add(extensionDays, 'days');
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
await this.verifyProblems(domainId, pids);
beginAt = beginAt.toDate();
endAt = endAt.toDate();
penaltySince = penaltySince.toDate();
await contest.edit(domainId, tid, {
title, content, beginAt, endAt, pids, penaltySince, penaltyRules, rated,
}, document.TYPE_HOMEWORK);
if (tdoc.beginAt !== beginAt
|| tdoc.endAt !== endAt
|| tdoc.penaltySince !== penaltySince
|| new Set(tdoc.pids) !== new Set(pids)) {
await contest.recalcStatus(domainId, tdoc.docId, document.TYPE_HOMEWORK);
}
this.response.body = { tid };
this.response.redirect = this.url('homework_detail', { tid });
}
}
class HomeworkScoreboardHandler extends HomeworkHandler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
console.log(123);
const [tdoc, rows, udict] = await this.getScoreboard(
domainId, tid, false, document.TYPE_HOMEWORK,
);
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
[tdoc.title, 'homework_detail', { tid }, true],
['homework_scoreboard', null],
];
this.response.template = 'contest_scoreboard.html';
this.response.body = {
tdoc, rows, path, udict,
};
}
}
class HomeworkScoreboardDownloadHandler extends HomeworkHandler {
@param('tid', Types.ObjectID)
@param('ext', Types.String)
async get(domainId: string, tid: ObjectID, ext: string) {
const getContent = {
csv: (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`,
html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }),
};
if (!getContent[ext]) throw new ValidationError('ext');
const [, rows] = await this.getScoreboard(domainId, tid, true, document.TYPE_CONTEST);
this.binary(await getContent[ext](rows), `${this.tdoc.title}.${ext}`);
}
}
class HomeworkCodeHandler extends HomeworkHandler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
this.checkPerm(PERM.PERM_READ_RECORD_CODE);
this.limitRate('homework_code', 3600, 60);
const [tdoc, tsdocs] = await contest.getAndListStatus(
domainId, tid, document.TYPE_HOMEWORK,
);
const rnames = {};
for (const tsdoc of tsdocs) {
for (const pdetail of tsdoc.detail || []) {
rnames[pdetail.rid] = `U${tsdoc.uid}_P${pdetail.pid}_R${pdetail.rid}`;
}
}
const zip = new AdmZip();
const rdocs = await record.getMulti(domainId, {
_id: {
$in: Array.from(Object.keys(rnames)).map((id) => new ObjectID(id)),
},
}).toArray();
for (const rdoc of rdocs) {
zip.addFile(`${rnames[rdoc._id]}.${rdoc.lang}`, rdoc.code);
}
await this.binary(zip.toBuffer(), `${tdoc.title}.zip`);
}
}
export async function apply() {
Route('homework_main', '/homework', HomeworkMainHandler, PERM.PERM_VIEW_HOMEWORK);
Route('homework_create', '/homework/create', HomeworkCreateHandler, PERM.PERM_CREATE_HOMEWORK);
Route('homework_detail', '/homework/:tid', HomeworkDetailHandler, PERM.PERM_VIEW_HOMEWORK);
Route('homework_scoreboard', '/homework/:tid/scoreboard', HomeworkScoreboardHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD);
Route('homework_scoreboard', '/homework/:tid/scoreboard/download/:ext', HomeworkScoreboardDownloadHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD);
Route('homework_detail_problem', '/homework/:tid/p/:pid', HomeworkDetailProblemHandler, PERM.PERM_VIEW_HOMEWORK);
Route('homework_detail_problem_submit', '/homework/:tid/p/:pid/submit', HomeworkDetailProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
Route('homework_code', '/homework/:tid/code', HomeworkCodeHandler, PERM.PERM_VIEW_HOMEWORK);
Route('homework_edit', '/homework/:tid/edit', HomeworkEditHandler);
}
global.Hydro.handler.homework = apply;
+61
View File
@@ -0,0 +1,61 @@
import fs from 'fs';
import path from 'path';
import os from 'os';
import { ValidationError } from '../error';
import * as problem from '../model/problem';
import { PERM } from '../model/builtin';
import {
Route, Handler, Types, param,
} from '../service/server';
import { isPid } from '../lib/validator';
class ProblemImportHandler extends Handler {
async prepare() {
this.checkPerm(PERM.PERM_CREATE_PROBLEM);
}
async get() {
this.response.template = 'problem_import.html';
this.response.body = {
path: [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
['problem_import', null],
],
};
}
@param('url', Types.String)
@param('pid', Types.String, isPid)
@param('hidden', Types.Boolean)
@param('remoteType', Types.String)
async post(domainId: string, url: string, pid: string, hidden = false, remoteType: string) {
if (typeof global.Hydro.lib[`import.${remoteType}`] !== 'function') {
throw new ValidationError('remoteType');
}
const [pdoc, testdata] = await global.Hydro.lib[`import.${remoteType}`](url, this);
if (hidden) pdoc.hidden = true;
const docId = await problem.add(
domainId, pid || pdoc.pid, pdoc.title, pdoc.content, this.user._id,
pdoc.tag || [], pdoc.category || [], null, pdoc.hidden,
);
if (testdata) {
const file = path.resolve(os.tmpdir(), 'hydro', `import_${domainId}_${pid || pdoc.pid || docId}.zip`);
const w = fs.createWriteStream(file);
await new Promise((resolve, reject) => {
w.on('finish', resolve);
w.on('error', reject);
testdata.pipe(w);
});
await problem.setTestdata(domainId, docId, file);
}
this.response.body = { pid: pid || docId };
this.response.redirect = this.url('problem_settings', { pid: pid || docId });
}
}
export async function apply() {
Route('problem_import', '/problem/import', ProblemImportHandler);
}
global.Hydro.handler.import = apply;
+146
View File
@@ -0,0 +1,146 @@
import { ObjectID } from 'mongodb';
import { JudgeResultBody, Rdoc } from '../interface';
import * as record from '../model/record';
import * as problem from '../model/problem';
import * as builtin from '../model/builtin';
import * as contest from '../model/contest';
import * as domain from '../model/domain';
import * as task from '../model/task';
import * as bus from '../service/bus';
import {
Route, Handler, Connection, ConnectionHandler,
} from '../service/server';
async function _postJudge(rdoc: Rdoc) {
const accept = rdoc.status === builtin.STATUS.STATUS_ACCEPTED;
const tasks = [];
if (rdoc.contest) {
tasks.push(
contest.updateStatus(
rdoc.domainId, rdoc.contest.tid, rdoc.uid,
rdoc._id, rdoc.pid, accept, rdoc.score, rdoc.contest.type,
),
);
}
if (await problem.updateStatus(rdoc.domainId, rdoc.pid, rdoc.uid, rdoc._id, rdoc.status)) {
if (accept && !rdoc.rejudged) {
tasks.push(
problem.inc(rdoc.domainId, rdoc.pid, 'nAccept', 1),
domain.incUserInDomain(rdoc.domainId, rdoc.uid, 'nAccept', 1),
);
}
}
await Promise.all(tasks);
}
export async function next(body: JudgeResultBody) {
if (body.rid) body.rid = new ObjectID(body.rid);
let rdoc = await record.get(body.domainId, body.rid);
const $set: any = {};
const $push: any = {};
if (body.case) {
const c: any = {};
c.memory = body.case.memory;
c.time = body.case.time;
c.message = body.case.message;
c.status = body.case.status;
rdoc.testCases.push(c);
$push.testCases = c;
}
if (body.message) {
rdoc.judgeTexts.push(body.message);
$push.judgeTexts = body.message;
}
if (body.compilerText) {
rdoc.compilerTexts.push(body.compilerText);
$push.compilerTexts = body.compilerText;
}
if (body.status) $set.status = body.status;
if (body.score) $set.score = body.score;
if (body.time) $set.time = body.time;
if (body.memory) $set.memory = body.memory;
if (body.progress) $set.progress = body.progress;
rdoc = await record.update(body.domainId, body.rid, $set, $push);
bus.publish('record_change', { rdoc, $set, $push });
}
export async function end(body: JudgeResultBody) {
if (body.rid) body.rid = new ObjectID(body.rid);
let rdoc = await record.get(body.domainId, body.rid);
const $set: any = {};
const $push: any = {};
const $unset = { progress: '' };
if (body.message) {
rdoc.judgeTexts.push(body.message);
$push.judgeTexts = body.message;
}
if (body.compilerText) {
rdoc.compilerTexts.push(body.compilerText);
$push.compilerTexts = body.compilerText;
}
if (body.status) $set.status = body.status;
if (body.score) $set.score = body.score;
if (body.stdout) $set.stdout = body.stdout;
if (body.stderr) $set.stderr = body.stderr;
if (body.time) $set.time = body.time;
if (body.memory) $set.memory = body.memory;
$set.judgeAt = new Date();
$set.judger = body.judger;
rdoc = await record.update(body.domainId, body.rid, $set, $push, $unset);
await _postJudge(rdoc);
bus.publish('record_change', { rdoc }); // trigger a full update
}
class JudgeHandler extends Handler {
async get({ check = false }) {
if (check) return;
const tasks = [];
let t = await task.getFirst({ type: 'judge' });
while (t) {
tasks.push(t);
t = await task.getFirst({ type: 'judge' }); // eslint-disable-line no-await-in-loop
}
this.response.body = { tasks };
}
async postNext() {
await next(this.request.body);
}
async postEnd() {
this.request.body.judger = this.user._id;
await end(this.request.body);
}
}
class JudgeConnectionHandler extends ConnectionHandler {
processing: any;
async message(msg) {
if (msg.key === 'next') await next(msg);
else if (msg.key === 'end') {
await end({ judger: this.user._id, ...msg });
this.processing = null;
const t = await task.getFirst({ type: 'judge' });
this.send({ task: t });
this.processing = t;
}
}
async cleanup() {
if (this.processing) {
await record.reset(this.processing.domainId, this.processing.rid, false);
task.add(this.processing);
}
}
}
export async function apply() {
Route('judge', '/judge', JudgeHandler, builtin.PRIV.PRIV_JUDGE);
Connection('judge_conn', '/judge/conn', JudgeConnectionHandler, builtin.PRIV.PRIV_JUDGE);
}
apply.next = next;
apply.end = end;
global.Hydro.handler.judge = apply;
+170
View File
@@ -0,0 +1,170 @@
import * as judge from './judge';
import { ValidationError } from '../error';
import * as check from '../check';
import * as setting from '../model/setting';
import * as system from '../model/system';
import { STATUS, PRIV } from '../model/builtin';
import * as record from '../model/record';
import {
Route, Connection, Handler, ConnectionHandler, param, Types,
} from '../service/server';
import { validate } from '../lib/validator';
function set(key, value) {
if (setting.SYSTEM_SETTINGS_BY_KEY[key]) {
const s = setting.SYSTEM_SETTINGS_BY_KEY[key];
if (s.flag & setting.FLAG_DISABLED) return undefined;
if (s.flag & setting.FLAG_SECRET && !value) return undefined;
if (s.type === 'boolean') {
if (value === 'on') return true;
return false;
}
if (s.type === 'number') {
if (!Number.isSafeInteger(parseInt(value, 10))) {
throw new ValidationError(key);
}
return parseInt(value, 10);
}
return value;
}
return undefined;
}
class SystemHandler extends Handler {
async prepare() {
this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
this.response.body = {
path: [
['Hydro', 'homepage'],
],
};
}
}
class SystemMainHandler extends SystemHandler {
async get() {
this.response.redirect = '/manage/dashboard';
}
}
class SystemCheckConnHandler extends ConnectionHandler {
id: any;
async prepare() {
this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
await this.check();
}
async check() {
const log = (payload: any) => this.send({ type: 'log', payload });
const warn = (payload: any) => this.send({ type: 'warn', payload });
const error = (payload: any) => this.send({ type: 'error', payload });
await check.start(this, log, warn, error, (id) => { this.id = id; });
}
async cleanup() {
check.cancel(this.id);
}
}
class SystemDashboardHandler extends SystemHandler {
async get() {
this.response.template = 'manage_dashboard.html';
this.response.body.path.push(['manage_dashboard', null]);
}
async postRestart() {
const count = await system.get('server.worker');
process.send({ event: 'restart', count });
this.back();
}
}
class SystemScriptHandler extends SystemHandler {
async get() {
this.response.template = 'manage_script.html';
this.response.body.scripts = global.Hydro.script;
this.response.body.path.push(['manage_script', null]);
}
@param('id', Types.String)
@param('args', Types.String, true)
async post(domainId: string, id: string, raw = '{}') {
if (!global.Hydro.script[id]) throw new ValidationError('id');
const args = JSON.parse(raw);
validate(global.Hydro.script[id].validate, args);
const rid = await record.add(domainId, -1, this.user._id, '-', id, false, { input: 'raw' });
const report = (data) => judge.next({ domainId, rid, ...data });
report({ message: `Running script: ${id}`, status: STATUS.STATUS_JUDGING });
const start = new Date().getTime();
// Maybe async?
global.Hydro.script[id].run(args, report)
.then((ret: any) => {
const time = new Date().getTime() - start;
judge.end({
domainId,
rid,
status: STATUS.STATUS_ACCEPTED,
message: ret.toString(),
judger: 1,
time,
memory: 0,
});
})
.catch((err: Error) => {
const time = new Date().getTime() - start;
judge.end({
domainId,
rid,
status: STATUS.STATUS_SYSTEM_ERROR,
message: `${err}\n${err.stack}`,
judger: 1,
time,
memory: 0,
});
});
this.response.body = { rid };
this.response.redirect = this.url('record_detail', { rid });
}
}
class SystemSettingHandler extends SystemHandler {
async get() {
this.response.template = 'manage_setting.html';
this.response.body.path.push(['manage_setting', null]);
this.response.body.current = {};
this.response.body.settings = setting.SYSTEM_SETTINGS;
for (const s of this.response.body.settings) {
// FIXME no-await-in-loop
// eslint-disable-next-line no-await-in-loop
this.response.body.current[s.key] = await system.get(s.key);
}
}
async post(args: any) {
const tasks = [];
for (const key in args) {
if (typeof args[key] === 'object') {
for (const subkey in args[key]) {
if (typeof set(`${key}.${subkey}`, args[key][subkey]) !== 'undefined') {
tasks.push(system.set(`${key}.${subkey}`, set(`${key}.${subkey}`, args[key][subkey])));
}
}
} else if (typeof set(key, args[key]) !== 'undefined') {
tasks.push(system.set(key, set(key, args[key])));
}
}
await Promise.all(tasks);
this.back();
}
}
async function apply() {
Route('manage', '/manage', SystemMainHandler);
Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler);
Route('manage_script', '/manage/script', SystemScriptHandler);
Route('manage_setting', '/manage/setting', SystemSettingHandler);
Connection('manage_check', '/manage/check-conn', SystemCheckConnHandler);
}
global.Hydro.handler.manage = apply;
+114
View File
@@ -0,0 +1,114 @@
/* eslint-disable camelcase */
import fs from 'fs';
import { ObjectID } from 'mongodb';
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 db from '../service/db';
import {
Route, Handler, Types, param, multipart,
} from '../service/server';
import { BadRequestError } from '../error';
const coll = db.collection('status');
class FileDownloadHandler extends Handler {
@param('fileId', Types.ObjectID)
@param('secret', Types.String)
@param('name', Types.String, true)
async get(domainId: string, fileId: ObjectID, secret: string, name: string) {
if (name) name = Buffer.from(name, 'base64').toString();
this.response.attachment(name || fileId.toHexString());
this.response.body = await file.getWithSecret(fileId, secret);
}
}
class FileUploadHandler extends Handler {
async getQuota() {
let quota = await system.get('user.quota');
if (this.user.hasPriv(PRIV.PRIV_UNLIMITED_QUOTA)) {
quota = 2 ** 63 - 1;
}
return quota;
}
async get() {
this.response.template = 'fs_upload.html';
this.response.body = { fdoc: null, usage: this.user.usage, quota: await this.getQuota() };
}
@multipart(256 * 1024)
@param('title', Types.String)
async post(domainId: string, title: string) {
if (!this.request.files.file) throw new BadRequestError();
const quota = await this.getQuota();
const lfdoc = await fs.promises.stat(this.request.files.file.path);
let ufid: ObjectID;
const udoc = await user.inc(this.user._id, 'usage', lfdoc.size);
try {
ufid = await file.add(this.request.files.file.path, title, this.user._id);
} catch (e) {
await user.inc(this.user._id, 'usage', -lfdoc.size);
throw e;
}
const fdoc = await file.getMeta(ufid);
this.response.template = 'fs_upload.html';
this.response.body = {
fdoc, ufid, usage: udoc.usage, quota,
};
}
}
class StatusHandler extends Handler {
async get() {
const stats = await coll.find().sort({ type: 1, updateAt: -1 }).toArray();
for (const i in stats) {
let desc = '';
const online = new Date(stats[i].updateAt).getTime() > new Date().getTime() - 300000;
if (!online) desc = 'Offline';
desc = desc || 'Online';
stats[i].isOnline = online;
stats[i].status = desc;
}
const path = [
['Hydro', 'homepage'],
['status', null],
];
this.response.body = { stats, path };
this.response.template = 'status.html';
}
}
class StatusUpdateHandler extends Handler {
async post(args) {
this.checkPriv(PRIV.PRIV_JUDGE);
args.type = 'judger';
return coll.updateOne(
{ mid: args.mid, type: 'judger' },
{ $set: args },
{ upsert: true },
);
}
}
class SwitchLanguageHandler extends Handler {
@param('lang', Types.String)
async get(domainId: string, lang: string) {
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
await user.setById(this.user._id, { viewLang: lang });
} else this.session.viewLang = lang;
this.back();
}
}
export async function apply() {
Route('file_download', '/fs/:fileId/:secret', FileDownloadHandler);
Route('file_download_with_name', '/fs/:fileId/:name/:secret', FileDownloadHandler);
Route('file_upload', '/fs/upload', FileUploadHandler, PRIV.PRIV_CREATE_FILE);
Route('status', '/status', StatusHandler);
Route('status_update', '/status/update', StatusUpdateHandler);
Route('switch_language', '/language/:lang', SwitchLanguageHandler);
}
global.Hydro.handler.misc = apply;
+18
View File
@@ -0,0 +1,18 @@
/* eslint-disable no-empty-function */
/* eslint-disable class-methods-use-this */
import { NotFoundError } from '../error';
import { Route, Handler } from '../service/server';
class NotFoundHandler extends Handler {
async prepare() {
throw new NotFoundError();
}
all() { }
}
export async function apply() {
Route('notfound', '*', NotFoundHandler);
}
global.Hydro.handler.notfound = apply;
+635
View File
@@ -0,0 +1,635 @@
import { isSafeInteger, flatten } from 'lodash';
import yaml from 'js-yaml';
import { ObjectID } from 'mongodb';
import {
NoProblemError, ProblemDataNotFoundError, BadRequestError,
SolutionNotFoundError, ProblemNotFoundError, ValidationError,
PermissionError,
} from '../error';
import {
Pdoc, User, Rdoc, PathComponent,
} from '../interface';
import paginate from '../lib/paginate';
import {
checkPid, isTitle, isContent, isPid,
} from '../lib/validator';
import * as file from '../model/file';
import * as problem from '../model/problem';
import * as record from '../model/record';
import * as domain from '../model/domain';
import * as user from '../model/user';
import * as solution from '../model/solution';
import { PERM, PRIV, CONSTANT } from '../model/builtin';
import * as bus from '../service/bus';
import {
Route, Connection, Handler, ConnectionHandler, Types, param, multipart,
} from '../service/server';
const parseCategory = (value: string) => flatten(value.split('+').map((e) => e.split(','))).map((e) => e.trim());
const parsePid = (value: string) => (isSafeInteger(value) ? parseInt(value, 10) : value);
class ProblemHandler extends Handler {
async __prepare() {
this.checkPerm(PERM.PERM_VIEW_PROBLEM);
}
async cleanup() {
if (this.response.template === 'problem_main.html' && this.request.json) {
const {
path, page, pcount, ppcount, pdocs, psdict, category,
} = this.response.body;
this.response.body = {
title: this.renderTitle(category),
fragments: (await Promise.all([
this.renderHTML('partials/problem_list.html', {
page, ppcount, pcount, pdocs, psdict,
}),
this.renderHTML('partials/problem_stat.html', { pcount }),
this.renderHTML('partials/problem_lucky.html', { category }),
this.renderHTML('partials/path.html', { path }),
])).map((i) => ({ html: i })),
raw: {
path, page, pcount, ppcount, pdocs, psdict, category,
},
};
}
}
}
class ProblemMainHandler extends ProblemHandler {
@param('page', Types.UnsignedInt, true)
@param('q', Types.String, true)
async get(domainId: string, page = 1, q = '') {
this.response.template = 'problem_main.html';
const query: any = {};
let psdict = {};
const path: PathComponent[] = [
['Hydro', 'homepage'],
['problem_main', null],
];
if (q) {
q = q.toLowerCase();
const $regex = new RegExp(`\\A\\Q${q}\\E`, 'gmi');
query.title = { $regex };
path.push([q, null, null, true]);
}
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) query.hidden = false;
const [pdocs, ppcount, pcount] = await paginate(
problem.getMulti(domainId, query).sort({ pid: 1 }),
page,
CONSTANT.PROBLEM_PER_PAGE,
);
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
psdict = await problem.getListStatus(
domainId, this.user._id, pdocs.map((pdoc) => pdoc.docId),
);
}
this.response.body = {
path, page, pcount, ppcount, pdocs, psdict, category: q,
};
}
@param('pid', Types.UnsignedInt)
async postStar(domainId: string, pid: number) {
await problem.setStar(domainId, pid, this.user._id, true);
this.back({ star: true });
}
@param('pid', Types.UnsignedInt)
async postUnstar(domainId: string, pid: number) {
await problem.setStar(domainId, pid, this.user._id, false);
this.back({ star: false });
}
}
class ProblemCategoryHandler extends ProblemHandler {
@param('page', Types.UnsignedInt, true)
@param('category', Types.String, null, parseCategory)
async get(domainId: string, page = 1, category: string[]) {
this.response.template = 'problem_main.html';
const q: any = { $and: [] };
for (const name of category) {
q.$and.push({
$or: [
{ category: { $elemMatch: { $eq: name } } },
{ tag: { $elemMatch: { $eq: name } } },
],
});
}
let psdict = {};
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
const [pdocs, ppcount, pcount] = await paginate(
problem.getMulti(domainId, q).sort({ pid: 1 }),
page,
CONSTANT.PROBLEM_PER_PAGE,
);
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
psdict = await problem.getListStatus(
domainId, this.user._id, pdocs.map((pdoc) => pdoc.docId),
);
}
const path = [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
[category, null, null, true],
];
this.response.body = {
path, page, pcount, ppcount, pdocs, psdict, category: category.join('+'),
};
}
}
class ProblemRandomHandler extends ProblemHandler {
@param('category', Types.String, true, null, parseCategory)
async get(domainId: string, category: string[] = []) {
const q: any = category.length ? { $and: [] } : {};
for (const name of category) {
if (name) {
q.$and.push({
$or: [
{ category: { $elemMatch: { $eq: name } } },
{ tag: { $elemMatch: { $eq: name } } },
],
});
}
}
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
const pid = await problem.random(domainId, q);
if (!pid) throw new NoProblemError();
this.response.body = { pid };
this.response.redirect = this.url('problem_detail', { pid });
}
}
class ProblemDetailHandler extends ProblemHandler {
pdoc: Pdoc;
udoc: User;
@param('pid', Types.String, null, parsePid)
async _prepare(domainId: string, pid: number | string) {
this.response.template = 'problem_detail.html';
this.pdoc = await problem.get(domainId, pid, this.user._id);
if (!this.pdoc) throw new ProblemNotFoundError(domainId, pid);
if (this.pdoc.hidden && this.pdoc.owner !== this.user._id) {
this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
this.udoc = await user.getById(domainId, this.pdoc.owner);
this.response.body = {
pdoc: this.pdoc,
udoc: this.udoc,
title: this.pdoc.title,
path: [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
[this.pdoc.title, null, true],
],
};
}
// eslint-disable-next-line
async get(...args: any[]) { }
@param('pid', Types.ObjectID)
@param('dest', Types.String)
@param('hidden', Types.Boolean)
async postCopy(domainId: string, pid: number, destDomainId: string, hidden = false) {
const udoc = await user.getById(destDomainId, this.user._id);
if (!udoc.hasPerm(PERM.PERM_CREATE_PROBLEM)) {
throw new PermissionError(PERM.PERM_CREATE_PROBLEM);
}
if (!this.pdoc.data) {
// Copy Without Data
pid = await problem.add(
destDomainId, this.pdoc.pid, this.pdoc.title,
this.pdoc.content, this.user._id, this.pdoc.tag,
this.pdoc.category, null, hidden,
);
} else if (this.pdoc.data instanceof ObjectID) {
// With data
pid = await problem.add(
destDomainId, this.pdoc.pid, this.pdoc.title,
this.pdoc.content, this.user._id, this.pdoc.tag,
this.pdoc.category, { domainId, pid }, hidden,
);
} else {
// TODO better message
// Data should only be copied once.
throw new BadRequestError('Cannot copy this problem.');
}
this.response.redirect = this.url('problem_settings', { domainId: destDomainId, pid });
}
}
class ProblemSubmitHandler extends ProblemDetailHandler {
@param('pid', Types.String, null, parsePid)
async get(domainId: string, pid: string | number) {
this.response.template = 'problem_submit.html';
const rdocs = await record
.getUserInProblemMulti(domainId, this.user._id, this.pdoc.docId)
.sort({ _id: -1 })
.limit(10)
.toArray();
this.response.body = {
path: [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
[this.pdoc.title, 'problem_detail', { pid }, true],
['problem_submit', null],
],
pdoc: this.pdoc,
udoc: this.udoc,
rdocs,
title: this.pdoc.title,
};
}
@param('lang', Types.String)
@param('code', Types.String)
async post(domainId: string, lang: string, code: string) {
const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true);
const [rdoc] = await Promise.all([
record.get(domainId, rid),
domain.incUserInDomain(domainId, this.user._id, 'nSubmit'),
]);
bus.publish('record_change', { rdoc });
this.response.body = { rid };
this.response.redirect = this.url('record_detail', { rid });
}
}
class ProblemPretestHandler extends ProblemDetailHandler {
@param('lang', Types.String)
@param('code', Types.String)
@param('input', Types.String, true)
async post(domainId: string, lang: string, code: string, input: string = '') {
this.limitRate('add_record', 3600, 100);
// TODO parseConfig
const rid = await record.add(
domainId, this.pdoc.docId, this.user._id,
lang, code, true,
{
input,
time: 1000,
memory: 256,
},
);
const rdoc = await record.get(domainId, rid);
bus.publish('record_change', { rdoc });
this.response.body = { rid };
}
}
class ProblemPretestConnectionHandler extends ConnectionHandler {
pid: string;
domainId: string;
id: string;
@param('pid', Types.String)
async prepare(domainId: string, pid: string) {
const pdoc = await problem.get(domainId, pid);
if (pdoc.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
if (!pdoc) throw new ProblemNotFoundError(domainId, pid);
this.pid = pdoc.docId.toString();
this.domainId = domainId;
this.id = bus.subscribe(['record_change'], this.onRecordChange.bind(this));
}
async onRecordChange(data) {
const rdoc: Rdoc = data.value.rdoc;
if (
rdoc.uid !== this.user._id
|| rdoc.pid.toString() !== this.pid
|| rdoc.domainId !== this.domainId
) return;
rdoc.compilerTexts = [];
rdoc.judgeTexts = [];
// @ts-ignore
rdoc.testCases = rdoc.testCases.map((c) => ({
status: c.status,
}));
// TODO handle update
if (rdoc.contest) return;
this.send({ rdoc });
}
async cleanup() {
bus.unsubscribe(['record_change'], this.id);
}
}
class ProblemStatisticsHandler extends ProblemDetailHandler {
async get(domainId: string) {
const udoc = await user.getById(domainId, this.pdoc.owner);
const path = [
['problem_main', 'problem_main'],
[this.pdoc.title, 'problem_detail', { pid: this.pdoc.pid }, true],
['problem_statistics', null],
];
this.response.template = 'problem_statistics.html';
this.response.body = { pdoc: this.pdoc, udoc, path };
}
}
class ProblemManageHandler extends ProblemDetailHandler {
async prepare() {
if (this.pdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
else this.checkPerm(PERM.PERM_EDIT_PROBLEM_SELF);
}
}
class ProblemSettingsHandler extends ProblemManageHandler {
@param('pid', Types.String)
async get(domainId: string, pid: string) {
this.response.template = 'problem_settings.html';
this.response.body.path = [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
[this.pdoc.title, 'problem_detail', { pid }, true],
['problem_settings', null],
];
if (this.response.body.config) {
this.response.body.config = yaml.safeDump(this.response.body.config);
}
}
@param('pid', Types.String, null, parsePid)
@param('yaml', Types.String)
async postConfig(domainId: string, pid: string | number, cfg: string) {
const pdoc = await problem.get(domainId, pid);
// TODO validate
const config = yaml.safeLoad(cfg);
await problem.edit(domainId, pdoc.docId, { config });
this.back();
}
@param('pid', Types.String, null, parsePid)
@param('hidden', Types.Boolean)
@param('category', Types.String, true, null, parseCategory)
@param('tag', Types.String, true, null, parseCategory)
async postSetting(
domainId: string, pid: string | number, hidden = false,
category: string[] = [], tag: string[] = [],
) {
const pdoc = await problem.get(domainId, pid);
await problem.edit(domainId, pdoc.docId, { hidden, category, tag });
this.back();
}
}
class ProblemEditHandler extends ProblemManageHandler {
async get({ pid }) {
this.response.template = 'problem_edit.html';
this.response.body.path = [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
[this.pdoc.title, 'problem_detail', { pid }, true],
['problem_edit', null],
];
}
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
async post(domainId: string, title: string, content: string) {
const pid = checkPid(this.request.body.pid);
const pdoc = await problem.get(domainId, this.request.params.pid);
await problem.edit(domainId, pdoc.docId, { title, content, pid });
this.response.redirect = this.url('problem_detail', { pid: this.request.params.pid });
}
}
class ProblemDataUploadHandler extends ProblemManageHandler {
async get() {
if (this.pdoc.data instanceof ObjectID) {
const f = await file.getMeta(this.pdoc.data);
this.response.body.md5 = f.md5;
} else if (this.pdoc.data) {
this.response.body.from = this.pdoc.data;
}
this.response.template = 'problem_upload.html';
}
@multipart(256 * 1024)
async post({ domainId }) {
if (!this.request.files.file) throw new ValidationError('file');
this.pdoc = await problem.setTestdata(
domainId, this.pdoc.docId, this.request.files.file.path,
);
if (this.pdoc.data instanceof ObjectID) {
const f = await file.getMeta(this.pdoc.data);
this.response.body.md5 = f.md5;
}
this.response.template = 'problem_upload.html';
}
}
class ProblemDataDownloadHandler extends ProblemDetailHandler {
async get({ pid }) {
if (!this.user.hasPriv(PRIV.PRIV_JUDGE)) {
if (this.user._id !== this.pdoc.owner) {
this.checkPerm(PERM.PERM_READ_PROBLEM_DATA);
} else this.checkPerm(PERM.PERM_READ_PROBLEM_DATA_SELF);
}
if (this.pdoc.data instanceof ObjectID) {
this.response.redirect = await file.url(this.pdoc.data, `${this.pdoc.title}.zip`);
} else if (this.pdoc.data) {
if (!this.pdoc.data.host) {
this.response.redirect = this.url('problem_data', {
domainId: this.pdoc.data.domainId,
pid: this.pdoc.data.pid,
});
} else {
const [scheme, raw] = this.pdoc.data.host.split('//');
const args = JSON.parse(Buffer.from(raw, 'base64').toString());
if (scheme === 'hydro') {
const [secure, host, port, domainId, id] = args;
this.response.redirect = `http${secure ? 's' : ''}://${host}:${port}/d/${domainId}/p/${id}/data`;
} else if (scheme === 'syzoj') {
const [secure, host, port, id] = args;
this.response.redirect = `http${secure ? 's' : ''}://${host}:${port}/problem/${id}/testdata/download`;
} else {
throw new ProblemDataNotFoundError(pid);
}
}
} else throw new ProblemDataNotFoundError(pid);
}
}
class ProblemSolutionHandler extends ProblemDetailHandler {
@param('page', Types.UnsignedInt, true)
async get(domainId: string, page = 1) {
this.response.template = 'problem_solution.html';
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
const [psdocs, pcount, pscount] = await paginate(
solution.getMulti(domainId, this.pdoc.docId),
page,
CONSTANT.SOLUTION_PER_PAGE,
);
const uids = [this.pdoc.owner];
const docids = [];
for (const psdoc of psdocs) {
docids.push(psdoc.docId);
uids.push(psdoc.owner);
if (psdoc.reply.length) {
for (const psrdoc of psdoc.reply) uids.push(psrdoc.owner);
}
}
const udict = await user.getList(domainId, uids);
const pssdict = solution.getListStatus(domainId, docids, this.user._id);
const path = [
['problem_main', 'problem_main'],
[this.pdoc.title, 'problem_detail', { pid: this.pdoc.pid }, true],
['problem_solution', null],
];
this.response.body = {
path, psdocs, page, pcount, pscount, udict, pssdict, pdoc: this.pdoc,
};
}
@param('content', Types.String, isContent)
async postSubmit(domainId: string, content: string) {
this.checkPerm(PERM.PERM_CREATE_PROBLEM_SOLUTION);
await solution.add(domainId, this.pdoc.docId, this.user._id, content);
this.back();
}
@param('content', Types.String, isContent)
@param('psid', Types.ObjectID)
async postEditSolution(domainId: string, content: string, psid: ObjectID) {
let psdoc = await solution.get(domainId, psid);
if (psdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION);
else this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF);
psdoc = await solution.edit(domainId, psdoc.docId, content);
this.back({ psdoc });
}
@param('psid', Types.ObjectID)
async postDeleteSolution(domainId: string, psid: ObjectID) {
const psdoc = await solution.get(domainId, psid);
if (psdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION);
else this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF);
await solution.del(domainId, psdoc.docId);
this.back();
}
@param('psid', Types.ObjectID)
@param('content', Types.String, isContent)
async postReply(domainId: string, psid: ObjectID, content: string) {
this.checkPerm(PERM.PERM_REPLY_PROBLEM_SOLUTION);
const psdoc = await solution.get(domainId, psid);
await solution.reply(domainId, psdoc.docId, this.user._id, content);
this.back();
}
@param('psid', Types.ObjectID)
@param('psrid', Types.ObjectID)
@param('content', Types.String, isContent)
async postEditReply(domainId: string, psid: ObjectID, psrid: ObjectID, content: string) {
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid);
if (!(psrdoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF))) {
this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY);
}
await solution.editReply(domainId, psid, psrid, content);
this.back();
}
@param('psid', Types.ObjectID)
@param('psrid', Types.ObjectID)
async postDeleteReply(domainId: string, psid: ObjectID, psrid: ObjectID) {
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid);
if (!(psrdoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF))) {
this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY);
}
await solution.delReply(domainId, psid, psrid);
this.back();
}
@param('psid', Types.ObjectID)
async postUpvote(domainId: string, psid: ObjectID) {
const [psdoc, pssdoc] = await solution.vote(domainId, psid, this.user._id, 1);
this.back({ vote: psdoc.vote, user_vote: pssdoc.vote });
}
@param('psid', Types.ObjectID)
async postDownvote(domainId: string, psid: ObjectID) {
const [psdoc, pssdoc] = await solution.vote(domainId, psid, this.user._id, -1);
this.back({ vote: psdoc.vote, user_vote: pssdoc.vote });
}
}
class ProblemSolutionRawHandler extends ProblemDetailHandler {
@param('psid', Types.ObjectID)
async get(domainId: string, psid: ObjectID) {
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
const psdoc = await solution.get(domainId, psid);
this.response.type = 'text/markdown';
this.response.body = psdoc.content;
}
}
class ProblemSolutionReplyRawHandler extends ProblemDetailHandler {
@param('psid', Types.ObjectID)
@param('psrid', Types.ObjectID)
async get(domainId: string, psid: ObjectID, psrid: ObjectID) {
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid, psrid);
this.response.type = 'text/markdown';
this.response.body = psrdoc.content;
}
}
class ProblemCreateHandler extends Handler {
async get() {
this.response.template = 'problem_edit.html';
this.checkPerm(PERM.PERM_CREATE_PROBLEM);
this.response.body = {
path: [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
['problem_create', null],
],
page_name: 'problem_create',
};
}
@param('title', Types.String, isTitle)
@param('pid', Types.String, isPid, true)
@param('content', Types.String, isContent)
@param('hidden', Types.Boolean)
async post(domainId: string, title: string, pid: string, content: string, hidden = false) {
const docId = await problem.add(
domainId, pid, title, content,
this.user._id, [], [], null, hidden,
);
this.response.body = { pid: docId };
this.response.redirect = this.url('problem_settings', { pid: docId });
}
}
export async function apply() {
Route('problem_main', '/p', ProblemMainHandler);
Route('problem_category', '/p/category/:category', ProblemCategoryHandler);
Route('problem_random', '/problem/random', ProblemRandomHandler);
Route('problem_detail', '/p/:pid', ProblemDetailHandler);
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
Route('problem_pretest', '/p/:pid/pretest', ProblemPretestHandler);
Route('problem_settings', '/p/:pid/settings', ProblemSettingsHandler);
Route('problem_statistics', '/p/:pid/statistics', ProblemStatisticsHandler);
Route('problem_edit', '/p/:pid/edit', ProblemEditHandler);
Route('problem_upload', '/p/:pid/upload', ProblemDataUploadHandler);
Route('problem_data', '/p/:pid/data', ProblemDataDownloadHandler);
Route('problem_solution', '/p/:pid/solution', ProblemSolutionHandler);
Route('problem_solution_raw', '/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler);
Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler);
Route('problem_create', '/problem/create', ProblemCreateHandler);
Connection('problem_pretest_conn', '/conn/pretest', ProblemPretestConnectionHandler);
}
global.Hydro.handler.problem = apply;
+190
View File
@@ -0,0 +1,190 @@
import { ObjectID } from 'mongodb';
import { PermissionError, RecordNotFoundError } from '../error';
import { PERM, CONSTANT } from '../model/builtin';
import * as problem from '../model/problem';
import * as record from '../model/record';
import * as contest from '../model/contest';
import * as user from '../model/user';
import paginate from '../lib/paginate';
import * as bus from '../service/bus';
import {
Route, Handler, Connection, ConnectionHandler, Types, param,
} from '../service/server';
import { Rdoc } from '../interface';
const RecordHandler = contest.ContestHandlerMixin(Handler);
class RecordListHandler extends RecordHandler {
@param('page', Types.UnsignedInt, true)
@param('pid', Types.String, true)
@param('tid', Types.ObjectID, true)
@param('uidOrName', Types.String, true)
async get(domainId: string, page = 1, pid?: string, tid?: ObjectID, uidOrName?: string) {
this.response.template = 'record_main.html';
if (tid) tid = new ObjectID(tid);
const q: any = { tid, hidden: false };
if (uidOrName) {
q.$or = [
{ unameLower: uidOrName.toLowerCase() },
{ _id: parseInt(uidOrName, 10) },
];
}
if (pid) q.pid = pid;
const [rdocs] = await paginate(
record.getMulti(domainId, q).sort('_id', -1),
page,
CONSTANT.RECORD_PER_PAGE,
);
const canViewProblemHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
const [udict, pdict] = await Promise.all([
user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)),
problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), canViewProblemHidden, false),
]);
const path = [
['Hydro', 'homepage'],
['record_main', null],
];
this.response.body = {
path,
page,
rdocs,
pdict,
udict,
filterPid: pid,
filterTid: tid,
filterUidOrName: uidOrName,
};
}
}
class RecordDetailHandler extends RecordHandler {
@param('rid', Types.ObjectID)
async get(domainId: string, rid: ObjectID) {
this.response.template = 'record_detail.html';
const rdoc = await record.get(domainId, rid);
if (!rdoc) throw new RecordNotFoundError(rid);
if (rdoc.contest) {
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;
}
const [pdoc, udoc] = await Promise.all([
problem.get(domainId, rdoc.pid),
user.getById(domainId, rdoc.uid),
]);
this.response.body = {
path: [
['Hydro', 'homepage'],
['record_detail', null],
],
udoc,
rdoc,
pdoc,
};
}
}
class RecordRejudgeHandler extends Handler {
@param('rid', Types.ObjectID)
async post(domainId: string, rid: ObjectID) {
this.checkPerm(PERM.PERM_REJUDGE);
const rdoc = await record.get(domainId, rid);
if (rdoc) {
await record.reset(domainId, rid, true);
await record.judge(domainId, rid);
}
this.back();
}
}
const RecordConnectionHandler = contest.ContestHandlerMixin(ConnectionHandler);
class RecordMainConnectionHandler extends RecordConnectionHandler {
@param('tid', Types.ObjectID, true)
async prepare(domainId: string, tid: ObjectID) {
this.domainId = domainId;
if (tid) {
const tdoc = await contest.get(domainId, new ObjectID(tid), -1);
if (this.canShowRecord(tdoc)) this.tid = tid;
else {
this.close();
return;
}
}
this.id = bus.subscribe(['record_change'], this.onRecordChange.bind(this));
}
async message(msg) {
if (msg.rids instanceof Array) {
const rids = msg.rids.map((id: string) => new ObjectID(id));
const rdocs = await record.getMulti(
this.domainId, { _id: { $in: rids } },
).toArray();
for (const rdoc of rdocs) {
this.onRecordChange({ value: { rdoc } });
}
}
}
async cleanup() {
bus.unsubscribe(['record_change'], this.id);
}
async onRecordChange(data) {
const rdoc: Rdoc = data.value.rdoc;
if (rdoc.contest && rdoc.contest.tid.toString() !== this.tid) return;
// eslint-disable-next-line prefer-const
let [udoc, pdoc] = await Promise.all([
user.getById(this.domainId, rdoc.uid),
problem.get(this.domainId, rdoc.pid, null),
]);
if (pdoc && pdoc.hidden && !this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null;
else this.send({ html: await this.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc }) });
}
}
class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(ConnectionHandler) {
@param('rid', Types.ObjectID)
async prepare(domainId: string, rid: ObjectID) {
const rdoc = await record.get(domainId, rid);
if (rdoc.contest) {
const tdoc = await contest.get(domainId, rdoc.contest.tid, -1);
if (!this.canShowRecord(tdoc)) {
this.close();
return;
}
}
this.rid = rid.toString();
this.id = bus.subscribe(['record_change'], this.onRecordChange.bind(this));
this.onRecordChange({ value: { rdoc } });
}
async onRecordChange(data) {
const rdoc: Rdoc = data.value.rdoc;
const { $set, $push } = data.value;
if (rdoc._id.toString() !== this.rid) return;
if ($set) this.send({ $set, $push });
else {
this.send({
status_html: await this.renderHTML('record_detail_status.html', { rdoc }),
summary_html: await this.renderHTML('record_detail_summary.html', { rdoc }),
});
}
}
async cleanup() {
bus.unsubscribe(['record_change'], this.id);
}
}
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);
}
global.Hydro.handler.record = apply;
+272
View File
@@ -0,0 +1,272 @@
import assert from 'assert';
import { ObjectID } from 'mongodb';
import { ValidationError, ProblemNotFoundError } from '../error';
import { isTitle, isContent, isDescription } from '../lib/validator';
import paginate from '../lib/paginate';
import { PERM, PRIV, CONSTANT } from '../model/builtin';
import * as problem from '../model/problem';
import * as builtin from '../model/builtin';
import * as training from '../model/training';
import * as user from '../model/user';
import {
Route, Handler, Types, param,
} from '../service/server';
import { Tdoc } from '../interface';
async function _parseDagJson(domainId, dag) {
const parsed = [];
try {
dag = JSON.parse(dag);
assert(dag instanceof Array, 'dag must be an array');
const ids = new Set(dag.map((s) => s._id));
assert(dag.length === ids.size, '_id must be unique');
for (const node of dag) {
assert(node._id, 'each node should have a _id');
assert(node.title, 'each node shoule have a title');
assert(node.requireNids instanceof Array);
assert(node.pids instanceof Array);
assert(node.pids.length);
for (const nid of node.requireNids) {
assert(ids.has(nid), `required nid ${nid} not found`);
}
const tasks = [];
for (const i in node.pids) {
tasks.push(problem.get(domainId, node.pids[i]).then((pdoc) => {
if (!pdoc) throw new ProblemNotFoundError(domainId, node.pids[i]);
node.pids[i] = pdoc.docId;
}));
}
// FIXME no-await-in-loop
// eslint-disable-next-line no-await-in-loop
await Promise.all(tasks);
const newNode = {
_id: parseInt(node._id, 10),
title: node.title,
requireNids: Array.from(new Set(node.requireNids)),
pids: Array.from(new Set(node.pids)),
};
parsed.push(newNode);
}
} catch (e) {
throw new ValidationError('dag', [e.message]);
}
return parsed;
}
class TrainingMainHandler extends Handler {
@param('page', Types.UnsignedInt, true)
async get(domainId: string, page = 1) {
const [tdocs, tpcount] = await paginate(
training.getMulti(domainId),
page,
CONSTANT.TRAINING_PER_PAGE,
);
const tids: Set<ObjectID> = new Set();
for (const tdoc of tdocs) tids.add(tdoc.docId);
const tsdict = {};
let tdict = {};
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
const enrolledTids: Set<ObjectID> = new Set();
const tsdocs = await training.getMultiStatus(domainId, {
uid: this.user._id,
$or: [{ docId: { $in: Array.from(tids) } }, { enroll: 1 }],
}).toArray();
for (const tsdoc of tsdocs) {
tsdict[tsdoc.docId] = tsdoc;
enrolledTids.add(tsdoc.docId);
}
for (const tid of tids) enrolledTids.delete(tid);
if (enrolledTids.size) {
tdict = await training.getList(domainId, Array.from(enrolledTids));
}
}
for (const tdoc of tdocs) tdict[tdoc.docId] = tdoc;
const path = [
['Hydro', 'homepage'],
['training_main', null],
];
this.response.template = 'training_main.html';
this.response.body = {
tdocs, page, tpcount, tsdict, tdict, path,
};
}
}
class TrainingDetailHandler extends Handler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
const tdoc = await training.get(domainId, tid);
const pids = training.getPids(tdoc);
const [owner, pdict] = await Promise.all([
user.getById(domainId, tdoc.owner),
problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)),
]);
const psdict = await problem.getListStatus(domainId, this.user._id, pids);
const donePids = new Set();
const progPids = new Set();
for (const pid in psdict) {
const psdoc = psdict[pid];
if (psdoc.status) {
if (psdoc.status === builtin.STATUS.STATUS_ACCEPTED) {
donePids.add(parseInt(pid, 10));
} else progPids.add(parseInt(pid, 10));
}
}
const nsdict = {};
const ndict = {};
const doneNids = new Set();
for (const node of tdoc.dag) {
ndict[node._id] = node;
const totalCount = node.pids.length;
const doneCount = Set.union(new Set(node.pids), donePids).size;
const nsdoc = {
progress: totalCount ? Math.floor(100 * (doneCount / totalCount)) : 100,
isDone: training.isDone(node, doneNids, donePids),
isProgress: training.isProgress(node, doneNids, donePids, progPids),
isOpen: training.isOpen(node, doneNids, donePids, progPids),
isInvalid: training.isInvalid(node, doneNids),
};
if (nsdoc.isDone) doneNids.add(node._id);
nsdict[node._id] = nsdoc;
}
const tsdoc = await training.setStatus(domainId, tdoc.docId, this.user._id, {
doneNids: Array.from(doneNids),
donePids: Array.from(donePids),
done: doneNids.size === tdoc.dag.length,
});
const path = [
['Hydro', 'homepage'],
['training_main', 'training_main'],
[tdoc.title, null, null, true],
];
this.response.template = 'training_detail.html';
this.response.body = {
path, tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, owner,
};
}
@param('tid', Types.ObjectID)
async postEnroll(domainId: string, tid: ObjectID) {
this.checkPriv(PRIV.PRIV_USER_PROFILE);
const tdoc = await training.get(domainId, tid);
await training.enroll(domainId, tdoc.docId, this.user._id);
this.back();
}
}
class TrainingCreateHandler extends Handler {
async get() {
const path = [
['Hydro', 'homepage'],
['training_main', 'training_main'],
['training_create', null],
];
this.response.template = 'training_edit.html';
this.response.body = { page_name: 'training_create', path };
}
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('dag', Types.String, isContent)
@param('description', Types.String, isDescription)
async post(
domainId: string, title: string, content: string,
_dag: string, description: string,
) {
const dag = await _parseDagJson(domainId, _dag);
const pids = training.getPids({ dag });
assert(pids.length, new ValidationError('dag'));
const pdocs = await problem.getMulti(domainId, {
$or: [{ docId: { $in: pids } }, { pid: { $in: pids } }],
}).sort('_id', 1).toArray();
const existPids = pdocs.map((pdoc) => pdoc.docId);
const existPnames = pdocs.map((pdoc) => pdoc.pid);
if (pids.length !== existPids.length) {
for (const pid of pids) {
assert(
existPids.includes(pid) || existPnames.includes(pid),
new ProblemNotFoundError(pid),
);
}
}
for (const pdoc of pdocs) {
if (pdoc.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
const tid = await training.add(domainId, title, content, this.user._id, dag, description);
this.response.body = { tid };
this.response.redirect = this.url('training_detail', { tid });
}
}
class TrainingEditHandler extends Handler {
tdoc: Tdoc;
@param('tid', Types.ObjectID)
async prepare(domainId: string, tid: ObjectID) {
this.tdoc = await training.get(domainId, tid);
if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_TRAINING);
else this.checkPerm(PERM.PERM_EDIT_TRAINING_SELF);
}
async get({ tid }) {
const dag = JSON.stringify(this.tdoc.dag, null, 2);
const path = [
['Hydro', 'homepage'],
['training_main', 'training_main'],
[this.tdoc.title, 'training_detail', { tid }, true],
['training_edit', null],
];
this.response.template = 'training_edit.html';
this.response.body = {
tdoc: this.tdoc, dag, path, page_name: 'training_edit',
};
}
@param('tid', Types.ObjectID)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('dag', Types.String, isContent)
@param('description', Types.String, isDescription)
async post(
domainId: string, tid: ObjectID,
title: string, content: string,
_dag: string, description: string,
) {
const dag = await _parseDagJson(domainId, _dag);
const pids = training.getPids({ dag });
assert(pids.length, new ValidationError('dag'));
const pdocs = await problem.getMulti(domainId, {
$or: [
{ docId: { $in: pids } },
{ pid: { $in: pids } },
],
}).sort('_id', 1).toArray();
const existPids = pdocs.map((pdoc) => pdoc.docId);
const existPnames = pdocs.map((pdoc) => pdoc.pid);
if (pids.length !== existPids.length) {
for (const pid in pids) {
assert(
existPids.includes(pid) || existPnames.includes(pid),
new ProblemNotFoundError(pid),
);
}
}
for (const pdoc of pdocs) {
if (pdoc.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
await training.edit(domainId, tid, {
title, content, dag, description,
});
this.response.body = { tid };
this.response.redirect = this.url('training_detail', { tid });
}
}
export async function apply() {
Route('training_main', '/training', TrainingMainHandler, PERM.PERM_VIEW_TRAINING);
Route('training_create', '/training/create', TrainingCreateHandler, PERM.PERM_CREATE_TRAINING);
Route('training_detail', '/training/:tid', TrainingDetailHandler, PERM.PERM_VIEW_TRAINING);
Route('training_edit', '/training/:tid/edit', TrainingEditHandler);
}
global.Hydro.handler.training = apply;
+274
View File
@@ -0,0 +1,274 @@
import moment from 'moment-timezone';
import {
UserAlreadyExistError, InvalidTokenError, VerifyPasswordError,
UserNotFoundError, SystemError, BlacklistedError,
UserFacingError,
} from '../error';
import {
Route, Handler, Types, param,
} from '../service/server';
import * as user from '../model/user';
import * as token from '../model/token';
import * as record from '../model/record';
import * as problem from '../model/problem';
import * as task from '../model/task';
import * as system from '../model/system';
import { PERM, PRIV } from '../model/builtin';
import { isEmail, isPassword, isUname } from '../lib/validator';
import { sendMail } from '../lib/mail';
import * as misc from '../lib/misc';
import paginate from '../lib/paginate';
class UserLoginHandler extends Handler {
async get() {
this.response.template = 'user_login.html';
}
@param('uname', Types.String)
@param('password', Types.String)
@param('rememberme', Types.Boolean)
async post(domainId: string, uname: string, password: string, rememberme = false) {
const udoc = await user.getByUname(domainId, uname);
if (!udoc) throw new UserNotFoundError(uname);
udoc.checkPassword(password);
await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip });
if (udoc.priv === PRIV.PRIV_NONE) throw new BlacklistedError(uname);
this.session.uid = udoc._id;
this.session.save = rememberme;
this.response.redirect = this.request.referer.endsWith('/login') ? '/' : this.request.referer;
}
}
class UserLogoutHandler extends Handler {
async get() {
this.response.template = 'user_logout.html';
}
async post() {
this.session.uid = 0;
}
}
class UserRegisterHandler extends Handler {
async get() {
this.response.template = 'user_register.html';
}
@param('mail', Types.String, isEmail)
async post(domainId: string, mail: string) {
if (await user.getByEmail('system', mail)) throw new UserAlreadyExistError(mail);
this.limitRate('send_mail', 3600, 30);
const t = await token.add(
token.TYPE_REGISTRATION,
await system.get('registration_token_expire_seconds'),
{ mail },
);
if (await system.get('smtp.user')) {
const m = await this.renderHTML('user_register_mail.html', {
path: `register/${t[0]}`,
url_prefix: await system.get('server.url'),
});
await sendMail(mail, 'Sign Up', 'user_register_mail', m);
this.response.template = 'user_register_mail_sent.html';
} else {
this.response.redirect = this.url('user_register_with_code', { code: t[0] });
}
}
}
class UserRegisterWithCodeHandler extends Handler {
@param('code', Types.String)
async get(domainId: string, code: string) {
this.response.template = 'user_register_with_code.html';
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
this.response.body = { mail };
}
@param('password', Types.String, isPassword)
@param('verifyPassword', Types.String)
@param('uname', Types.String, isUname)
@param('code', Types.String)
async post(
domainId: string, password: string, verify: string,
uname: string, code: string,
) {
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
if (password !== verify) throw new VerifyPasswordError();
const uid = await user.create(mail, uname, password, undefined, this.request.ip);
await token.del(code, token.TYPE_REGISTRATION);
this.session.uid = uid;
this.response.redirect = this.url('homepage');
}
}
class UserLostPassHandler extends Handler {
async get() {
if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail');
this.response.template = 'user_lostpass.html';
}
@param('mail', Types.String, isEmail)
async post(domainId: string, mail: string) {
if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail');
const udoc = await user.getByEmail('system', mail);
if (!udoc) throw new UserNotFoundError(mail);
const [tid] = await token.add(
token.TYPE_LOSTPASS,
await system.get('lostpass_token_expire_seconds'),
{ uid: udoc._id },
);
const m = await this.renderHTML('user_lostpass_mail', { url: `lostpass/${tid}`, uname: udoc.uname });
await sendMail(mail, 'Lost Password', 'user_lostpass_mail', m);
this.response.template = 'user_lostpass_mail_sent.html';
}
}
class UserLostPassWithCodeHandler extends Handler {
async get({ domainId, code }) {
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
const udoc = await user.getById(domainId, tdoc.uid);
this.response.body = { uname: udoc.uname };
}
@param('code', Types.String)
@param('password', Types.String, isPassword)
@param('verifyPassword', Types.String)
async post(domainId: string, code: string, password: string, verifyPassword: string) {
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
if (password !== verifyPassword) throw new VerifyPasswordError();
await user.setPassword(tdoc.uid, password);
await token.del(code, token.TYPE_LOSTPASS);
this.response.redirect = this.url('homepage');
}
}
class UserDetailHandler extends Handler {
@param('uid', Types.Int)
async get(domainId: string, uid: number) {
const isSelfProfile = this.user._id === uid;
const udoc = await user.getById(domainId, uid);
if (!udoc) throw new UserNotFoundError(uid);
const [sdoc, rdocs, [pdocs, pcount]] = await Promise.all([
token.getMostRecentSessionByUid(uid),
record.getByUid(domainId, uid, 30),
paginate(
problem.getMulti(domainId, { owner: this.user._id }),
1,
100,
),
]);
const pdict = await problem.getList(
domainId, rdocs.map((rdoc) => rdoc.pid),
this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), false,
);
// Remove sensitive data
if (!isSelfProfile && sdoc) {
sdoc.createIp = '';
sdoc.updateIp = '';
sdoc._id = '';
}
const path = [
['Hydro', 'homepage'],
['user_detail', 'user_detail', { uid }],
];
this.response.template = 'user_detail.html';
this.response.body = {
isSelfProfile, udoc, sdoc, rdocs, pdocs, pcount, pdict, path,
};
}
}
class UserDeleteHandler extends Handler {
async post({ password }) {
this.user.checkPassword(password);
const tid = await task.add({
executeAfter: moment().add(7, 'days').toDate(),
type: 'script',
id: 'deleteUser',
args: { uid: this.user._id },
});
await user.setById(this.user._id, { del: tid });
this.response.template = 'user_delete_pending.html';
}
}
class UserSearchHandler extends Handler {
@param('q', Types.String)
@param('exectMatch', Types.Boolean)
async get(domainId: string, q: string, exactMatch = false) {
let udoc = await user.getById(domainId, parseInt(q, 10));
const udocs = udoc ? [udoc] : [];
udoc = await user.getByUname(domainId, q);
if (udoc) udocs.push(udoc);
udoc = await user.getByEmail(domainId, q);
if (udoc) udocs.push(udoc);
if (!exactMatch) udocs.push(...await user.getPrefixList(q, 20));
for (const i in udocs) {
udocs[i].gravatar = misc.gravatar(udocs[i].gravatar || '');
}
this.response.body = udocs;
}
}
class OauthHandler extends Handler {
@param('type', Types.String)
async get(domainId: string, type: string) {
if (global.Hydro.lib[`oauth_${type}`]) await global.Hydro.lib[`oauth_${type}`].get.call(this);
}
}
class OauthCallbackHandler extends Handler {
async get(args: any) {
let r;
if (global.Hydro.lib[`oauth_${args.type}`]) r = await global.Hydro.lib[`oauth_${args.type}`].callback(args);
else throw new UserFacingError('Oauth type');
const udoc = await user.getByEmail('system', r.email);
if (udoc) {
this.session.uid = udoc._id;
} else {
this.checkPriv(PRIV.PRIV_REGISTER_USER);
let username = '';
r.uname = r.uname || [];
r.uname.push(String.random(16));
for (const uname of r.uname) {
// eslint-disable-next-line no-await-in-loop
const nudoc = await user.getByUname('system', uname);
if (!nudoc) {
username = uname;
break;
}
}
const uid = await user.create(
r.email, username, String.random(32),
undefined, this.request.ip,
);
const $set: any = {
oauth: args.type,
};
if (r.bio) $set.bio = r.bio;
if (r.viewLang) $set.viewLang = r.viewLang;
await user.setById(uid, $set);
this.session.uid = uid;
}
}
}
export async function apply() {
Route('user_login', '/login', UserLoginHandler);
Route('user_oauth', '/oauth/:type', OauthHandler);
Route('user_oauth_callback', '/oauth/:type/callback', OauthCallbackHandler);
Route('user_register', '/register', UserRegisterHandler, PRIV.PRIV_REGISTER_USER);
Route('user_register_with_code', '/register/:code', UserRegisterWithCodeHandler, PRIV.PRIV_REGISTER_USER);
Route('user_logout', '/logout', UserLogoutHandler, PRIV.PRIV_USER_PROFILE);
Route('user_lostpass', '/lostpass', UserLostPassHandler);
Route('user_lostpass_with_code', '/lostpass/:code', UserLostPassWithCodeHandler);
Route('user_search', '/user/search', UserSearchHandler, PRIV.PRIV_USER_PROFILE);
Route('user_delete', '/user/delete', UserDeleteHandler, PRIV.PRIV_USER_PROFILE);
Route('user_detail', '/user/:uid', UserDetailHandler);
}
global.Hydro.handler.user = apply;