mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
config: remove all setting.yaml
This commit is contained in:
16 files changed
+285
-386
No files matched your search
@@ -6,24 +6,31 @@ import { fs, yaml } from '@hydrooj/utils';
|
||||
|
||||
const argv = cac().parse();
|
||||
|
||||
const JudgeSettings = Schema.object({
|
||||
cache_dir: Schema.string().default(path.resolve(os.homedir(), '.cache', 'hydro', 'judge')),
|
||||
const defaultEnv = `\
|
||||
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')),
|
||||
stdio_size: Schema.string().pattern(/^\d+[kmg]b?$/g).default('32m'),
|
||||
memoryMax: Schema.string().pattern(/^\d+[kmg]b?$/g).default('512m'),
|
||||
strict_memory: Schema.boolean().default(false),
|
||||
sandbox_host: Schema.string().role('link').default('http://localhost:5050'),
|
||||
testcases_max: Schema.number().default(100),
|
||||
total_time_limit: Schema.number().default(60),
|
||||
processLimit: Schema.number().default(32),
|
||||
parallelism: Schema.number().default(2),
|
||||
strict_memory: Schema.boolean().default(false).description('Use address space memory limit'),
|
||||
sandbox_host: Schema.string().role('url').default('http://localhost:5050'),
|
||||
testcases_max: Schema.number().default(100).min(1).step(1),
|
||||
total_time_limit: Schema.number().default(60).min(1),
|
||||
processLimit: Schema.number().default(32).min(1).step(1),
|
||||
parallelism: Schema.number().default(2).min(1).step(1),
|
||||
concurrency: Schema.number(),
|
||||
singleTaskParallelism: Schema.number().default(2),
|
||||
rerun: Schema.number().description('Re-Run testcase if time-limit-exceeded (max per submission)').default(0),
|
||||
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).min(0).step(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(),
|
||||
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),
|
||||
detail: Schema.boolean().description('Show diff detail').default(true),
|
||||
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 newPath = path.resolve(os.homedir(), '.hydro', 'judge.yaml');
|
||||
|
||||
const config = global.Hydro
|
||||
let config = global.Hydro
|
||||
? JudgeSettings({})
|
||||
: (() => {
|
||||
const base: any = {};
|
||||
@@ -54,6 +61,8 @@ const config = global.Hydro
|
||||
return JudgeSettings(cfg);
|
||||
})();
|
||||
|
||||
export const getConfig: <K extends keyof typeof config>(key: K) => typeof config[K] = global.Hydro
|
||||
? (key) => global.Hydro.model.system.get(`hydrojudge.${key}`) ?? config[key]
|
||||
: (key) => config[key];
|
||||
export function overrideConfig(update: ReturnType<typeof JudgeSettings>) {
|
||||
config = JudgeSettings(update);
|
||||
}
|
||||
|
||||
export const getConfig: <K extends keyof typeof config>(key: K) => typeof config[K] = (key) => config[key];
|
||||
@@ -4,7 +4,7 @@ import path from 'path';
|
||||
import { fs } from '@hydrooj/utils';
|
||||
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
|
||||
import {
|
||||
db, JudgeHandler, JudgeResultBody, ObjectId, RecordModel,
|
||||
Context as HydroContext, db, JudgeHandler, JudgeResultBody, ObjectId, RecordModel,
|
||||
SettingModel, StorageModel, SystemModel, TaskModel,
|
||||
} from 'hydrooj';
|
||||
import { langs } from 'hydrooj/src/model/setting';
|
||||
@@ -59,10 +59,11 @@ const session: Session = {
|
||||
},
|
||||
};
|
||||
|
||||
export async function postInit(ctx) {
|
||||
if (SystemModel.get('hydrojudge.disable')) return;
|
||||
export async function* apply(ctx: HydroContext) {
|
||||
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'));
|
||||
const info = await sysinfo.get();
|
||||
@@ -76,6 +77,7 @@ export async function postInit(ctx) {
|
||||
};
|
||||
const parallelism = Math.max(getConfig('parallelism'), 2);
|
||||
const taskConsumer = TaskModel.consume({ type: 'judge' }, handle, true, parallelism);
|
||||
yield () => taskConsumer.destroy();
|
||||
async function collectInfo() {
|
||||
const coll = db.collection('status');
|
||||
const compilers = await compilerVersions(langs);
|
||||
@@ -91,11 +93,11 @@ export async function postInit(ctx) {
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
ctx.on('system/setting', () => {
|
||||
yield ctx.on('system/setting', () => {
|
||||
taskConsumer.setConcurrency(Math.max(getConfig('parallelism'), 2));
|
||||
collectInfo();
|
||||
});
|
||||
collectInfo();
|
||||
TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle);
|
||||
TaskModel.consume({ type: 'generate' }, handle);
|
||||
const generateConsumer = TaskModel.consume({ type: 'generate' }, handle);
|
||||
yield () => generateConsumer.destroy();
|
||||
}
|
||||
@@ -1,20 +1,11 @@
|
||||
import { Context } from 'hydrooj';
|
||||
import { JudgeSettings, overrideConfig } from './config';
|
||||
|
||||
declare module 'hydrooj' {
|
||||
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 const Config = JudgeSettings;
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
export function apply(ctx: Context, config: ReturnType<typeof Config>) {
|
||||
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);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import type { DomainDoc, GeoIP, ModuleInterfaces } from './interface';
|
||||
import { inject } from './lib/ui';
|
||||
import { Loader } from './loader';
|
||||
import type { EventMap } from './service/bus';
|
||||
import type { CheckService } from './service/check';
|
||||
import type CheckService from './service/check';
|
||||
import type { } from './service/migration';
|
||||
|
||||
// TODO: this is an broken declaration
|
||||
|
||||
@@ -13,7 +13,6 @@ import record from '../model/record';
|
||||
import * as setting from '../model/setting';
|
||||
import * as system from '../model/system';
|
||||
import user from '../model/user';
|
||||
import { check } from '../service/check';
|
||||
import {
|
||||
ConnectionHandler, Handler, param, requireSudo, Types,
|
||||
} from '../service/server';
|
||||
@@ -70,11 +69,11 @@ class SystemCheckConnHandler extends ConnectionHandler {
|
||||
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.run(this, log, warn, error, (id) => { this.id = id; });
|
||||
await this.ctx.check.run(this, log, warn, error, (id) => { this.id = id; });
|
||||
}
|
||||
|
||||
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) {
|
||||
ctx.Route('manage', '/manage', SystemMainHandler);
|
||||
ctx.Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler);
|
||||
|
||||
@@ -7,11 +7,49 @@ import * as system from '../model/system';
|
||||
import db from './db';
|
||||
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> = {};
|
||||
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) {
|
||||
this.ctx.effect(() => {
|
||||
this.checkers[`check${type}`] = checkFunc;
|
||||
@@ -38,43 +76,3 @@ export class CheckService extends Service {
|
||||
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;
|
||||
}
|
||||
@@ -1,39 +1,27 @@
|
||||
import {
|
||||
Context, ForbiddenError, Handler, superagent, SystemModel,
|
||||
Context, ForbiddenError, Handler, Schema, Service, superagent, SystemModel,
|
||||
TokenModel, UserFacingError, ValidationError,
|
||||
} from 'hydrooj';
|
||||
|
||||
declare module 'hydrooj' {
|
||||
interface SystemKeys {
|
||||
'login-with-github.id': string;
|
||||
'login-with-github.secret': string;
|
||||
'login-with-github.endpoint': string;
|
||||
}
|
||||
}
|
||||
export default class LoginWithGithubService extends Service {
|
||||
static Config = Schema.object({
|
||||
id: Schema.string().description('Github OAuth AppID').required(),
|
||||
secret: Schema.string().description('Github OAuth Secret').role('secret').required(),
|
||||
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),
|
||||
]);
|
||||
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(`${endpoint || 'https://github.com'}/login/oauth/access_token`)
|
||||
const res = await superagent.post(`${config.endpoint || 'https://github.com'}/login/oauth/access_token`)
|
||||
.send({
|
||||
client_id: appid,
|
||||
client_secret: secret,
|
||||
client_id: config.id,
|
||||
client_secret: config.secret,
|
||||
code,
|
||||
redirect_uri: `${url}oauth/github/callback`,
|
||||
state,
|
||||
@@ -45,7 +33,7 @@ async function callback({ state, code }) {
|
||||
);
|
||||
}
|
||||
const t = res.body.access_token;
|
||||
const userInfo = await superagent.get(`${endpoint ? `${endpoint}/api` : 'https://api.github.com'}/user`)
|
||||
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}`);
|
||||
@@ -57,7 +45,7 @@ async function callback({ state, code }) {
|
||||
avatar: `github:${userInfo.body.login}`,
|
||||
};
|
||||
if (!ret.email) {
|
||||
const emailInfo = await superagent.get(`${endpoint ? `${endpoint}/api` : 'https://api.github.com'}/user/emails`)
|
||||
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}`);
|
||||
@@ -68,15 +56,15 @@ async function callback({ state, code }) {
|
||||
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,
|
||||
},
|
||||
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 登录',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,24 +1,7 @@
|
||||
import {
|
||||
Context, Handler, superagent, SystemModel, TokenModel, UserFacingError,
|
||||
Context, Handler, Schema, Service, superagent, SystemModel, TokenModel, UserFacingError,
|
||||
} 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) {
|
||||
escapedString += new Array(5 - (escapedString.length % 4)).join('=');
|
||||
return escapedString.replace(/-/g, '+').replace(/_/g, '/');
|
||||
@@ -42,20 +25,29 @@ function decodeJWT(idToken: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function callback(this: Handler, {
|
||||
export default class GoogleOAuthService extends Service {
|
||||
static Config = Schema.object({
|
||||
id: Schema.string().description('Google OAuth AppID').required(),
|
||||
secret: Schema.string().description('Google OAuth Secret').role('secret').required(),
|
||||
proxy: Schema.string().description('Google OAuth Proxy').role('proxy'),
|
||||
});
|
||||
|
||||
constructor(ctx: Context, config: ReturnType<typeof GoogleOAuthService.Config>) {
|
||||
super(ctx, 'oauth.google');
|
||||
ctx.provideModule('oauth', 'google', {
|
||||
text: 'Login with Google',
|
||||
callback: async function callback(this: Handler, {
|
||||
state, code, error,
|
||||
}) {
|
||||
}) {
|
||||
if (error) throw new UserFacingError(error);
|
||||
const [[appid, secret, url], s] = await Promise.all([
|
||||
SystemModel.getMany([
|
||||
'login-with-google.id', 'login-with-google.secret', 'server.url',
|
||||
]),
|
||||
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: appid,
|
||||
client_secret: secret,
|
||||
client_id: config.id,
|
||||
client_secret: config.secret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: `${url}oauth/google/callback`,
|
||||
@@ -70,15 +62,20 @@ async function callback(this: Handler, {
|
||||
uname: [payload.given_name, payload.name, payload.family_name],
|
||||
viewLang: payload.locale.replace('-', '_'),
|
||||
};
|
||||
}
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.provideModule('oauth', 'google', {
|
||||
text: 'Login with Google',
|
||||
callback,
|
||||
get,
|
||||
},
|
||||
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 登录',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,7 +1,7 @@
|
||||
import { sign } from 'jsonwebtoken';
|
||||
import { } from '@hydrooj/ui-default/backendlib/markdown-it-media';
|
||||
import {
|
||||
Context, Handler, superagent, SystemModel, UiContextBase, ValidationError,
|
||||
Context, Handler, Schema, superagent, SystemModel, UiContextBase, ValidationError,
|
||||
} from 'hydrooj';
|
||||
|
||||
declare module 'hydrooj' {
|
||||
@@ -72,19 +72,6 @@ class OnlyofficeJWTHandler extends Handler {
|
||||
}
|
||||
|
||||
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) => {
|
||||
c.i18n.load('en', {
|
||||
'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,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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: ''
|
||||
@@ -100,7 +100,7 @@ export async function apply(ctx: Context) {
|
||||
},
|
||||
},
|
||||
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';
|
||||
return {
|
||||
problem_id: tdoc.pids.indexOf(j.pid),
|
||||
|
||||
+123
-48
@@ -1,18 +1,88 @@
|
||||
import {
|
||||
Ingest, PushOptions, QueryOptions,
|
||||
Search, SuggestOptions,
|
||||
} from 'sonic-channel';
|
||||
import {
|
||||
Context, iterateAllProblem, iterateAllProblemInDomain,
|
||||
Logger, ProblemModel, Schema, SystemModel,
|
||||
Logger, ProblemModel, Schema, Service, SystemModel,
|
||||
} from 'hydrooj';
|
||||
import { SonicService } from './service';
|
||||
|
||||
declare module 'hydrooj' {
|
||||
interface Context {
|
||||
sonic?: SonicService;
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger('sonic');
|
||||
|
||||
export const inject = { 'sonic': { required: false } };
|
||||
export function apply(ctx: Context) {
|
||||
ctx.plugin(SonicService);
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ctx.provideModule('problemSearch', 'sonic', async (domainId, query, opts) => {
|
||||
export class SonicService extends Service {
|
||||
public search: Search;
|
||||
public ingest: Ingest;
|
||||
public error = '';
|
||||
|
||||
static Config = Schema.object({
|
||||
host: Schema.string().default('::1'),
|
||||
port: Schema.number().default(1491),
|
||||
auth: Schema.string().default(''),
|
||||
});
|
||||
|
||||
constructor(ctx: Context, private config: ReturnType<typeof SonicService.Config>) {
|
||||
super(ctx, 'sonic');
|
||||
}
|
||||
|
||||
*[Context.init]() {
|
||||
this.search = new Search(this.config);
|
||||
this.ingest = new Ingest(this.config);
|
||||
this.search.connect(getHandler('search', this));
|
||||
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 ctx.sonic.query('problem', `${domainId}@title`, query, { limit });
|
||||
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) {
|
||||
@@ -23,69 +93,74 @@ export function apply(ctx: Context) {
|
||||
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 }));
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
async function run({ domainId }, report) {
|
||||
if (domainId) await ctx.sonic.flushb('problem', domainId);
|
||||
else await ctx.sonic.flushc('problem');
|
||||
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 && ctx.sonic.push(
|
||||
pdoc.title && this.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()),
|
||||
&& 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/add', async (doc, docId) => {
|
||||
Promise.all([
|
||||
ctx.sonic.push('problem', `${doc.domainId}@title`, `${doc.domainId}/${docId}`, `${doc.pid || ''} ${doc.title} ${doc.tag?.join(' ')}`),
|
||||
ctx.sonic.push('problem', `${doc.domainId}@content`, `${doc.domainId}/${docId}`, doc.content.toString()),
|
||||
]).catch((e) => logger.error(e));
|
||||
});
|
||||
|
||||
ctx.on('problem/edit', async (pdoc) => {
|
||||
const id = `${pdoc.domainId}/${pdoc.docId}`;
|
||||
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) => {
|
||||
const id = `${domainId}/${docId}`;
|
||||
await Promise.all([
|
||||
ctx.sonic.flusho('problem', `${domainId}@title`, id),
|
||||
ctx.sonic.flusho('problem', `${domainId}@content`, id),
|
||||
]);
|
||||
});
|
||||
|
||||
ctx.addScript(
|
||||
'ensureSonicSearch', 'Sonic problem search re-index',
|
||||
Schema.object({
|
||||
domainId: Schema.string(),
|
||||
}),
|
||||
run,
|
||||
},
|
||||
);
|
||||
|
||||
ctx.i18n.load('zh', {
|
||||
this.ctx.i18n.load('zh', {
|
||||
'Sonic problem search re-index': '重建题目搜索索引。',
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
host:
|
||||
value: ::1
|
||||
port:
|
||||
type: number
|
||||
value: 1491
|
||||
auth:
|
||||
type: text
|
||||
value: SecretPassword
|
||||
Reference in new issue
Block a user