diff --git a/packages/hydrooj/bin/commands.ts b/packages/hydrooj/bin/commands.ts index 63e1ab46..125b0d96 100644 --- a/packages/hydrooj/bin/commands.ts +++ b/packages/hydrooj/bin/commands.ts @@ -1,13 +1,16 @@ -import child, { execSync } from 'child_process'; +import child from 'child_process'; import os from 'os'; import path from 'path'; import cac from 'cac'; import fs from 'fs-extra'; +import superagent from 'superagent'; +import tar from 'tar'; const argv = cac().parse(); -let isGlobal = null; +let yarnVersion = 0; try { - isGlobal = __dirname.startsWith(execSync('yarn global dir').toString().trim()); + // eslint-disable-next-line no-unsafe-optional-chaining + yarnVersion = +child.execSync('yarn --version', { cwd: os.tmpdir() }).toString().split('v').pop()!.split('.')[0]; } catch (e) { // yarn 2 does not support global dir } @@ -124,14 +127,35 @@ if (!argv.args[0] || argv.args[0] === 'cli') { console.log('Current Addons: ', addons); fs.writeFileSync(addonPath, JSON.stringify(addons, null, 2)); }); - cli.command('install [package]').action((name) => { - if (!isGlobal) { - console.warn('This is not a global installation, unable to install.'); - return; - } - // TODO support install from tarball - child.execSync(`yarn global install '${name}'`, { stdio: 'inherit' }); - child.execSync(`hydrooj addon add '${name}'`); + cli.command('install [package]').action(async (src) => { + if (yarnVersion !== 1) throw new Error('Yarn 1 is required.'); + const addonDir = path.join(hydroPath, 'addons'); + let newAddonPath: string = ''; + fs.ensureDirSync(addonDir); + // TODO: install from npm and check for update + if (src.startsWith('http')) { + const url = new URL(src); + const filename = url.pathname.split('/').pop()!; + if (['.tar.gz', '.tgz', '.zip'].find((i) => filename.endsWith(i))) { + const name = filename.replace(/(-?\d+\.\d+\.\d+)?(\.tar\.gz|\.zip|\.tgz)$/g, ''); + newAddonPath = path.join(addonDir, name); + console.log(`Downloading ${src} to ${newAddonPath}`); + fs.ensureDirSync(newAddonPath); + await new Promise((resolve, reject) => { + superagent.get(src) + .pipe(tar.x({ + C: newAddonPath, + strip: 1, + })) + .on('finish', resolve) + .on('error', reject); + }); + } else throw new Error('Unsupported file type'); + } else throw new Error('Unsupported install schema'); + if (!newAddonPath) throw new Error('Addon download failed'); + console.log('Installing depedencies'); + child.execSync('yarn --production', { stdio: 'inherit', cwd: newAddonPath }); + child.execSync(`hydrooj addon add '${newAddonPath}'`); }); cli.help(); cli.parse(); diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 80d6fbd4..f5435002 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -52,6 +52,7 @@ "semver": "^7.3.8", "serialize-javascript": "^6.0.0", "superagent": "^8.0.3", + "tar": "^6.1.12", "thirty-two": "^1.0.2", "ws": "^8.10.0" }, @@ -72,6 +73,7 @@ "@types/semver": "^7.3.13", "@types/serialize-javascript": "^5.0.2", "@types/superagent": "^4.1.15", + "@types/tar": "^6.1.3", "moment": "^2.29.4" } } diff --git a/packages/hydrooj/src/lib/validator.ts b/packages/hydrooj/src/lib/validator.ts index fd85c7cd..277a0741 100644 --- a/packages/hydrooj/src/lib/validator.ts +++ b/packages/hydrooj/src/lib/validator.ts @@ -3,7 +3,6 @@ import { ValidationError } from '../error'; const RE_UID = /^-?\d+$/i; const RE_DOMAINID = /^[a-zA-Z][a-zA-Z0-9_]{3,31}$/i; const RE_PID = /^[a-zA-Z]+[a-zA-Z0-9]*$/i; -const RE_UNAME = /^.{3,31}$/i; const RE_ROLE = /^[_0-9A-Za-z]{1,31}$/i; const RE_MAIL = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i; @@ -13,8 +12,6 @@ export const isDomainId = (s) => RE_DOMAINID.test(s); export const checkDomainId = (s) => { if (!isDomainId(s)) throw new ValidationError('domainId'); else return s; }; export const isUid = (s) => RE_UID.test(s); export const checkUid = (s) => { if (!isUid(s)) throw new ValidationError('uid'); else return s; }; -export const isUname = (s) => RE_UNAME.test(s); -export const checkUname = (s) => { if (!isUname(s)) throw new ValidationError('uname'); else return s; }; export const isRole = (s) => RE_ROLE.test(s); export const checkRole = (s) => { if (!isRole(s)) throw new ValidationError('role'); else return s; }; export const isPassword = (s) => s.length >= 5; @@ -37,8 +34,6 @@ global.Hydro.lib.validator = { checkDomainId, isUid, checkUid, - isUname, - checkUname, isRole, checkRole, isPassword, diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index b2a3f179..0845f7da 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -298,15 +298,15 @@ const oi = buildContestRule({ const udict = await user.getList(tdoc.domainId, uids); const psdict = {}; const first = {}; - for (const pid of tdoc.pids) { - // eslint-disable-next-line @typescript-eslint/no-use-before-define, no-await-in-loop + await Promise.all(tdoc.pids.map(async (pid) => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define const [data] = await getMultiStatus(tdoc.domainId, { docType: document.TYPE_CONTEST, docId: tdoc.docId, [`detail.${pid}.status`]: STATUS.STATUS_ACCEPTED, }).sort({ [`detail.${pid}.rid`]: 1 }).limit(1).toArray(); first[pid] = data ? data.detail[pid].rid.generationTime : new ObjectID().generationTime; - } + })); // eslint-disable-next-line @typescript-eslint/no-use-before-define if (isDone(tdoc)) { const psdocs = await Promise.all( diff --git a/packages/login-with-github/index.ts b/packages/login-with-github/index.ts index 4625cf07..377f822f 100644 --- a/packages/login-with-github/index.ts +++ b/packages/login-with-github/index.ts @@ -76,4 +76,7 @@ export function apply(ctx: Context) { callback, get, }); + ctx.i18n.load('zh', { + 'Login With Github': '使用 Github 登录', + }); } diff --git a/packages/login-with-github/locales/zh.yaml b/packages/login-with-github/locales/zh.yaml deleted file mode 100644 index d12c4f7d..00000000 --- a/packages/login-with-github/locales/zh.yaml +++ /dev/null @@ -1 +0,0 @@ -Login With Github: 使用 Github 登录 \ No newline at end of file diff --git a/packages/login-with-google/index.ts b/packages/login-with-google/index.ts index 7e94bcb0..ff1da8ae 100644 --- a/packages/login-with-google/index.ts +++ b/packages/login-with-google/index.ts @@ -78,4 +78,7 @@ export function apply(ctx: Context) { callback, get, }); + ctx.i18n.load('zh', { + 'Login With Google': '使用 Google 登录', + }); } diff --git a/packages/login-with-google/locales/zh.yaml b/packages/login-with-google/locales/zh.yaml deleted file mode 100644 index c56e260e..00000000 --- a/packages/login-with-google/locales/zh.yaml +++ /dev/null @@ -1 +0,0 @@ -Login With Google: 使用 Google 登录 \ No newline at end of file diff --git a/packages/migrate/index.ts b/packages/migrate/index.ts index 6b3cf9db..41290268 100644 --- a/packages/migrate/index.ts +++ b/packages/migrate/index.ts @@ -9,6 +9,7 @@ export function apply(ctx: Context) { hustoj(ctx); vijos(ctx); syzoj(ctx); + ctx.provideModule('hash', 'hust', ($password, $saved) => { $password = md5($password); if (RE_MD5.test($saved)) return $password === $saved; @@ -27,4 +28,10 @@ export function apply(ctx: Context) { return `${Buffer.from(uname).toString('base64')}|${mixedSha1}`; }); ctx.provideModule('hash', 'syzoj', (password: string) => md5(`${password}syzoj2_xxx`)); + + ctx.i18n.load('zh', { + 'migrate from hustoj': '从 HustOJ 导入', + 'migrate from vijos': '从 Vijos 导入', + 'migrate from syzoj': '从 SYZOJ 导入', + }); } diff --git a/packages/migrate/locale/zh.yaml b/packages/migrate/locale/zh.yaml deleted file mode 100644 index 915ee05a..00000000 --- a/packages/migrate/locale/zh.yaml +++ /dev/null @@ -1,2 +0,0 @@ -migrate from hustoj: 从 HustOJ 导入 -migrate from vijos: 从 Vijos 导入 \ No newline at end of file diff --git a/packages/recaptcha/index.ts b/packages/recaptcha/index.ts index 04db12dc..aa3868aa 100644 --- a/packages/recaptcha/index.ts +++ b/packages/recaptcha/index.ts @@ -15,7 +15,7 @@ export function apply(ctx: Context) { .field('secret', SystemModel.get('recaptcha.secret')) .field('response', thisArg.args.captcha) .field('remoteip', thisArg.request.ip); - if (!response.body.success) throw new ForbiddenError('captcha fail'); + if (!response.body.success) throw new ForbiddenError('Failed to solve the captcha.'); }); ctx.on('handler/after/UserRegister', async (thisArg) => { @@ -24,4 +24,8 @@ export function apply(ctx: Context) { `; }); + + ctx.i18n.load('zh', { + 'Failed to solve the captcha': '没有通过 ReCaptcha 验证。', + }); } diff --git a/packages/recaptcha/locale/zh.yaml b/packages/recaptcha/locale/zh.yaml deleted file mode 100644 index 8979369c..00000000 --- a/packages/recaptcha/locale/zh.yaml +++ /dev/null @@ -1 +0,0 @@ -captcha fail: 您没有通过 Recaptcha 验证。 \ No newline at end of file diff --git a/packages/sonic/index.ts b/packages/sonic/index.ts index 563c6669..ea6f6756 100644 --- a/packages/sonic/index.ts +++ b/packages/sonic/index.ts @@ -92,4 +92,8 @@ export function apply(ctx: Context) { }), run, ); + + ctx.i18n.load('zh', { + 'Sonic problem search re-index': '重建题目搜索索引。', + }); } diff --git a/packages/sonic/locale/zh.yaml b/packages/sonic/locale/zh.yaml deleted file mode 100644 index 39efcc53..00000000 --- a/packages/sonic/locale/zh.yaml +++ /dev/null @@ -1 +0,0 @@ -Sonic problem search re-index: 重建题目搜索索引。