diff --git a/.eslintrc.js b/.eslintrc.js
index c07334458..34cf399a0 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -20,6 +20,7 @@ module.exports = {
'guard-for-in': 'off',
'implicit-arrow-linebreak': 'off',
'import/extensions': 'off',
+ 'import/no-named-as-default-member': 'off',
'import/prefer-default-export': 'off',
indent: ['warn', 4],
'max-classes-per-file': 'off',
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 61eea6cdc..007d6ff9e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -11,6 +11,7 @@ jobs:
build:
runs-on: ubuntu-latest
env:
+ LICENSE_KEY: ${{ secrets.MAXMIND_KEY }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- name: Check out
diff --git a/.gitignore b/.gitignore
index 3b9fe481c..f65a932fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,9 +14,12 @@ build/*.js
*.js.map
*.hydro
!.gitkeep
-tsconfig.tsbuildinfo
+*.tsbuildinfo
yarn.lock
packages/**/*.js
+!**/bin/*.js
packages/**/*.js.map
-packages/**/*.d.ts
\ No newline at end of file
+packages/**/*.d.ts
+
+*.mmdb
\ No newline at end of file
diff --git a/build/start.ts b/build/start.ts
new file mode 100644
index 000000000..0952a3acc
--- /dev/null
+++ b/build/start.ts
@@ -0,0 +1,39 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import os from 'os';
+import path from 'path';
+import cluster from 'cluster';
+import fs from 'fs-extra';
+import { filter } from 'lodash';
+import { argv } from 'yargs';
+import * as hydro from 'hydrooj';
+
+if (!cluster.isMaster) {
+ // Forked by hydro
+ hydro.load().catch((e) => {
+ console.error(e);
+ process.exit(1);
+ });
+} else {
+ fs.ensureDirSync(path.resolve(os.homedir(), '.hydro'));
+ const addonPath = path.resolve(os.homedir(), '.hydro', 'addon.json');
+ let addons = filter(
+ fs.readdirSync(path.resolve(__dirname, '../packages')),
+ (i) => i !== 'hydrooj',
+ ).map((i) => `@hydrooj/${i}`);
+ fs.writeFileSync(addonPath, JSON.stringify(addons, null, 2));
+
+ try {
+ const ui = argv.ui as string || '@hydrooj/ui-default';
+ require.resolve(ui);
+ addons.push(ui);
+ } catch (e) {
+ console.error('Please also install @hydrooj/ui-default');
+ }
+
+ addons = Array.from(new Set(addons));
+ for (const addon of addons) hydro.addon(addon);
+ (argv._[0] === 'cli' ? hydro.loadCli : hydro.load)().catch((e) => {
+ console.error(e);
+ process.exit(1);
+ });
+}
diff --git a/package.json b/package.json
index 426337a48..9c555ddef 100644
--- a/package.json
+++ b/package.json
@@ -31,5 +31,9 @@
"semver": "^7.3.2",
"ts-jest": "^26.1.4",
"typescript": "^3.9.7"
+ },
+ "dependencies": {
+ "fs-extra": "^9.0.1",
+ "yargs": "^15.4.1"
}
}
diff --git a/packages/geoip/README.md b/packages/geoip/README.md
new file mode 100644
index 000000000..99df0d4e6
--- /dev/null
+++ b/packages/geoip/README.md
@@ -0,0 +1,5 @@
+# GeoIp
+
+显示用户登录地。
+
+IP geo-location data is provided by [Maxmind](http://www.maxmind.com)
diff --git a/packages/geoip/lib.ts b/packages/geoip/lib.ts
new file mode 100644
index 000000000..43ede5d77
--- /dev/null
+++ b/packages/geoip/lib.ts
@@ -0,0 +1,26 @@
+import fs from 'fs';
+import path from 'path';
+import { Reader } from 'maxmind';
+
+const buffer = fs.readFileSync(path.resolve(__dirname, 'GeoLite2-City.mmdb'));
+const reader = new Reader(buffer);
+
+function lookup(ip: string, locale: string) {
+ const res: any = reader.get(ip);
+ if (!res) return {};
+ const ret: any = {};
+ if (res.location) ret.location = res.location;
+ if (res.continent) ret.continent = res.continent.names[locale] || res.continent.names.en;
+ if (res.country || res.registered_country) {
+ ret.country = (res.country || res.registered_country).names[locale]
+ || (res.country || res.registered_country).names.en;
+ }
+ if (res.city) ret.city = res.city.names[locale] || res.city.names.en;
+ ret.display = `${ret.continent} ${ret.country} ${ret.city}`;
+ return ret;
+}
+
+global.Hydro.lib.geoip = exports = {
+ provider: 'MaxMind',
+ lookup,
+};
diff --git a/packages/geoip/locale/zh_CN.yaml b/packages/geoip/locale/zh_CN.yaml
new file mode 100644
index 000000000..98cad2d36
--- /dev/null
+++ b/packages/geoip/locale/zh_CN.yaml
@@ -0,0 +1 @@
+geoip_locale: zh-CN
\ No newline at end of file
diff --git a/packages/geoip/locale/zh_TW.yaml b/packages/geoip/locale/zh_TW.yaml
new file mode 100644
index 000000000..98cad2d36
--- /dev/null
+++ b/packages/geoip/locale/zh_TW.yaml
@@ -0,0 +1 @@
+geoip_locale: zh-CN
\ No newline at end of file
diff --git a/packages/geoip/package.json b/packages/geoip/package.json
new file mode 100644
index 000000000..2dab5c1c7
--- /dev/null
+++ b/packages/geoip/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@hydrooj/geoip",
+ "version": "1.1.6",
+ "main": "package.json",
+ "repository": "git@github.com:hydro-dev/Hydro.git",
+ "author": "undefined ",
+ "license": "MIT",
+ "dependencies": {
+ "maxmind": "^4.1.3"
+ },
+ "peerDependencies": {
+ "hydrooj": "^2.12.0"
+ }
+}
\ No newline at end of file
diff --git a/packages/geoip/prepublish.sh b/packages/geoip/prepublish.sh
new file mode 100644
index 000000000..e32fa0850
--- /dev/null
+++ b/packages/geoip/prepublish.sh
@@ -0,0 +1,4 @@
+wget -O ./GeoLite2-City.tar.gz "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=${LICENSE_KEY}&suffix=tar.gz"
+tar zxvf ./GeoLite2-City.tar.gz -C .
+mv ./GeoLite2-City_*/GeoLite2-City.mmdb ./GeoLite2-City.mmdb
+rm -r ./GeoLite2-City_* ./GeoLite2-City.tar.gz
\ No newline at end of file
diff --git a/packages/hydrooj/bin/hydrooj.js b/packages/hydrooj/bin/hydrooj.js
new file mode 100755
index 000000000..696233a2b
--- /dev/null
+++ b/packages/hydrooj/bin/hydrooj.js
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+
+const os = require('os');
+const path = require('path');
+const cluster = require('cluster');
+const fs = require('fs-extra');
+const { argv } = require('yargs');
+const hydro = require('../dist/loader');
+
+if (!cluster.isMaster) {
+ // Forked by hydro
+ hydro.load().catch((e) => {
+ console.error(e);
+ process.exit(1);
+ });
+} else {
+ fs.ensureDirSync(path.resolve(os.homedir(), '.hydro'));
+ const addonPath = path.resolve(os.homedir(), '.hydro', 'addon.json');
+ if (!fs.existsSync(addonPath)) fs.writeFileSync(addonPath, '[]');
+ let addons = JSON.parse(fs.readFileSync(addonPath).toString());
+
+ try {
+ const ui = argv.ui || '@hydrooj/ui-default';
+ require.resolve(ui);
+ addons.push(ui);
+ } catch (e) {
+ console.error('Please also install @hydrooj/ui-default');
+ }
+
+ if (argv._[0] && argv._[0] !== 'cli') {
+ const operation = argv._[0];
+ const arg1 = argv._[1];
+ const arg2 = argv._[2];
+ if (operation === 'addon') {
+ if (arg1 === 'add') addons.push(arg2);
+ else if (arg1 === 'remove') {
+ for (let i = 0; i < addons.length; i++) {
+ if (addons[i] === arg2) {
+ addons.splice(i, 1);
+ break;
+ }
+ }
+ }
+ addons = Array.from(new Set(addons));
+ console.log('Current Addons: ', addons);
+ fs.writeFileSync(addonPath, JSON.stringify(addons, null, 2));
+ }
+ } else {
+ addons = Array.from(new Set(addons));
+ for (const addon of addons) hydro.addon(addon);
+ (argv._[0] === 'cli' ? hydro.loadCli : hydro.load)().catch((e) => {
+ console.error(e);
+ process.exit(1);
+ });
+ }
+}
diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json
index 8c84fa049..47e4cb906 100644
--- a/packages/hydrooj/package.json
+++ b/packages/hydrooj/package.json
@@ -1,8 +1,8 @@
{
"name": "hydrooj",
- "version": "2.12.2",
- "main": "dist/loader.js",
+ "version": "2.12.3",
"bin": "bin/hydrooj.js",
+ "main": "dist/loader.js",
"repository": "https://github.com/hydro-dev/Hydro.git",
"author": "masnn",
"license": "GPL-3.0-only",
@@ -40,7 +40,7 @@
"ps-list": "^7.2.0",
"serialize-javascript": "^4.0.0",
"sockjs": "^0.3.20",
- "superagent": "^5.2.2",
+ "superagent": "^5.3.1",
"systeminformation": "^4.26.10",
"yargs": "^15.4.1"
},
@@ -70,4 +70,4 @@
"build": "tsc",
"build:watch": "tsc --watch"
}
-}
+}
\ No newline at end of file
diff --git a/packages/hydrooj/src/entry/master.ts b/packages/hydrooj/src/entry/master.ts
index 18718925c..cb29ebff2 100644
--- a/packages/hydrooj/src/entry/master.ts
+++ b/packages/hydrooj/src/entry/master.ts
@@ -58,7 +58,10 @@ export async function load(call: Entry) {
await ins.run({ username: 'Root', password: 'rootroot' });
}
for (const postInit of global.Hydro.postInit) {
- await postInit().catch(console.error);
+ await postInit().catch((e) => {
+ console.error('Error running postInit:', postInit.toString());
+ console.error(e);
+ });
}
return await modelSystem.get('server.worker');
}
diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts
index af4525480..27fd08ee9 100644
--- a/packages/hydrooj/src/handler/contest.ts
+++ b/packages/hydrooj/src/handler/contest.ts
@@ -118,7 +118,7 @@ class ContestBoardcastHandler extends ContestHandler {
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)));
+ const uids: number[] = 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)),
);
@@ -343,7 +343,7 @@ class ContestCodeHandler extends ContestHandler {
},
}).toArray();
for (const rdoc of rdocs) {
- zip.addFile(`${rnames[rdoc._id]}.${rdoc.lang}`, rdoc.code);
+ zip.addFile(`${rnames[rdoc._id.toHexString()]}.${rdoc.lang}`, Buffer.from(rdoc.code));
}
await this.binary(zip.toBuffer(), `${tdoc.title}.zip`);
}
diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts
index a4bb9a6cf..312ec032e 100644
--- a/packages/hydrooj/src/handler/homework.ts
+++ b/packages/hydrooj/src/handler/homework.ts
@@ -395,7 +395,7 @@ class HomeworkCodeHandler extends HomeworkHandler {
},
}).toArray();
for (const rdoc of rdocs) {
- zip.addFile(`${rnames[rdoc._id]}.${rdoc.lang}`, rdoc.code);
+ zip.addFile(`${rnames[rdoc._id.toHexString()]}.${rdoc.lang}`, Buffer.from(rdoc.code));
}
await this.binary(zip.toBuffer(), `${tdoc.title}.zip`);
}
diff --git a/packages/hydrooj/src/handler/user.ts b/packages/hydrooj/src/handler/user.ts
index c7d93a541..8d1833507 100644
--- a/packages/hydrooj/src/handler/user.ts
+++ b/packages/hydrooj/src/handler/user.ts
@@ -18,6 +18,7 @@ import { isEmail, isPassword, isUname } from '../lib/validator';
import { sendMail } from '../lib/mail';
import * as misc from '../lib/misc';
import paginate from '../lib/paginate';
+import { User } from '../interface';
class UserLoginHandler extends Handler {
async get() {
@@ -201,12 +202,12 @@ class UserSearchHandler extends Handler {
@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] : [];
+ const udocs: User[] = 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));
+ if (!exactMatch) udocs.push(...await user.getPrefixList(domainId, q, 20));
for (const i in udocs) {
udocs[i].gravatar = misc.gravatar(udocs[i].gravatar || '');
}
diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts
index c7a13bd96..ee30309d3 100644
--- a/packages/hydrooj/src/interface.ts
+++ b/packages/hydrooj/src/interface.ts
@@ -247,9 +247,10 @@ export interface Mdoc {
export interface Ufdoc {
_id: ObjectID,
secret: string,
- md5: string,
+ md5?: string,
owner: number,
- size: number,
+ size?: number,
+ filename: string,
}
// Blacklist
@@ -357,68 +358,75 @@ export interface JudgeResultBody {
export type PathComponent = [string, string, any?, boolean?];
+export interface Model {
+ blacklist: typeof import('./model/blacklist'),
+ builtin: typeof import('./model/builtin'),
+ contest: typeof import('./model/contest'),
+ discussion: typeof import('./model/discussion'),
+ document: typeof import('./model/document'),
+ domain: typeof import('./model/domain'),
+ file: typeof import('./model/file'),
+ message: typeof import('./model/message'),
+ opcount: typeof import('./model/opcount'),
+ problem: typeof import('./model/problem'),
+ record: typeof import('./model/record'),
+ setting: typeof import('./model/setting'),
+ solution: typeof import('./model/solution'),
+ system: typeof import('./model/system'),
+ task: typeof import('./model/task'),
+ token: typeof import('./model/token'),
+ training: typeof import('./model/training'),
+ user: typeof import('./model/user'),
+ [key: string]: any,
+}
+
+export interface Service {
+ bus: typeof import('./service/bus'),
+ db: typeof import('./service/db'),
+ gridfs: GridFSBucket,
+ monitor: typeof import('./service/monitor'),
+ server: typeof import('./service/server'),
+ [key: string]: any,
+}
+
+export interface Lib {
+ download: typeof import('./lib/download').default,
+ superagent: typeof import('./lib/superagent'),
+ 'hash.hydro': typeof import('./lib/hash.hydro').default,
+ i18n: typeof import('./lib/i18n').default,
+ 'import.syzoj': typeof import('./lib/import.syzoj').syzoj,
+ jwt: typeof import('./lib/jwt'),
+ logger: typeof import('./lib/logger').default,
+ mail: typeof import('./lib/mail'),
+ md5: typeof import('./lib/md5').default,
+ misc: typeof import('./lib/misc'),
+ nav: typeof import('./lib/nav').default,
+ paginate: typeof import('./lib/paginate').default,
+ rank: typeof import('./lib/rank').default,
+ rating: typeof import('./lib/rating').default,
+ testdataConfig: typeof import('./lib/testdataConfig'),
+ sha1: typeof import('./lib/sha1').default,
+ sysinfo: typeof import('./lib/sysinfo'),
+ 'testdata.convert.ini': typeof import('./lib/testdata.convert.ini').default,
+ useragent: typeof import('./lib/useragent'),
+ validator: typeof import('./lib/validator'),
+ [key: string]: any
+}
+
declare global {
namespace NodeJS {
interface Global {
Hydro: {
- model: {
- blacklist: typeof import('./model/blacklist'),
- builtin: typeof import('./model/builtin'),
- contest: typeof import('./model/contest'),
- discussion: typeof import('./model/discussion'),
- document: typeof import('./model/document'),
- domain: typeof import('./model/domain'),
- file: typeof import('./model/file'),
- message: typeof import('./model/message'),
- opcount: typeof import('./model/opcount'),
- problem: typeof import('./model/problem'),
- record: typeof import('./model/record'),
- setting: typeof import('./model/setting'),
- solution: typeof import('./model/solution'),
- system: typeof import('./model/system'),
- task: typeof import('./model/task'),
- token: typeof import('./model/token'),
- training: typeof import('./model/training'),
- user: typeof import('./model/user'),
- [key: string]: any,
- },
- handler: { [key: string]: Function },
- script: { [key: string]: Script },
- service: {
- bus: typeof import('./service/bus'),
- db: typeof import('./service/db'),
- gridfs: GridFSBucket,
- monitor: typeof import('./service/monitor'),
- server: typeof import('./service/server'),
- [key: string]: any,
- },
- lib: {
- download: typeof import('./lib/download').default,
- 'hash.hydro': typeof import('./lib/hash.hydro').default,
- i18n: typeof import('./lib/i18n').default,
- 'import.syzoj': typeof import('./lib/import.syzoj').syzoj,
- jwt: typeof import('./lib/jwt'),
- logger: typeof import('./lib/logger').default,
- mail: typeof import('./lib/mail'),
- md5: typeof import('./lib/md5').default,
- misc: typeof import('./lib/misc'),
- nav: typeof import('./lib/nav').default,
- paginate: typeof import('./lib/paginate').default,
- rank: typeof import('./lib/rank').default,
- rating: typeof import('./lib/rating').default,
- testdataConfig: typeof import('./lib/testdataConfig'),
- sha1: typeof import('./lib/sha1').default,
- sysinfo: typeof import('./lib/sysinfo'),
- 'testdata.convert.ini': typeof import('./lib/testdata.convert.ini').default,
- useragent: typeof import('./lib/useragent'),
- validator: typeof import('./lib/validator'),
- [key: string]: any
- },
+ model: Model,
+ handler: Dict,
+ script: Dict