diff --git a/framework/framework/validator.ts b/framework/framework/validator.ts index 2dcf7149d..d66f5f23a 100644 --- a/framework/framework/validator.ts +++ b/framework/framework/validator.ts @@ -82,7 +82,7 @@ export const Types: Types = { UidOrName: saslprepString(/^(.{3,31}|[\u4e00-\u9fa5]{2}|-?[0-9]+)$/), Username: saslprepString(/^(.{3,31}|[\u4e00-\u9fa5]{2})$/), Password: basicString(/^.{6,255}$/), - ProblemId: saslprepString(/^[a-zA-Z0-9]+$/i, () => true, (s) => (Number.isSafeInteger(+s) ? +s : s)), + ProblemId: saslprepString(/^([a-zA-Z0-9]{1,10}-)?[a-zA-Z0-9]+$/i, () => true, (s) => (Number.isSafeInteger(+s) ? +s : s)), Email: saslprepString(/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i), DomainId: saslprepString(/^[a-zA-Z][a-zA-Z0-9_]{3,31}$/), Role: saslprepString(/^[_0-9A-Za-z\u4e00-\u9fa5]{1,31}$/i), diff --git a/packages/elastic/index.ts b/packages/elastic/index.ts index d33f89b55..60d51d6ec 100644 --- a/packages/elastic/index.ts +++ b/packages/elastic/index.ts @@ -1,6 +1,6 @@ import { Client } from '@elastic/elasticsearch'; import { - _, Context, DomainModel, iterateAllProblem, iterateAllProblemInDomain, + _, Context, iterateAllProblem, iterateAllProblemInDomain, ProblemDoc, ProblemModel, Schema, SystemModel, } from 'hydrooj'; @@ -19,8 +19,6 @@ global.Hydro.lib.problemSearch = async (domainId, q, opts) => { const allowedSize = SystemModel.get('elasic-search.indexSize') || 10000; const size = opts?.limit || SystemModel.get('pagination.problem'); const from = Math.min(allowedSize - size, opts?.skip || 0); - const union = await DomainModel.get(domainId); - const domainIds = [domainId, ...(union?.union || [])]; const res = await client.search({ index: 'problem', size, @@ -34,7 +32,7 @@ global.Hydro.lib.problemSearch = async (domainId, q, opts) => { post_filter: { bool: { minimum_should_match: 1, - should: domainIds.map((i) => ({ match: { domainId: i } })), + should: [{ match: { domainId } }], }, }, }); diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index fb8844b58..7d5275c34 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -142,10 +142,9 @@ export class ProblemMainHandler extends Handler { // eslint-disable-next-line prefer-const let [pdocs, ppcount, pcount] = this.queryContext.fail ? [[], 0, 0] - : await problem.list( - domainId, query, sort.length ? 1 : page, limit, - quick ? ['title', 'pid', 'domainId', 'docId'] : undefined, - this.user._id, + : await this.paginate( + problem.getMulti(domainId, query, quick ? ['title', 'pid', 'domainId', 'docId'] : undefined).sort({ sort: 1, docId: 1 }), + sort.length ? 1 : page, limit, ); if (total) { pcount = total; diff --git a/packages/hydrooj/src/handler/user.ts b/packages/hydrooj/src/handler/user.ts index c608ad9b8..cebb40d91 100644 --- a/packages/hydrooj/src/handler/user.ts +++ b/packages/hydrooj/src/handler/user.ts @@ -358,25 +358,22 @@ class UserDetailHandler extends Handler { async get(domainId: string, uid: number) { if (uid === 0) throw new UserNotFoundError(0); const isSelfProfile = this.user._id === uid; - const [udoc, sdoc, union] = await Promise.all([ + const [udoc, sdoc] = await Promise.all([ user.getById(domainId, uid), token.getMostRecentSessionByUid(uid, ['createAt', 'updateAt']), - domain.get(domainId), ]); if (!udoc) throw new UserNotFoundError(uid); const pdocs: ProblemDoc[] = []; const acInfo: Record = {}; const canViewHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id; if (this.user.hasPerm(PERM.PERM_VIEW_PROBLEM)) { - await Promise.all([domainId, ...(union?.union || [])].map(async (did) => { - const psdocs = await problem.getMultiStatus(did, { uid, status: STATUS.STATUS_ACCEPTED }).toArray(); - pdocs.push(...Object.values( - await problem.getList( - did, psdocs.map((i) => i.docId), canViewHidden, - false, problem.PROJECTION_LIST, true, - ), - )); - })); + const psdocs = await problem.getMultiStatus(domainId, { uid, status: STATUS.STATUS_ACCEPTED }).toArray(); + pdocs.push(...Object.values( + await problem.getList( + domainId, psdocs.map((i) => i.docId), canViewHidden, + false, problem.PROJECTION_LIST, true, + ), + )); } for (const pdoc of pdocs) { for (const tag of pdoc.tag) { diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index b805a4cad..2c80800d9 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -16,6 +16,7 @@ import type { } from '../interface'; import { parseConfig } from '../lib/testdataConfig'; import * as bus from '../service/bus'; +import db from '../service/db'; import { ArrayKeys, MaybeArray, NumberKeys, Projection, } from '../typeutils'; @@ -27,14 +28,15 @@ import RecordModel from './record'; import SolutionModel from './solution'; import storage from './storage'; import * as SystemModel from './system'; -import user from './user'; export interface ProblemDoc extends Document { } export type Field = keyof ProblemDoc; const logger = new Logger('problem'); -function sortable(source: string) { - return source.replace(/(\d+)/g, (str) => (str.length >= 6 ? str : ('0'.repeat(6 - str.length) + str))); +function sortable(source: string, namespaces: Record) { + const [namespace, pid] = source.includes('-') ? source.split('-') : ['default', source]; + return ((namespaces ? `${namespaces[namespace]}-` : '') + pid) + .replace(/(\d+)/g, (str) => (str.length >= 6 ? str : ('0'.repeat(6 - str.length) + str))); } function findOverrideContent(dir: string, base: string) { @@ -149,8 +151,9 @@ export class ProblemModel { content: string, owner: number, tag: string[] = [], meta: ProblemCreateOptions = {}, ) { + const ddoc = await DomainModel.get(domainId); const args: Partial = { - title, tag, hidden: meta.hidden || false, nSubmit: 0, nAccept: 0, sort: sortable(pid || `P${docId}`), + title, tag, hidden: meta.hidden || false, nSubmit: 0, nAccept: 0, sort: sortable(pid || `P${docId}`, ddoc?.namespaces), }; if (pid) args.pid = pid; if (meta.difficulty) args.difficulty = meta.difficulty; @@ -171,9 +174,10 @@ export class ProblemModel { rawConfig = false, ): Promise { if (Number.isSafeInteger(+pid)) pid = +pid; + const ddoc = await DomainModel.get(domainId); const res = typeof pid === 'number' ? await document.get(domainId, document.TYPE_PROBLEM, pid, projection) - : (await document.getMulti(domainId, document.TYPE_PROBLEM, { sort: sortable(pid), pid }) + : (await document.getMulti(domainId, document.TYPE_PROBLEM, { sort: sortable(pid, ddoc?.namespaces), pid }) .project(buildProjection(projection)).limit(1).toArray())[0]; if (!res) return null; try { @@ -188,33 +192,16 @@ export class ProblemModel { return document.getMulti(domainId, document.TYPE_PROBLEM, query, projection).sort({ sort: 1 }); } + /** @deprecated */ static async list( domainId: string, query: Filter, page: number, pageSize: number, - projection = ProblemModel.PROJECTION_LIST, uid?: number, + projection = ProblemModel.PROJECTION_LIST, ): Promise<[ProblemDoc[], number, number]> { - const union = await DomainModel.get(domainId); - const domainIds = [domainId, ...(union.union || [])]; - let count = 0; - const pdocs = []; - for (const id of domainIds) { - // TODO enhance performance - if (typeof uid === 'number') { - // eslint-disable-next-line no-await-in-loop - const udoc = await user.getById(id, uid); - if (!udoc.hasPerm(PERM.PERM_VIEW_PROBLEM)) continue; - } - // eslint-disable-next-line no-await-in-loop - const ccount = await document.count(id, document.TYPE_PROBLEM, query); - if (pdocs.length < pageSize && (page - 1) * pageSize - count <= ccount) { - // eslint-disable-next-line no-await-in-loop - pdocs.push(...await document.getMulti(id, document.TYPE_PROBLEM, query, projection) - .sort({ sort: 1, docId: 1 }) - .skip(Math.max((page - 1) * pageSize - count, 0)).limit(pageSize - pdocs.length).toArray()); - } - count += ccount; - } - return [pdocs, Math.ceil(count / pageSize), count]; + return await db.paginate( + document.getMulti(domainId, document.TYPE_PROBLEM, query, projection).sort({ sort: 1, docId: 1 }), + page, pageSize, + ); } static getStatus(domainId: string, docId: number, uid: number) { @@ -227,11 +214,12 @@ export class ProblemModel { static async edit(domainId: string, _id: number, $set: Partial): Promise { const delpid = $set.pid === ''; + const ddoc = await DomainModel.get(domainId); if (delpid) { delete $set.pid; - $set.sort = sortable(`P${_id}`); + $set.sort = sortable(`P${_id}`, ddoc.namespaces); } else if ($set.pid) { - $set.sort = sortable($set.pid); + $set.sort = sortable($set.pid, ddoc.namespaces); } await bus.parallel('problem/before-edit', $set); const result = await document.set(domainId, document.TYPE_PROBLEM, _id, $set, delpid ? { pid: '' } : undefined); diff --git a/packages/hydrooj/src/model/storage.ts b/packages/hydrooj/src/model/storage.ts index dfa8afd1a..4779584ce 100644 --- a/packages/hydrooj/src/model/storage.ts +++ b/packages/hydrooj/src/model/storage.ts @@ -123,6 +123,19 @@ export class StorageModel { return await storage.signDownloadLink(res.value?.link || res.value?._id || target, filename, noExpire, useAlternativeEndpointFor); } + static async move(src: string, dst: string) { + const res = await StorageModel.coll.findOneAndUpdate( + { path: src, autoDelete: null }, + { $set: { path: dst } }, + ); + return !!res.value; + } + + static async exists(path: string) { + const value = await StorageModel.coll.findOne({ path, autoDelete: null }); + return !!value; + } + static async copy(src: string, dst: string) { const { value } = await StorageModel.coll.findOneAndUpdate( { path: src, autoDelete: null }, diff --git a/packages/hydrooj/src/script/rating.ts b/packages/hydrooj/src/script/rating.ts index 60bd95c0d..2ec3fa0af 100644 --- a/packages/hydrooj/src/script/rating.ts +++ b/packages/hydrooj/src/script/rating.ts @@ -136,14 +136,12 @@ export async function calcLevel(domainId: string, report: Function) { } async function runInDomain(domainId: string, report: Function) { - const info = await domain.get(domainId); - const domainIds = [domainId, ...(info.union || [])]; const results: Record = {}; const udict = Counter(); await db.collection('domain.user').updateMany({ domainId }, { $set: { rpInfo: {} } }); for (const type in RpTypes) { results[type] = new Proxy({}, { get: (self, key) => self[key] || RpTypes[type].base }); - await RpTypes[type].run(domainIds, results[type], report); + await RpTypes[type].run([domainId], results[type], report); const bulk = db.collection('domain.user').initializeUnorderedBulkOp(); for (const uid in results[type]) { const udoc = await UserModel.getById(domainId, +uid); diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index e1d11976e..44f5d1b89 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -32,6 +32,7 @@ declare module '@hydrooj/framework' { domain: DomainDoc; paginate(cursor: FindCursor, page: number, key: string): Promise<[docs: T[], numPages: number, count: number]>; + paginate(cursor: FindCursor, page: number, limit: number): Promise<[docs: T[], numPages: number, count: number]>; progress(message: string, params: any[]): void; limitRate(op: string, periodSecs: number, maxOperations: number, defaultKey?: string): Promise; renderTitle(str: string): string; @@ -237,8 +238,8 @@ export async function apply(ctx: Context) { : str.toString().translate(...this.context.acceptsLanguages(), system.get('server.language')); return res; }, - paginate(cursor: FindCursor, page: number, key: string) { - return db.paginate(cursor, page, this.ctx.setting.get(`pagination.${key}`) || 20); + paginate(cursor: FindCursor, page: number, key: string | number) { + return db.paginate(cursor, page, typeof key === 'number' ? key : (this.ctx.setting.get(`pagination.${key}`) || 20)); }, checkPerm(...args: bigint[]) { if (!this.user.hasPerm(...args)) { diff --git a/packages/sonic/index.ts b/packages/sonic/index.ts index 2fd9014a4..74c4e4414 100644 --- a/packages/sonic/index.ts +++ b/packages/sonic/index.ts @@ -1,11 +1,12 @@ import { - Context, DomainModel, iterateAllProblem, iterateAllProblemInDomain, + Context, iterateAllProblem, iterateAllProblemInDomain, Logger, ProblemModel, Schema, SystemModel, } from 'hydrooj'; import { SonicService } from './service'; const logger = new Logger('sonic'); +export const inject = { 'sonic': { required: false } }; export function apply(ctx: Context) { ctx.plugin(SonicService); @@ -37,18 +38,14 @@ export function apply(ctx: Context) { const cb = async (pdoc) => { i++; if (!(i % 1000)) report({ message: `${i} problems indexed` }); - const union = await DomainModel.getMulti({ union: pdoc.domainId }).toArray(); - const tasks = []; - for (const did of [pdoc.domainId, ...union.map((j) => j._id)]) { - tasks.push( - pdoc.title && ctx.sonic.push( - 'problem', `${did}@title`, `${pdoc.domainId}/${pdoc.docId}`, - `${pdoc.pid || ''} ${pdoc.title} ${pdoc.tag.join(' ')}`, - ), - pdoc.content.toString() && ctx.sonic.push('problem', `${did}@content`, `${pdoc.domainId}/${pdoc.docId}`, pdoc.content.toString()), - ); - } - await Promise.all(tasks).catch((e) => console.log(`${pdoc.domainId}/${pdoc.docId}`, e)); + 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); @@ -56,43 +53,28 @@ export function apply(ctx: Context) { } ctx.on('problem/add', async (doc, docId) => { - const union = await DomainModel.getMulti({ union: doc.domainId }).toArray(); - const tasks = []; - for (const domainId of [doc.domainId, ...union.map((i) => i._id)]) { - tasks.push( - ctx.sonic.push('problem', `${domainId}@title`, `${doc.domainId}/${docId}`, `${doc.pid || ''} ${doc.title} ${doc.tag?.join(' ')}`), - ctx.sonic.push('problem', `${domainId}@content`, `${doc.domainId}/${docId}`, doc.content.toString()), - ); - } - Promise.all(tasks).catch((e) => logger.error(e)); + 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 union = await DomainModel.getMulti({ union: pdoc.domainId }).toArray(); - const tasks = []; const id = `${pdoc.domainId}/${pdoc.docId}`; - for (const domainId of [pdoc.domainId, ...union.map((i) => i._id)]) { - tasks.push( - ctx.sonic.flusho('problem', `${domainId}@title`, id) - .then(() => ctx.sonic.push('problem', `${domainId}@title`, id, `${pdoc.pid || ''} ${pdoc.title} ${pdoc.tag?.join(' ')}`)), - ctx.sonic.flusho('problem', `${domainId}@content`, id) - .then(() => ctx.sonic.push('problem', `${domainId}@content`, id, pdoc.content.toString())), - ); - } - Promise.all(tasks).catch((e) => logger.error(e)); + 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 union = await DomainModel.getMulti({ union: domainId }).toArray(); - const tasks = []; const id = `${domainId}/${docId}`; - for (const domain of [domainId, ...union.map((i) => i._id)]) { - tasks.push( - ctx.sonic.flusho('problem', `${domain}@title`, id), - ctx.sonic.flusho('problem', `${domain}@content`, id), - ); - } - await Promise.all(tasks); + await Promise.all([ + ctx.sonic.flusho('problem', `${domainId}@title`, id), + ctx.sonic.flusho('problem', `${domainId}@content`, id), + ]); }); ctx.addScript( diff --git a/packages/ui-default/templates/components/problem.html b/packages/ui-default/templates/components/problem.html index 3cb6034ae..d6048a8aa 100644 --- a/packages/ui-default/templates/components/problem.html +++ b/packages/ui-default/templates/components/problem.html @@ -1,17 +1,15 @@ -{% macro render_problem_title(pdoc, tdoc=none, show_tags=true, show_invisible_flag=true, invalid=false, inline=false, small=false, alphabetic=false) %} +{% macro render_problem_title(pdoc, tdoc=none, show_tags=true, show_invisible_flag=true, invalid=false, inline=false, show_pid=true, small=false, alphabetic=false) %} {%- if not invalid -%} {% set _linkArgs = { pid:pdoc.pid|default(pdoc.docId) } %} {% if tdoc %}{{ set(_linkArgs, 'query', {tid:tdoc.docId}) }}{% endif %} - {% if pdoc.domainId !== handler.args.domainId %}{{ set(_linkArgs, 'domainId', pdoc.domainId) }}{% endif %} {%- endif -%} -{%- if pdoc.domainId !== handler.args.domainId -%}{{ pdoc.domainId }}#{%- endif -%} -{%- if tdoc and alphabetic -%} - {{ String.fromCharCode(65 + tdoc.pids.indexOf(pdoc.docId)) if tdoc.pids.includes(pdoc.docId) else '#' }} -{%- else -%} - {{ pdoc.pid or ('P'+pdoc.docId if pdoc.domainId === handler.args.domainId else pdoc.docId) }} -{%- endif -%} -{%- if not small -%}  {{ pdoc.title }}{%- endif -%} +{% if show_pid %} +{%- if tdoc and alphabetic -%}{{ String.fromCharCode(65+tdoc.pids.indexOf(pdoc.docId)) }} +{%- elif pdoc.pid and pdoc.pid.includes('-') -%}{{ pdoc.pid.split('-').join('#') }} +{%- else -%}{{ pdoc.pid|default(pdoc.docId) }}{%- endif -%} +{% endif %} +{%- if not small -%}{% if show_pid %}  {% endif %}{{ pdoc.title }}{%- endif -%} {%- if not invalid -%} {%- endif -%} diff --git a/packages/ui-default/templates/partials/homepage/recent_problems.html b/packages/ui-default/templates/partials/homepage/recent_problems.html index d17fd7b13..f3ba5fb9c 100644 --- a/packages/ui-default/templates/partials/homepage/recent_problems.html +++ b/packages/ui-default/templates/partials/homepage/recent_problems.html @@ -8,7 +8,7 @@
{%- for pdoc in pdocs -%}

- {{ problem.render_problem_title(pdoc, show_tags=false, show_invisible_flag=false) }} + {{ problem.render_problem_title(pdoc, show_tags=false, show_invisible_flag=false, show_pid=false) }} {{ datetimeSpan(pdoc._id)|safe }}

{%- endfor -%} diff --git a/packages/ui-default/templates/problem_detail.html b/packages/ui-default/templates/problem_detail.html index 7335a5a47..a8bd0bde8 100644 --- a/packages/ui-default/templates/problem_detail.html +++ b/packages/ui-default/templates/problem_detail.html @@ -45,7 +45,13 @@ {% endif %} - #{{ String.fromCharCode(65+tdoc.pids.indexOf(pdoc.docId)) if tdoc else pdoc.pid|default(pdoc.docId) }}. {{ pdoc.title }} + {%- if tdoc -%} + {{ String.fromCharCode(65+tdoc.pids.indexOf(pdoc.docId)) }} + {%- elif pdoc.pid and pdoc.pid.includes('-') -%} + {{ pdoc.pid.split('-').join('#') }} + {%- else -%} + #{{ pdoc.pid|default(pdoc.docId) }} + {%- endif -%}. {{ pdoc.title }} {% if tdoc and tdoc.pids|length <= 26 %}