Merge branch 'master' into contest-access-lock

This commit is contained in:
undefined committed 2022-12-12 02:48:02 +08:00
commit 73dc91c342
146 files changed
+751 -757

No files matched your search

+13 -13
View File
@@ -12,12 +12,12 @@
},
"preferUnplugged": true,
"dependencies": {
"@aws-sdk/client-s3": "^3.216.0",
"@aws-sdk/lib-storage": "^3.216.0",
"@aws-sdk/middleware-endpoint": "^3.215.0",
"@aws-sdk/s3-presigned-post": "^3.216.0",
"@aws-sdk/s3-request-presigner": "^3.216.0",
"@graphql-tools/schema": "^9.0.10",
"@aws-sdk/client-s3": "^3.226.0",
"@aws-sdk/lib-storage": "^3.226.0",
"@aws-sdk/middleware-endpoint": "^3.226.0",
"@aws-sdk/s3-presigned-post": "^3.226.0",
"@aws-sdk/s3-request-presigner": "^3.226.0",
"@graphql-tools/schema": "^9.0.12",
"@hydrooj/utils": "workspace:*",
"adm-zip": "0.5.5",
"cac": "^6.7.14",
@@ -30,16 +30,16 @@
"graphql-scalars": "1.20.1",
"isbinaryfile": "^5.0.0",
"js-yaml": "^4.1.0",
"koa": "^2.13.4",
"koa": "^2.14.1",
"koa-body": "^6.0.1",
"koa-compress": "^5.1.0",
"koa-proxies": "^0.12.2",
"koa-proxies": "^0.12.3",
"koa-router": "^12.0.0",
"koa-static-cache": "^5.1.4",
"lodash": "^4.17.21",
"lru-cache": "7.14.1",
"mime-types": "^2.1.35",
"moment-timezone": "^0.5.39",
"moment-timezone": "^0.5.40",
"mongodb": "^3.7.3",
"nanoid": "^4.0.0",
"nodemailer": "^6.8.0",
@@ -48,11 +48,11 @@
"path-to-regexp": "^6.2.1",
"require-resolve-hook": "^1.1.0",
"saslprep": "^1.0.3",
"schemastery": "^3.5.4",
"schemastery": "^3.6.1",
"semver": "^7.3.8",
"serialize-javascript": "^6.0.0",
"superagent": "^8.0.4",
"tar": "^6.1.12",
"superagent": "^8.0.6",
"tar": "^6.1.13",
"thirty-two": "^1.0.2",
"ws": "^8.11.0"
},
@@ -64,7 +64,7 @@
"@types/koa-compress": "^4.0.3",
"@types/koa-router": "^7.4.4",
"@types/koa-static-cache": "^5.1.1",
"@types/lodash": "^4.14.190",
"@types/lodash": "^4.14.191",
"@types/mime-types": "^2.1.1",
"@types/mongodb": "^3.6.20",
"@types/nodemailer": "^6.4.6",
+5
View File
@@ -41,6 +41,11 @@ langs:
time_limit_rate: Time limit rate
target: Compiler output file
display: Display name
bash:
display: Bash
code_file: foo.sh
execute: /bin/bash foo.sh
highlight: bash
c:
compile: /usr/bin/gcc -Wall --std=c99 -o foo foo.c -lm
highlight: c
+13 -8
View File
@@ -2,7 +2,7 @@ import AdmZip from 'adm-zip';
import { readFile, statSync } from 'fs-extra';
import { isBinaryFile } from 'isbinaryfile';
import {
escapeRegExp, flattenDeep, intersection, isSafeInteger, pick,
escapeRegExp, flattenDeep, intersection, isSafeInteger, pick, uniqBy,
} from 'lodash';
import { FilterQuery, ObjectID } from 'mongodb';
import { nanoid } from 'nanoid';
@@ -35,6 +35,7 @@ import * as bus from '../service/bus';
import {
Handler, param, post, query, route, Types,
} from '../service/server';
import { buildProjection } from '../utils';
import { registerResolver, registerValue } from './api';
import { ContestDetailBaseHandler } from './contest';
@@ -992,18 +993,22 @@ export class ProblemCreateHandler extends Handler {
export class ProblemPrefixListHandler extends Handler {
@param('prefix', Types.Name)
async get(domainId: string, prefix: string) {
const pdocs = await problem.getPrefixList(domainId, prefix);
if (!Number.isNaN(+prefix) && !pdocs.filter((i) => i.docId === +prefix)) {
const pdoc = await problem.get(domainId, +prefix, ['domainId', 'docId', 'pid', 'title']);
if (pdoc) pdocs.unshift(pdoc);
}
const projection = ['domainId', 'docId', 'pid', 'title'] as const;
const [pdocs, pdoc, apdoc] = await Promise.all([
problem.getPrefixList(domainId, prefix),
problem.get(domainId, Number.isSafeInteger(+prefix) ? +prefix : prefix, projection),
/^P\d+$/.test(prefix) ? problem.get(domainId, +prefix.substring(1), projection) : Promise.resolve(null),
]);
if (apdoc) pdocs.unshift(apdoc);
if (pdoc) pdocs.unshift(pdoc);
if (pdocs.length < 20) {
const search = global.Hydro.lib.problemSearch || defaultSearch;
const result = await search(domainId, prefix, { limit: 20 - pdocs.length });
const docs = await problem.getMulti(domainId, { docId: { $in: result.hits.map((i) => +i.split('/')[1]) } }).toArray();
const docs = await problem.getMulti(domainId, { docId: { $in: result.hits.map((i) => +i.split('/')[1]) } })
.project(buildProjection(projection)).toArray();
pdocs.push(...docs);
}
this.response.body = pdocs;
this.response.body = uniqBy(pdocs, 'docId');
}
}
+5 -1
View File
@@ -12,6 +12,7 @@ import { sendMail } from '../lib/mail';
import { isEmail, isPassword } from '../lib/validator';
import BlackListModel from '../model/blacklist';
import { PERM, PRIV, STATUS } from '../model/builtin';
import * as ContestModel from '../model/contest';
import domain from '../model/domain';
import oauth from '../model/oauth';
import * as oplog from '../model/oplog';
@@ -356,9 +357,12 @@ class UserDetailHandler extends Handler {
}
}
const tags = Object.entries(acInfo).sort((a, b) => b[1] - a[1]).slice(0, 20);
const tsdocs = await ContestModel.getMultiStatus(domainId, { uid, attend: { $exists: true } }).project({ docId: 1 }).toArray();
const tdocs = await ContestModel.getMulti(domainId, { docId: { $in: tsdocs.map((i) => i.docId) } })
.project({ docId: 1, title: 1, rule: 1 }).sort({ _id: -1 }).toArray();
this.response.template = 'user_detail.html';
this.response.body = {
isSelfProfile, udoc, sdoc, pdocs, tags,
isSelfProfile, udoc, sdoc, pdocs, tags, tdocs,
};
if (this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION)) {
const psdocs = await SolutionModel.getByUser(domainId, uid).limit(10).toArray();
+3
View File
@@ -69,6 +69,9 @@ export async function add(
payload.domainId!, payload.content!, payload.owner!, document.TYPE_DISCUSSION,
null, payload.parentType, payload.parentId, omit(payload, ['domainId', 'content', 'owner', 'parentType', 'parentId']),
);
await coll.insertOne({
domainId, docId: res, content, uid: owner, ip, time: new Date(),
});
payload.docId = res;
await bus.parallel('discussion/add', payload);
return payload.docId;
+1 -2
View File
@@ -330,8 +330,7 @@ export class ProblemModel {
}
static async getPrefixList(domainId: string, prefix: string) {
prefix = prefix.toLowerCase();
const $regex = new RegExp(`\\A${escapeRegExp(prefix)}`, 'gmi');
const $regex = new RegExp(`^${escapeRegExp(prefix.toLowerCase())}`, 'i');
const filter = { $or: [{ pid: { $regex } }, { title: { $regex } }] };
return await document.getMulti(domainId, document.TYPE_PROBLEM, filter, ['domainId', 'docId', 'pid', 'title'])
.limit(20).toArray();
+1 -1
View File
@@ -60,7 +60,7 @@ export class StorageModel {
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
if (target.length && !target.endsWith('/')) target += '/';
const results = await StorageModel.coll.find({
path: { $regex: new RegExp(`^${escapeRegExp(target)}${recursive ? '' : '[^/]+$'}`, 'i') },
path: { $regex: `^${escapeRegExp(target)}${recursive ? '' : '[^/]+$'}` },
autoDelete: null,
}).toArray();
return results.map((i) => ({
+1 -2
View File
@@ -367,8 +367,7 @@ class UserModel {
@ArgMethod
static async getPrefixList(domainId: string, prefix: string, limit: number = 50) {
prefix = prefix.toLowerCase();
const $regex = new RegExp(`\\A${escapeRegExp(prefix)}`, 'gmi');
const $regex = `^${escapeRegExp(prefix.toLowerCase())}`;
const udocs = await coll.find({ unameLower: { $regex } })
.limit(limit).project({ _id: 1 }).toArray();
const users = [];
+6 -13
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-constant-condition */
/* eslint-disable no-await-in-loop */
import { ObjectID } from 'mongodb';
import Schema from 'schemastery';
@@ -29,10 +28,8 @@ export async function udoc(report) {
},
];
let bulk = db.collection('domain.user').initializeUnorderedBulkOp();
const cursor = db.collection('record').aggregate(pipeline, { allowDiskUse: true });
while (true) {
const adoc = await cursor.next() as any;
if (!adoc) break;
const cursor = db.collection('record').aggregate<any>(pipeline, { allowDiskUse: true });
for await (const adoc of cursor) {
bulk.find({
domainId: adoc._id.domainId,
uid: adoc._id.uid,
@@ -61,10 +58,8 @@ export async function psdoc(report) {
},
},
];
const data = db.collection('record').aggregate(pipeline, { allowDiskUse: true });
while (true) {
const adoc = await data.next() as any;
if (!adoc) break;
const data = db.collection('record').aggregate<any>(pipeline, { allowDiskUse: true });
for await (const adoc of data) {
await document.setStatus(adoc._id.domainId, document.TYPE_PROBLEM, adoc._id.pid, adoc._id.uid, { nSubmit: adoc.nSubmit });
}
}
@@ -117,11 +112,9 @@ export async function pdoc(report) {
pipeline[2].$group[`s${i}`] = { $sum: `$s${i}` };
}
let bulk = db.collection('document').initializeUnorderedBulkOp();
const data = db.collection('record').aggregate(pipeline, { allowDiskUse: true });
const data = db.collection('record').aggregate<any>(pipeline, { allowDiskUse: true });
let cnt = 0;
while (true) {
const adoc = await data.next() as any;
if (!adoc) break;
for await (const adoc of data) {
const $set = {
nSubmit: adoc.nSubmit,
nAccept: adoc.nAccept,
+15 -17
View File
@@ -3,6 +3,7 @@
import { NumericDictionary, unionWith } from 'lodash';
import { FilterQuery } from 'mongodb';
import Schema from 'schemastery';
import { Counter } from '@hydrooj/utils';
import { Tdoc, Udoc } from '../interface';
import difficultyAlgorithm from '../lib/difficulty';
import rating from '../lib/rating';
@@ -100,21 +101,15 @@ export const RpTypes: Record<string, RpDef> = {
global.Hydro.model.rp = RpTypes;
export async function calcLevel(domainId: string, report: Function) {
const filter = { rp: { $gt: 0 } };
const ducnt = await domain.getMultiUserInDomain(domainId, filter).count();
await domain.setMultiUserInDomain(domainId, {}, { level: 0, rank: null });
if (!ducnt) return;
let last = { rp: null };
let rank = 0;
let count = 0;
const coll = db.collection('domain.user');
const filter = { rp: { $gt: 0 }, uid: { $nin: [0, 1], $gt: -1000 } };
const ducur = domain.getMultiUserInDomain(domainId, filter).project({ rp: 1 }).sort({ rp: -1 });
let bulk = coll.initializeUnorderedBulkOp();
// eslint-disable-next-line no-constant-condition
while (true) {
const dudoc = await ducur.next();
if (!dudoc) break;
if ([0, 1].includes(dudoc.uid)) continue;
for await (const dudoc of ducur) {
count++;
if (!dudoc.rp) dudoc.rp = null;
if (dudoc.rp !== last.rp) rank = count;
@@ -122,6 +117,7 @@ export async function calcLevel(domainId: string, report: Function) {
last = dudoc;
if (count % 100 === 0) report({ message: `#${count}: Rank ${rank}` });
}
if (!count) return;
await bulk.execute();
const levels = global.Hydro.model.builtin.LEVELS;
bulk = coll.initializeUnorderedBulkOp();
@@ -136,28 +132,30 @@ export async function calcLevel(domainId: string, report: Function) {
await bulk.execute();
}
async function runInDomain(id: string, report: Function) {
const info = await domain.get(id);
const domainIds = [id, ...(info.union || [])];
async function runInDomain(domainId: string, report: Function) {
const info = await domain.get(domainId);
const domainIds = [domainId, ...(info.union || [])];
const results: Record<keyof typeof RpTypes, ND> = {};
const udict = new Proxy({}, { get: (self, key) => self[key] || 0 });
const udict = Counter();
for (const type in RpTypes) {
results[type] = new Proxy({}, { get: (self, key) => self[key] || RpTypes[type].base });
await RpTypes[type].run(domainIds, results[type], report);
const bulk = db.collection('domain.user').initializeUnorderedBulkOp();
for (const uid in results[type]) {
const udoc = await UserModel.getById(id, +uid);
const udoc = await UserModel.getById(domainId, +uid);
if (!udoc?.hasPriv(PRIV.PRIV_USER_PROFILE)) continue;
await domain.updateUserInDomain(id, +uid, { $set: { [`rpInfo.${type}`]: results[type][uid] } });
bulk.find({ domainId, uid: +uid }).updateOne({ $set: { [`rpInfo.${type}`]: results[type][uid] } });
udict[+uid] += results[type][uid];
}
if (bulk.length) await bulk.execute();
}
await domain.setMultiUserInDomain(id, {}, { rp: 0 });
await domain.setMultiUserInDomain(domainId, {}, { rp: 0 });
const bulk = db.collection('domain.user').initializeUnorderedBulkOp();
for (const uid in udict) {
bulk.find({ domainId: id, uid: +uid }).upsert().update({ $set: { rp: Math.max(0, udict[uid]) } });
bulk.find({ domainId, uid: +uid }).upsert().update({ $set: { rp: Math.max(0, udict[uid]) } });
}
if (bulk.length) await bulk.execute();
await calcLevel(id, report);
await calcLevel(domainId, report);
}
export async function run({ domainId }, report: Function) {
+8 -2
View File
@@ -17,6 +17,7 @@ interface MongoConfig {
port?: string,
name?: string,
url?: string,
uri?: string,
prefix?: string,
}
@@ -29,7 +30,7 @@ class MongoService {
let mongourl = `${opts.protocol || 'mongodb'}://`;
if (opts.username) mongourl += `${opts.username}:${encodeURIComponent(opts.password)}@`;
mongourl += `${opts.host}:${opts.port}/${opts.name}`;
if (opts.url) mongourl = opts.url;
if (opts.url || opts.uri) mongourl = opts.url || opts.uri;
return mongourl;
}
@@ -42,7 +43,12 @@ class MongoService {
mongourl = mongod.getUri();
}
this.opts = opts;
this.client = await MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true });
this.client = await MongoClient.connect(mongourl, {
useNewUrlParser: true,
useUnifiedTopology: true,
readPreference: 'nearest',
writeConcern: 'majority',
});
this.db = this.client.db(opts.name || 'hydro');
await bus.parallel('database/connect', this.db);
}
+1 -1
View File
@@ -10,7 +10,7 @@ export type NestKeys<O, T = any, S = any[]> = O extends object ? O extends S ? n
export type Value<O, V = ''> = {
[K in keyof O]: V
};
export type Projection<O> = (string & keyof O)[];
export type Projection<O> = readonly (string & keyof O)[];
export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Filter<T, U> = T extends U ? T : never;
export type MaybeArray<T> = T | T[];
+1 -1
View File
@@ -1,4 +1,4 @@
export function buildProjection(fields: Array<string | number>): Record<string, 1> {
export function buildProjection(fields: readonly (string | number)[]): Record<string, 1> {
const o = {};
for (const k of fields) o[k] = 1;
return o;