config: remove all setting.yaml

This commit is contained in:
undefined committed 2025-03-24 19:42:15 +08:00
1 parent e1f4f699b8
commit 9bdf241142
16 files changed
+373 -474

No files matched your search

+25 -16
View File
@@ -6,24 +6,31 @@ import { fs, yaml } from '@hydrooj/utils';
const argv = cac().parse(); const argv = cac().parse();
const JudgeSettings = Schema.object({ const defaultEnv = `\
cache_dir: Schema.string().default(path.resolve(os.homedir(), '.cache', 'hydro', 'judge')), PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/w
# modify to your python version installed
PYTHONPATH=/lib/python3.12/site-packages
`;
export const JudgeSettings = Schema.object({
cache_dir: Schema.string().default(path.resolve(os.homedir(), '.cache', 'hydro', 'judge')).description('Testdata cache directory'),
tmp_dir: Schema.string().default(path.resolve(os.tmpdir(), 'hydro', 'judge')), tmp_dir: Schema.string().default(path.resolve(os.tmpdir(), 'hydro', 'judge')),
stdio_size: Schema.string().pattern(/^\d+[kmg]b?$/g).default('32m'), stdio_size: Schema.string().pattern(/^\d+[kmg]b?$/g).default('32m'),
memoryMax: Schema.string().pattern(/^\d+[kmg]b?$/g).default('512m'), memoryMax: Schema.string().pattern(/^\d+[kmg]b?$/g).default('512m'),
strict_memory: Schema.boolean().default(false), strict_memory: Schema.boolean().default(false).description('Use address space memory limit'),
sandbox_host: Schema.string().role('link').default('http://localhost:5050'), sandbox_host: Schema.string().role('url').default('http://localhost:5050'),
testcases_max: Schema.number().default(100), testcases_max: Schema.number().default(100).min(1).step(1),
total_time_limit: Schema.number().default(60), total_time_limit: Schema.number().default(60).min(1),
processLimit: Schema.number().default(32), processLimit: Schema.number().default(32).min(1).step(1),
parallelism: Schema.number().default(2), parallelism: Schema.number().default(2).min(1).step(1),
concurrency: Schema.number(), concurrency: Schema.number(),
singleTaskParallelism: Schema.number().default(2), singleTaskParallelism: Schema.number().default(2).min(1).step(1),
rerun: Schema.number().description('Re-Run testcase if time-limit-exceeded (max per submission)').default(0), rerun: Schema.number().description('Re-Run testcase if time-limit-exceeded (max per submission)').default(0).min(0).step(1),
rate: Schema.number().default(1), rate: Schema.number().default(1),
env: Schema.string().default('PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\nHOME=/w'), env: Schema.string().default(defaultEnv),
host: Schema.any(), host: Schema.any(),
secret: Schema.string().description('Judge Token Secret').default(String.random(32)), secret: Schema.string().description('Judge Token Secret').default('Hydro-Judge-Secret'),
disable: Schema.boolean().description('Disable builtin judge').default(false), disable: Schema.boolean().description('Disable builtin judge').default(false),
detail: Schema.boolean().description('Show diff detail').default(true), detail: Schema.boolean().description('Show diff detail').default(true),
performance: Schema.boolean().description('Performance mode').default(false), performance: Schema.boolean().description('Performance mode').default(false),
@@ -32,7 +39,7 @@ const JudgeSettings = Schema.object({
const oldPath = path.resolve(os.homedir(), '.config', 'hydro', 'judge.yaml'); const oldPath = path.resolve(os.homedir(), '.config', 'hydro', 'judge.yaml');
const newPath = path.resolve(os.homedir(), '.hydro', 'judge.yaml'); const newPath = path.resolve(os.homedir(), '.hydro', 'judge.yaml');
const config = global.Hydro let config = global.Hydro
? JudgeSettings({}) ? JudgeSettings({})
: (() => { : (() => {
const base: any = {}; const base: any = {};
@@ -54,6 +61,8 @@ const config = global.Hydro
return JudgeSettings(cfg); return JudgeSettings(cfg);
})(); })();
export const getConfig: <K extends keyof typeof config>(key: K) => typeof config[K] = global.Hydro export function overrideConfig(update: ReturnType<typeof JudgeSettings>) {
? (key) => global.Hydro.model.system.get(`hydrojudge.${key}`) ?? config[key] config = JudgeSettings(update);
: (key) => config[key]; }
export const getConfig: <K extends keyof typeof config>(key: K) => typeof config[K] = (key) => config[key];
+9 -7
View File
@@ -4,7 +4,7 @@ import path from 'path';
import { fs } from '@hydrooj/utils'; import { fs } from '@hydrooj/utils';
import * as sysinfo from '@hydrooj/utils/lib/sysinfo'; import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
import { import {
db, JudgeHandler, JudgeResultBody, ObjectId, RecordModel, Context as HydroContext, db, JudgeHandler, JudgeResultBody, ObjectId, RecordModel,
SettingModel, StorageModel, SystemModel, TaskModel, SettingModel, StorageModel, SystemModel, TaskModel,
} from 'hydrooj'; } from 'hydrooj';
import { langs } from 'hydrooj/src/model/setting'; import { langs } from 'hydrooj/src/model/setting';
@@ -59,10 +59,11 @@ const session: Session = {
}, },
}; };
export async function postInit(ctx) { export async function* apply(ctx: HydroContext) {
if (SystemModel.get('hydrojudge.disable')) return;
ctx.inject(['check'], (c) => { ctx.inject(['check'], (c) => {
c.check.addChecker('Judge', (_ctx, log, warn, error) => versionCheck(warn, error)); c.check.addChecker('Judge', async (_ctx, log, warn, error) => {
await versionCheck(warn, error);
});
}); });
await fs.ensureDir(getConfig('tmp_dir')); await fs.ensureDir(getConfig('tmp_dir'));
const info = await sysinfo.get(); const info = await sysinfo.get();
@@ -76,6 +77,7 @@ export async function postInit(ctx) {
}; };
const parallelism = Math.max(getConfig('parallelism'), 2); const parallelism = Math.max(getConfig('parallelism'), 2);
const taskConsumer = TaskModel.consume({ type: 'judge' }, handle, true, parallelism); const taskConsumer = TaskModel.consume({ type: 'judge' }, handle, true, parallelism);
yield () => taskConsumer.destroy();
async function collectInfo() { async function collectInfo() {
const coll = db.collection('status'); const coll = db.collection('status');
const compilers = await compilerVersions(langs); const compilers = await compilerVersions(langs);
@@ -91,11 +93,11 @@ export async function postInit(ctx) {
{ upsert: true }, { upsert: true },
); );
} }
ctx.on('system/setting', () => { yield ctx.on('system/setting', () => {
taskConsumer.setConcurrency(Math.max(getConfig('parallelism'), 2)); taskConsumer.setConcurrency(Math.max(getConfig('parallelism'), 2));
collectInfo(); collectInfo();
}); });
collectInfo(); collectInfo();
TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle); const generateConsumer = TaskModel.consume({ type: 'generate' }, handle);
TaskModel.consume({ type: 'generate' }, handle); yield () => generateConsumer.destroy();
} }
+6 -15
View File
@@ -1,20 +1,11 @@
import { Context } from 'hydrooj'; import { Context } from 'hydrooj';
import { JudgeSettings, overrideConfig } from './config';
declare module 'hydrooj' { export const Config = JudgeSettings;
interface SystemKeys {
'hydrojudge.cache_dir': string;
'hydrojudge.tmp_dir': string;
'hydrojudge.sandbox_host': string;
'hydrojudge.memoryMax': string;
'hydrojudge.testcases_max': number;
'hydrojudge.total_time_limit': number;
'hydrojudge.parallelism': number;
'hydrojudge.disable': boolean;
'hydrojudge.detail': boolean;
}
}
export function apply(ctx: Context) { export function apply(ctx: Context, config: ReturnType<typeof Config>) {
if (process.env.NODE_APP_INSTANCE !== '0') return; if (process.env.NODE_APP_INSTANCE !== '0') return;
ctx.once('app/started', () => require('./hosts/builtin').postInit(ctx)); overrideConfig(config);
// eslint-disable-next-line consistent-return
if (!config.disable) return require('./hosts/builtin').apply(ctx);
} }
+1 -1
View File
@@ -6,7 +6,7 @@ import type { DomainDoc, GeoIP, ModuleInterfaces } from './interface';
import { inject } from './lib/ui'; import { inject } from './lib/ui';
import { Loader } from './loader'; import { Loader } from './loader';
import type { EventMap } from './service/bus'; import type { EventMap } from './service/bus';
import type { CheckService } from './service/check'; import type CheckService from './service/check';
import type { } from './service/migration'; import type { } from './service/migration';
// TODO: this is an broken declaration // TODO: this is an broken declaration
+3 -4
View File
@@ -13,7 +13,6 @@ import record from '../model/record';
import * as setting from '../model/setting'; import * as setting from '../model/setting';
import * as system from '../model/system'; import * as system from '../model/system';
import user from '../model/user'; import user from '../model/user';
import { check } from '../service/check';
import { import {
ConnectionHandler, Handler, param, requireSudo, Types, ConnectionHandler, Handler, param, requireSudo, Types,
} from '../service/server'; } from '../service/server';
@@ -70,11 +69,11 @@ class SystemCheckConnHandler extends ConnectionHandler {
const log = (payload: any) => this.send({ type: 'log', payload }); const log = (payload: any) => this.send({ type: 'log', payload });
const warn = (payload: any) => this.send({ type: 'warn', payload }); const warn = (payload: any) => this.send({ type: 'warn', payload });
const error = (payload: any) => this.send({ type: 'error', payload }); const error = (payload: any) => this.send({ type: 'error', payload });
await check.run(this, log, warn, error, (id) => { this.id = id; }); await this.ctx.check.run(this, log, warn, error, (id) => { this.id = id; });
} }
async cleanup() { async cleanup() {
check.cancel(this.id); this.ctx.check.cancel(this.id);
} }
} }
@@ -351,7 +350,7 @@ class SystemUserPrivHandler extends SystemHandler {
} }
} }
export const inject = ['config']; export const inject = ['config', 'check'];
export async function apply(ctx) { export async function apply(ctx) {
ctx.Route('manage', '/manage', SystemMainHandler); ctx.Route('manage', '/manage', SystemMainHandler);
ctx.Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler); ctx.Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler);
+40 -42
View File
@@ -7,11 +7,49 @@ import * as system from '../model/system';
import db from './db'; import db from './db';
import storage from './storage'; import storage from './storage';
type CheckItem = (context: any, log: Function, warn: Function, error: Function) => Promise<void>; type ReportFunction = (message: string, ...args: any[]) => Promise<void>;
type CheckItem = (context: any, log: ReportFunction, warn: ReportFunction, error: ReportFunction) => Promise<void>;
export class CheckService extends Service { export default class CheckService extends Service {
checkers: Dictionary<CheckItem> = {}; checkers: Dictionary<CheckItem> = {};
c = {}; c = {};
constructor(ctx: Context) {
super(ctx, 'check');
this.addChecker('Db', async (c, log, warn, error) => {
try {
const coll = db.collection('check');
const d = await coll.findOne({ _id: 'check' });
if (!d) await coll.insertOne({ _id: 'check', value: 'check' });
await coll.createIndex('check');
await coll.deleteOne({ _id: 'check' });
await coll.drop();
} catch (e) {
error(`Mongo Error: Database read-write failed.\n${e.message}`);
}
});
this.addChecker('Storage', async (c, log, warn, error) => {
const status = await storage.status();
if (!status.status) error(`Connect to ${status.type} Storage ${status.bucket} Error: ${status.error}`);
});
this.addChecker('System', async (c, log, warn) => {
const platform = os.platform();
if (platform !== 'linux') {
warn(`Hydro is running on ${platform}, suggest to use linux4.4+.`);
}
});
this.addChecker('Mail', async (c, log, warn) => {
const from = system.get('smtp.from');
if (!from) warn('SMTP account is not provided, email verification disabled.');
});
this.addChecker('Setting', async (c, log, warn) => {
const url = system.get('server.url');
if (url === '/') warn("server.url isn't set.");
const header = system.get('server.xff');
if (header && !c.request.ip) warn('IP header seems incorrect.\nCheck dashboard>settings>server.');
});
}
addChecker(type: string, checkFunc: CheckItem) { addChecker(type: string, checkFunc: CheckItem) {
this.ctx.effect(() => { this.ctx.effect(() => {
this.checkers[`check${type}`] = checkFunc; this.checkers[`check${type}`] = checkFunc;
@@ -38,43 +76,3 @@ export class CheckService extends Service {
this.c[id] = true; this.c[id] = true;
} }
} }
export const check = new CheckService(app, 'check');
check.addChecker('Db', async (ctx, log, warn, error) => {
try {
const coll = db.collection('check');
const d = await coll.findOne({ _id: 'check' });
if (!d) await coll.insertOne({ _id: 'check', value: 'check' });
await coll.createIndex('check');
await coll.deleteOne({ _id: 'check' });
await coll.drop();
} catch (e) {
error(`Mongo Error: Database read-write failed.\n${e.message}`);
}
});
check.addChecker('Storage', async (ctx, log, warn, error) => {
const status = await storage.status();
if (!status.status) error(`Connect to ${status.type} Storage ${status.bucket} Error: ${status.error}`);
});
check.addChecker('System', async (ctx, log, warn) => {
const platform = os.platform();
if (platform !== 'linux') {
warn(`Hydro is running on ${platform}, suggest to use linux4.4+.`);
}
});
check.addChecker('Mail', async (ctx, log, warn) => {
const from = system.get('smtp.from');
if (!from) warn('SMTP account is not provided, email verification disabled.');
});
check.addChecker('Setting', async (ctx, log, warn) => {
const url = system.get('server.url');
if (url === '/') warn("server.url isn't set.");
const header = system.get('server.xff');
if (header && !ctx.request.ip) warn('IP header seems incorrect.\nCheck dashboard>settings>server.');
});
export async function apply(ctx: Context) {
ctx.provide('check', CheckService, true);
ctx.check = check;
}
+64 -76
View File
@@ -1,82 +1,70 @@
import { import {
Context, ForbiddenError, Handler, superagent, SystemModel, Context, ForbiddenError, Handler, Schema, Service, superagent, SystemModel,
TokenModel, UserFacingError, ValidationError, TokenModel, UserFacingError, ValidationError,
} from 'hydrooj'; } from 'hydrooj';
declare module 'hydrooj' { export default class LoginWithGithubService extends Service {
interface SystemKeys { static Config = Schema.object({
'login-with-github.id': string; id: Schema.string().description('Github OAuth AppID').required(),
'login-with-github.secret': string; secret: Schema.string().description('Github OAuth Secret').role('secret').required(),
'login-with-github.endpoint': string; endpoint: Schema.string().description('Github Endpoint'),
}
}
async function get(this: Handler) {
const [appid, [state]] = await Promise.all([
SystemModel.get('login-with-github.id'),
TokenModel.add(TokenModel.TYPE_OAUTH, 600, { redirect: this.request.referer }),
]);
this.response.redirect = `https://github.com/login/oauth/authorize?client_id=${appid}&state=${state}&scope=read:user,user:email`;
}
async function callback({ state, code }) {
const [[appid, secret, endpoint, url], s] = await Promise.all([
SystemModel.getMany([
'login-with-github.id',
'login-with-github.secret',
'login-with-github.endpoint',
'server.url',
]),
TokenModel.get(state, TokenModel.TYPE_OAUTH),
]);
if (!s) throw new ValidationError('token');
const res = await superagent.post(`${endpoint || 'https://github.com'}/login/oauth/access_token`)
.send({
client_id: appid,
client_secret: secret,
code,
redirect_uri: `${url}oauth/github/callback`,
state,
})
.set('accept', 'application/json');
if (res.body.error) {
throw new UserFacingError(
res.body.error, res.body.error_description, res.body.error_uri,
);
}
const t = res.body.access_token;
const userInfo = await superagent.get(`${endpoint ? `${endpoint}/api` : 'https://api.github.com'}/user`)
.set('User-Agent', 'Hydro-OAuth')
.set('Accept', 'application/vnd.github.v3+json')
.set('Authorization', `token ${t}`);
const ret = {
_id: `${userInfo.body.id}@github.local`,
email: userInfo.body.email,
bio: userInfo.body.bio,
uname: [userInfo.body.name, userInfo.body.login].filter((i) => i),
avatar: `github:${userInfo.body.login}`,
};
if (!ret.email) {
const emailInfo = await superagent.get(`${endpoint ? `${endpoint}/api` : 'https://api.github.com'}/user/emails`)
.set('User-Agent', 'Hydro-OAuth')
.set('Accept', 'application/vnd.github.v3+json')
.set('Authorization', `token ${t}`);
if (emailInfo.body.length) {
ret.email = emailInfo.body.find((e) => e.primary && e.verified).email;
}
}
await TokenModel.del(s._id, TokenModel.TYPE_OAUTH);
if (!ret.email) throw new ForbiddenError("You don't have a verified email.");
return ret;
}
export function apply(ctx: Context) {
ctx.provideModule('oauth', 'github', {
text: 'Login with Github',
callback,
get,
});
ctx.i18n.load('zh', {
'Login With Github': '使用 Github 登录',
}); });
constructor(ctx: Context, config: ReturnType<typeof LoginWithGithubService.Config>) {
super(ctx, 'oauth.github');
ctx.provideModule('oauth', 'github', {
text: 'Login with Github',
callback: async function callback({ state, code }) {
const url = await SystemModel.get('server.url');
const s = await TokenModel.get(state, TokenModel.TYPE_OAUTH);
if (!s) throw new ValidationError('token');
const res = await superagent.post(`${config.endpoint || 'https://github.com'}/login/oauth/access_token`)
.send({
client_id: config.id,
client_secret: config.secret,
code,
redirect_uri: `${url}oauth/github/callback`,
state,
})
.set('accept', 'application/json');
if (res.body.error) {
throw new UserFacingError(
res.body.error, res.body.error_description, res.body.error_uri,
);
}
const t = res.body.access_token;
const userInfo = await superagent.get(`${config.endpoint ? `${config.endpoint}/api` : 'https://api.github.com'}/user`)
.set('User-Agent', 'Hydro-OAuth')
.set('Accept', 'application/vnd.github.v3+json')
.set('Authorization', `token ${t}`);
const ret = {
_id: `${userInfo.body.id}@github.local`,
email: userInfo.body.email,
bio: userInfo.body.bio,
uname: [userInfo.body.name, userInfo.body.login].filter((i) => i),
avatar: `github:${userInfo.body.login}`,
};
if (!ret.email) {
const emailInfo = await superagent.get(`${config.endpoint ? `${config.endpoint}/api` : 'https://api.github.com'}/user/emails`)
.set('User-Agent', 'Hydro-OAuth')
.set('Accept', 'application/vnd.github.v3+json')
.set('Authorization', `token ${t}`);
if (emailInfo.body.length) {
ret.email = emailInfo.body.find((e) => e.primary && e.verified).email;
}
}
await TokenModel.del(s._id, TokenModel.TYPE_OAUTH);
if (!ret.email) throw new ForbiddenError("You don't have a verified email.");
return ret;
},
get: async function get(this: Handler) {
const [state] = await TokenModel.add(TokenModel.TYPE_OAUTH, 600, { redirect: this.request.referer });
// eslint-disable-next-line max-len
this.response.redirect = `https://github.com/login/oauth/authorize?client_id=${config.appid}&state=${state}&scope=read:user,user:email`;
},
});
ctx.i18n.load('zh', {
'Login With Github': '使用 Github 登录',
});
}
} }
-15
View File
@@ -1,15 +0,0 @@
id:
type: text
category: system
desc: Github OAuth AppID
name: id
secret:
type: text
category: system
desc: Github OAuth Secret
name: secret
endpoint:
type: text
category: system
desc: Github Api Endpoint
name: endpoint
+53 -56
View File
@@ -1,24 +1,7 @@
import { import {
Context, Handler, superagent, SystemModel, TokenModel, UserFacingError, Context, Handler, Schema, Service, superagent, SystemModel, TokenModel, UserFacingError,
} from 'hydrooj'; } from 'hydrooj';
declare module 'hydrooj' {
interface SystemKeys {
'login-with-google.id': string,
'login-with-google.secret': string,
}
}
async function get(this: Handler) {
const [appid, url, [state]] = await Promise.all([
SystemModel.get('login-with-google.id'),
SystemModel.get('server.url'),
TokenModel.add(TokenModel.TYPE_OAUTH, 600, { redirect: this.request.referer }),
]);
// eslint-disable-next-line max-len
this.response.redirect = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${appid}&response_type=code&redirect_uri=${url}oauth/google/callback&scope=https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile&state=${state}`;
}
function unescapedString(escapedString: string) { function unescapedString(escapedString: string) {
escapedString += new Array(5 - (escapedString.length % 4)).join('='); escapedString += new Array(5 - (escapedString.length % 4)).join('=');
return escapedString.replace(/-/g, '+').replace(/_/g, '/'); return escapedString.replace(/-/g, '+').replace(/_/g, '/');
@@ -42,43 +25,57 @@ function decodeJWT(idToken: string) {
} }
} }
async function callback(this: Handler, { export default class GoogleOAuthService extends Service {
state, code, error, static Config = Schema.object({
}) { id: Schema.string().description('Google OAuth AppID').required(),
if (error) throw new UserFacingError(error); secret: Schema.string().description('Google OAuth Secret').role('secret').required(),
const [[appid, secret, url], s] = await Promise.all([ proxy: Schema.string().description('Google OAuth Proxy').role('proxy'),
SystemModel.getMany([ });
'login-with-google.id', 'login-with-google.secret', 'server.url',
]),
TokenModel.get(state, TokenModel.TYPE_OAUTH),
]);
const res = await superagent.post('https://oauth2.googleapis.com/token')
.send({
client_id: appid,
client_secret: secret,
code,
grant_type: 'authorization_code',
redirect_uri: `${url}oauth/google/callback`,
});
const payload = decodeJWT(res.body.id_token).payload;
await TokenModel.del(state, TokenModel.TYPE_OAUTH);
this.response.redirect = s.redirect;
return {
// TODO use openid
_id: payload.email,
email: payload.email,
uname: [payload.given_name, payload.name, payload.family_name],
viewLang: payload.locale.replace('-', '_'),
};
}
export function apply(ctx: Context) { constructor(ctx: Context, config: ReturnType<typeof GoogleOAuthService.Config>) {
ctx.provideModule('oauth', 'google', { super(ctx, 'oauth.google');
text: 'Login with Google', ctx.provideModule('oauth', 'google', {
callback, text: 'Login with Google',
get, callback: async function callback(this: Handler, {
}); state, code, error,
ctx.i18n.load('zh', { }) {
'Login With Google': '使用 Google 登录', if (error) throw new UserFacingError(error);
}); const [url, s] = await Promise.all([
SystemModel.get('server.url'),
TokenModel.get(state, TokenModel.TYPE_OAUTH),
]);
const res = await superagent.post('https://oauth2.googleapis.com/token')
.send({
client_id: config.id,
client_secret: config.secret,
code,
grant_type: 'authorization_code',
redirect_uri: `${url}oauth/google/callback`,
});
const payload = decodeJWT(res.body.id_token).payload;
await TokenModel.del(state, TokenModel.TYPE_OAUTH);
this.response.redirect = s.redirect;
return {
// TODO use openid
_id: payload.email,
email: payload.email,
uname: [payload.given_name, payload.name, payload.family_name],
viewLang: payload.locale.replace('-', '_'),
};
},
get: async function get(this: Handler) {
const [appid, url, [state]] = await Promise.all([
config.id,
config.proxy,
TokenModel.add(TokenModel.TYPE_OAUTH, 600, { redirect: this.request.referer }),
]);
const scope = encodeURIComponent('https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile');
// eslint-disable-next-line max-len
this.response.redirect = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${appid}&response_type=code&redirect_uri=${url}oauth/google/callback&scope=${scope}&state=${state}`;
},
});
ctx.i18n.load('zh', {
'Login With Google': '使用 Google 登录',
});
}
} }
-15
View File
@@ -1,15 +0,0 @@
id:
type: text
category: system
desc: Google OAuth AppID
name: id
secret:
type: text
category: system
desc: Google OAuth Secret
name: secret
proxy:
type: text
category: system
desc: Google OAuth Proxy
name: proxy
+21 -14
View File
@@ -1,7 +1,7 @@
import { sign } from 'jsonwebtoken'; import { sign } from 'jsonwebtoken';
import { } from '@hydrooj/ui-default/backendlib/markdown-it-media'; import { } from '@hydrooj/ui-default/backendlib/markdown-it-media';
import { import {
Context, Handler, superagent, SystemModel, UiContextBase, ValidationError, Context, Handler, Schema, superagent, SystemModel, UiContextBase, ValidationError,
} from 'hydrooj'; } from 'hydrooj';
declare module 'hydrooj' { declare module 'hydrooj' {
@@ -72,19 +72,6 @@ class OnlyofficeJWTHandler extends Handler {
} }
export function apply(ctx: Context) { export function apply(ctx: Context) {
Object.defineProperty(UiContextBase, 'onlyofficeApi', {
configurable: true,
enumerable: true,
get() {
return SystemModel.get('onlyoffice.api');
},
});
ctx.on('dispose', () => {
Object.defineProperty(UiContextBase, 'onlyofficeApi', {
enumerable: false,
});
});
ctx.inject(['i18n'], (c) => { ctx.inject(['i18n'], (c) => {
c.i18n.load('en', { c.i18n.load('en', {
'onlyoffice.not_configured': 'Onlyoffice API not configured.', 'onlyoffice.not_configured': 'Onlyoffice API not configured.',
@@ -104,4 +91,24 @@ export function apply(ctx: Context) {
}, },
}); });
} }
ctx.setting.SystemSetting(Schema.object({
onlyoffice: Schema.object({
api: Schema.string().description('OnlyOffice API URL').role('url').default('https://documentserver/web-apps/apps/api/documents/api.js'),
jwtsecret: Schema.string().description('JWT Secret').default('secret'),
pdf: Schema.boolean().description('Handle pdf documents').default(false),
externalSign: Schema.string().description('External Sign URL').default(''),
}),
}));
Object.defineProperty(UiContextBase, 'onlyofficeApi', {
configurable: true,
enumerable: true,
get() {
return SystemModel.get('onlyoffice.api');
},
});
return () => {
Object.defineProperty(UiContextBase, 'onlyofficeApi', {
enumerable: false,
});
};
} }
-20
View File
@@ -1,20 +0,0 @@
api:
type: text
name: api
desc: OnlyOffice API URL
default: https://documentserver/web-apps/apps/api/documents/api.js
jwtsecret:
type: text
name: jwtsecret
desc: JWT Secret
default: secret
pdf:
type: boolean
name: pdf
desc: Handle pdf documents
default: false
externalSign:
type: text
name: externalSign
desc: External Sign URL
default: ''
+1 -1
View File
@@ -100,7 +100,7 @@ export async function apply(ctx: Context) {
}, },
}, },
submissions: tsdocs.flatMap((i) => (i.journal || []).map((j) => { submissions: tsdocs.flatMap((i) => (i.journal || []).map((j) => {
const submit = new ObjectId(j.rid).getTimestamp().getTime(); const submit = new ObjectId(j.rid as string).getTimestamp().getTime();
const curStatus = status[j.status] || 'SYSTEM_ERROR'; const curStatus = status[j.status] || 'SYSTEM_ERROR';
return { return {
problem_id: tdoc.pids.indexOf(j.pid), problem_id: tdoc.pids.indexOf(j.pid),
+150 -75
View File
@@ -1,91 +1,166 @@
import {
Ingest, PushOptions, QueryOptions,
Search, SuggestOptions,
} from 'sonic-channel';
import { import {
Context, iterateAllProblem, iterateAllProblemInDomain, Context, iterateAllProblem, iterateAllProblemInDomain,
Logger, ProblemModel, Schema, SystemModel, Logger, ProblemModel, Schema, Service, SystemModel,
} from 'hydrooj'; } from 'hydrooj';
import { SonicService } from './service';
declare module 'hydrooj' {
interface Context {
sonic?: SonicService;
}
}
const logger = new Logger('sonic'); const logger = new Logger('sonic');
export const inject = { 'sonic': { required: false } }; function getHandler(type: string, that: any) {
export function apply(ctx: Context) { return {
ctx.plugin(SonicService); connected() {
logger.info(`Sonic Channel succeeded to connect to host (${type}).`);
},
disconnected() {
logger.error(`Sonic Channel is now disconnected (${type}).`);
},
timeout() {
logger.error(`Sonic Channel connection timed out (${type}).`);
},
retrying() {
logger.error(`Trying to reconnect to Sonic Channel (${type})...`);
},
error(error) {
that.error = error;
logger.error(`Sonic Channel failed to connect to host (${type}).`, error);
},
};
}
ctx.provideModule('problemSearch', 'sonic', async (domainId, query, opts) => { export class SonicService extends Service {
const limit = opts?.limit || SystemModel.get('pagination.problem'); public search: Search;
let hits = await ctx.sonic.query('problem', `${domainId}@title`, query, { limit }); public ingest: Ingest;
if (!opts.skip) { public error = '';
let pdoc = await ProblemModel.get(domainId, +query || query, ProblemModel.PROJECTION_LIST);
if (pdoc) { static Config = Schema.object({
hits = hits.filter((i) => i !== `${pdoc.domainId}/${pdoc.docId}`); host: Schema.string().default('::1'),
hits.unshift(`${pdoc.domainId}/${pdoc.docId}`); port: Schema.number().default(1491),
} else if (/^P\d+$/.test(query)) { auth: Schema.string().default(''),
pdoc = await ProblemModel.get(domainId, +query.substring(1), ProblemModel.PROJECTION_LIST);
if (pdoc) hits.unshift(`${pdoc.domainId}/${pdoc.docId}`);
}
}
if (limit - hits.length > 0) hits.push(...await ctx.sonic.query('problem', `${domainId}@content`, query, { limit: limit - hits.length }));
return {
countRelation: hits.length >= limit ? 'gte' : 'eq',
total: hits.length,
hits,
};
}); });
async function run({ domainId }, report) { constructor(ctx: Context, private config: ReturnType<typeof SonicService.Config>) {
if (domainId) await ctx.sonic.flushb('problem', domainId); super(ctx, 'sonic');
else await ctx.sonic.flushc('problem');
let i = 0;
const cb = async (pdoc) => {
i++;
if (!(i % 1000)) report({ message: `${i} problems indexed` });
await Promise.all([
pdoc.title && ctx.sonic.push(
'problem', `${pdoc.domainid}@title`, `${pdoc.domainId}/${pdoc.docId}`,
`${pdoc.pid || ''} ${pdoc.title} ${pdoc.tag.join(' ')}`,
),
pdoc.content.toString()
&& ctx.sonic.push('problem', `${pdoc.domainId}@content`, `${pdoc.domainId}/${pdoc.docId}`, pdoc.content.toString()),
]).catch((e) => console.log(`${pdoc.domainId}/${pdoc.docId}`, e));
};
if (domainId) await iterateAllProblemInDomain(domainId, ['title', 'content'], cb);
else await iterateAllProblem(['title', 'content', 'tag'], cb);
return true;
} }
ctx.on('problem/add', async (doc, docId) => { *[Context.init]() {
Promise.all([ this.search = new Search(this.config);
ctx.sonic.push('problem', `${doc.domainId}@title`, `${doc.domainId}/${docId}`, `${doc.pid || ''} ${doc.title} ${doc.tag?.join(' ')}`), this.ingest = new Ingest(this.config);
ctx.sonic.push('problem', `${doc.domainId}@content`, `${doc.domainId}/${docId}`, doc.content.toString()), this.search.connect(getHandler('search', this));
]).catch((e) => logger.error(e)); yield () => this.search.close();
}); this.ingest.connect(getHandler('ingest', this));
yield () => this.ingest.close();
yield this.ctx.on('problem/add', async (doc, docId) => {
Promise.all([
this.push('problem', `${doc.domainId}@title`, `${doc.domainId}/${docId}`, `${doc.pid || ''} ${doc.title} ${doc.tag?.join(' ')}`),
this.push('problem', `${doc.domainId}@content`, `${doc.domainId}/${docId}`, doc.content.toString()),
]).catch((e) => logger.error(e));
});
yield this.ctx.on('problem/edit', async (pdoc) => {
const id = `${pdoc.domainId}/${pdoc.docId}`;
Promise.all([
this.flusho('problem', `${pdoc.domainId}@title`, id)
.then(() => this.push('problem', `${pdoc.domainId}@title`, id, `${pdoc.pid || ''} ${pdoc.title} ${pdoc.tag?.join(' ')}`)),
this.flusho('problem', `${pdoc.domainId}@content`, id)
.then(() => this.push('problem', `${pdoc.domainId}@content`, id, pdoc.content.toString())),
]).catch((e) => logger.error(e));
});
yield this.ctx.on('problem/del', async (domainId, docId) => {
const id = `${domainId}/${docId}`;
await Promise.all([
this.flusho('problem', `${domainId}@title`, id),
this.flusho('problem', `${domainId}@content`, id),
]);
});
yield this.ctx.provideModule('problemSearch', 'sonic', async (domainId, query, opts) => {
const limit = opts?.limit || SystemModel.get('pagination.problem');
let hits = await this.query('problem', `${domainId}@title`, query, { limit });
if (!opts.skip) {
let pdoc = await ProblemModel.get(domainId, +query || query, ProblemModel.PROJECTION_LIST);
if (pdoc) {
hits = hits.filter((i) => i !== `${pdoc.domainId}/${pdoc.docId}`);
hits.unshift(`${pdoc.domainId}/${pdoc.docId}`);
} else if (/^P\d+$/.test(query)) {
pdoc = await ProblemModel.get(domainId, +query.substring(1), ProblemModel.PROJECTION_LIST);
if (pdoc) hits.unshift(`${pdoc.domainId}/${pdoc.docId}`);
}
}
if (limit - hits.length > 0) hits.push(...await this.query('problem', `${domainId}@content`, query, { limit: limit - hits.length }));
return {
countRelation: hits.length >= limit ? 'gte' : 'eq',
total: hits.length,
hits,
};
});
yield this.ctx.addScript(
'ensureSonicSearch', 'Sonic problem search re-index',
Schema.object({
domainId: Schema.string(),
}),
async ({ domainId }, report) => {
if (domainId) await this.flushb('problem', domainId);
else await this.flushc('problem');
let i = 0;
const cb = async (pdoc) => {
i++;
if (!(i % 1000)) report({ message: `${i} problems indexed` });
await Promise.all([
pdoc.title && this.push(
'problem', `${pdoc.domainid}@title`, `${pdoc.domainId}/${pdoc.docId}`,
`${pdoc.pid || ''} ${pdoc.title} ${pdoc.tag.join(' ')}`,
),
pdoc.content.toString()
&& this.push('problem', `${pdoc.domainId}@content`, `${pdoc.domainId}/${pdoc.docId}`, pdoc.content.toString()),
]).catch((e) => console.log(`${pdoc.domainId}/${pdoc.docId}`, e));
};
if (domainId) await iterateAllProblemInDomain(domainId, ['title', 'content'], cb);
else await iterateAllProblem(['title', 'content', 'tag'], cb);
return true;
},
);
ctx.on('problem/edit', async (pdoc) => { this.ctx.i18n.load('zh', {
const id = `${pdoc.domainId}/${pdoc.docId}`; 'Sonic problem search re-index': '重建题目搜索索引。',
Promise.all([ });
ctx.sonic.flusho('problem', `${pdoc.domainId}@title`, id) }
.then(() => ctx.sonic.push('problem', `${pdoc.domainId}@title`, id, `${pdoc.pid || ''} ${pdoc.title} ${pdoc.tag?.join(' ')}`)),
ctx.sonic.flusho('problem', `${pdoc.domainId}@content`, id)
.then(() => ctx.sonic.push('problem', `${pdoc.domainId}@content`, id, pdoc.content.toString())),
]).catch((e) => logger.error(e));
});
ctx.on('problem/del', async (domainId, docId) => { async query(collection: string, bucket: string, terms: string, options?: QueryOptions) {
const id = `${domainId}/${docId}`; return await this.search.query(collection, bucket, terms, options);
await Promise.all([ }
ctx.sonic.flusho('problem', `${domainId}@title`, id),
ctx.sonic.flusho('problem', `${domainId}@content`, id),
]);
});
ctx.addScript( async suggest(collection: string, bucket: string, word: string, options?: SuggestOptions) {
'ensureSonicSearch', 'Sonic problem search re-index', return await this.search.suggest(collection, bucket, word, options);
Schema.object({ }
domainId: Schema.string(),
}),
run,
);
ctx.i18n.load('zh', { async push(collection: string, bucket: string, object: string, text: string, options?: PushOptions) {
'Sonic problem search re-index': '重建题目搜索索引。', return await this.ingest.push(collection, bucket, object, text, options);
}); }
async pop(collection: string, bucket: string, object: string, text: string) {
return await this.ingest.pop(collection, bucket, object, text);
}
async count(collection: string, bucket?: string, object?: string) {
return await this.ingest.count(collection, bucket, object);
}
async flusho(collection: string, bucket: string, object: string) {
return await this.ingest.flusho(collection, bucket, object);
}
async flushb(collection: string, bucket: string) {
return await this.ingest.flushb(collection, bucket);
}
async flushc(collection: string) {
return await this.ingest.flushc(collection);
}
} }
-109
View File
@@ -1,109 +0,0 @@
import {
Ingest, PushOptions, QueryOptions,
Search, SuggestOptions,
} from 'sonic-channel';
import {
Context, Logger, Service, SystemModel,
} from 'hydrooj';
declare module 'hydrooj' {
interface SystemKeys {
'sonic.host': string;
'sonic.port': number;
'sonic.auth': string;
}
interface Context {
sonic?: SonicService;
}
}
const logger = new Logger('sonic');
function getHandler(type: string, that: any) {
return {
connected() {
logger.info(`Sonic Channel succeeded to connect to host (${type}).`);
},
disconnected() {
logger.error(`Sonic Channel is now disconnected (${type}).`);
},
timeout() {
logger.error(`Sonic Channel connection timed out (${type}).`);
},
retrying() {
logger.error(`Trying to reconnect to Sonic Channel (${type})...`);
},
error(error) {
that.error = error;
logger.error(`Sonic Channel failed to connect to host (${type}).`, error);
},
};
}
export class SonicService extends Service {
public search: Search;
public ingest: Ingest;
public error = '';
constructor(ctx: Context) {
super(ctx, 'sonic');
}
async start() {
const [host, port, auth] = SystemModel.getMany(['sonic.host', 'sonic.port', 'sonic.auth']);
const cfg = {
host: host || '::1',
port: port || 1491,
auth: auth || '',
};
this.search = new Search(cfg);
this.ingest = new Ingest(cfg);
try {
this.search.connect(getHandler('search', this));
this.ingest.connect(getHandler('ingest', this));
} catch (e) {
logger.warn('Sonic init fail. will retry later.');
this.error = e.toString();
setTimeout(() => this.start(), 10000);
}
}
async query(collection: string, bucket: string, terms: string, options?: QueryOptions) {
return await this.search.query(collection, bucket, terms, options);
}
async suggest(collection: string, bucket: string, word: string, options?: SuggestOptions) {
return await this.search.suggest(collection, bucket, word, options);
}
async push(collection: string, bucket: string, object: string, text: string, options?: PushOptions) {
return await this.ingest.push(collection, bucket, object, text, options);
}
async pop(collection: string, bucket: string, object: string, text: string) {
return await this.ingest.pop(collection, bucket, object, text);
}
async count(collection: string, bucket?: string, object?: string) {
return await this.ingest.count(collection, bucket, object);
}
async flusho(collection: string, bucket: string, object: string) {
return await this.ingest.flusho(collection, bucket, object);
}
async flushb(collection: string, bucket: string) {
return await this.ingest.flushb(collection, bucket);
}
async flushc(collection: string) {
return await this.ingest.flushc(collection);
}
async stop() {
await Promise.all([
this.search.close(),
this.ingest.close(),
]);
}
}
-8
View File
@@ -1,8 +0,0 @@
host:
value: ::1
port:
type: number
value: 1491
auth:
type: text
value: SecretPassword