mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-19 17:38:19 +08:00
Merge pull request #993 from hydro-dev/events
This commit is contained in:
23 files changed
+257
-94
No files matched your search
@@ -60,9 +60,9 @@ jobs:
|
||||
.github/workflows/**
|
||||
- name: Build And Lint
|
||||
run: |
|
||||
yarn build:ui:iconfont # icon font file needed for types
|
||||
if [[ ${{ steps.ui-changed-files.outputs.any_changed }} == true ]] || [[ $(npm info @hydrooj/ui-default version) != $(node -e 'console.log(require("./packages/ui-default/package.json").version)') ]]
|
||||
then
|
||||
yarn build:ui:iconfont
|
||||
parallel --tty -j+0 yarn ::: lint:ci build build:ui:production:webpack test
|
||||
else
|
||||
parallel --tty -j+0 yarn ::: lint:ci build test
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
"license": "MIT",
|
||||
"main": "base.mjs",
|
||||
"dependencies": {
|
||||
"@eslint-react/eslint-plugin": "^1.51.3",
|
||||
"@eslint/compat": "^1.2.9",
|
||||
"@eslint-react/eslint-plugin": "^1.52.2",
|
||||
"@eslint/compat": "^1.3.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@stylistic/eslint-plugin": "^4.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.34.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.1",
|
||||
"@typescript-eslint/parser": "^8.34.1",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-plugin-de-morgan": "^1.3.0",
|
||||
"eslint-plugin-github": "^6.0.0",
|
||||
|
||||
@@ -214,7 +214,6 @@ export class Handler<C = CordisContext> extends HandlerCommon<C> {
|
||||
static [kHandler] = 'Handler';
|
||||
|
||||
loginMethods: any;
|
||||
noCheckPermView = false;
|
||||
notUsage = false;
|
||||
allowCors = false;
|
||||
__param: Record<string, decorators.ParamOption<any>[]>;
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"esbuild": "0.25.2",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-import-resolver-typescript": "^4.4.3",
|
||||
"eslint-import-resolver-webpack": "^0.13.10",
|
||||
"fs-extra": "^11.3.0",
|
||||
@@ -53,7 +53,7 @@
|
||||
"mongodb-memory-server": "10.1.4",
|
||||
"nyc": "^17.1.0",
|
||||
"ora": "^8.2.0",
|
||||
"oxlint": "^1.0.0",
|
||||
"oxlint": "^1.2.0",
|
||||
"package-json": "^10.0.1",
|
||||
"semver": "^7.7.2",
|
||||
"simple-git": "^3.28.0",
|
||||
|
||||
@@ -134,7 +134,7 @@ class BlogUserHandler extends BlogHandler {
|
||||
|
||||
class BlogDetailHandler extends BlogHandler {
|
||||
@param('did', Types.ObjectId)
|
||||
async get(domainId: string, did: ObjectId) {
|
||||
async get({ domainId }, did: ObjectId) {
|
||||
const dsdoc = this.user.hasPriv(PRIV.PRIV_USER_PROFILE)
|
||||
? await BlogModel.getStatus(did, this.user._id)
|
||||
: null;
|
||||
@@ -156,13 +156,13 @@ class BlogDetailHandler extends BlogHandler {
|
||||
}
|
||||
|
||||
@param('did', Types.ObjectId)
|
||||
async postStar(domainId: string, did: ObjectId) {
|
||||
async postStar({ }, did: ObjectId) {
|
||||
await BlogModel.setStar(did, this.user._id, true);
|
||||
this.back({ star: true });
|
||||
}
|
||||
|
||||
@param('did', Types.ObjectId)
|
||||
async postUnstar(domainId: string, did: ObjectId) {
|
||||
async postUnstar({ }, did: ObjectId) {
|
||||
await BlogModel.setStar(did, this.user._id, false);
|
||||
this.back({ star: false });
|
||||
}
|
||||
@@ -176,7 +176,7 @@ class BlogEditHandler extends BlogHandler {
|
||||
|
||||
@param('title', Types.Title)
|
||||
@param('content', Types.Content)
|
||||
async postCreate(domainId: string, title: string, content: string) {
|
||||
async postCreate({ }, title: string, content: string) {
|
||||
await this.limitRate('add_blog', 3600, 60);
|
||||
const did = await BlogModel.add(this.user._id, title, content, this.request.ip);
|
||||
this.response.body = { did };
|
||||
@@ -186,7 +186,7 @@ class BlogEditHandler extends BlogHandler {
|
||||
@param('did', Types.ObjectId)
|
||||
@param('title', Types.Title)
|
||||
@param('content', Types.Content)
|
||||
async postUpdate(domainId: string, did: ObjectId, title: string, content: string) {
|
||||
async postUpdate({ }, did: ObjectId, title: string, content: string) {
|
||||
if (!this.user.own(this.ddoc!)) this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
|
||||
await Promise.all([
|
||||
BlogModel.edit(did, title, content),
|
||||
@@ -197,7 +197,7 @@ class BlogEditHandler extends BlogHandler {
|
||||
}
|
||||
|
||||
@param('did', Types.ObjectId)
|
||||
async postDelete(domainId: string, did: ObjectId) {
|
||||
async postDelete({ }, did: ObjectId) {
|
||||
if (!this.user.own(this.ddoc!)) this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
|
||||
await Promise.all([
|
||||
BlogModel.del(did),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@hydrooj/blog",
|
||||
"version": "0.1.0-beta.0",
|
||||
"version": "0.1.0-beta.1",
|
||||
"license": "AGPL-3.0-or-later"
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
"devDependencies": {
|
||||
"@types/diff": "^8.0.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.17",
|
||||
"@types/lodash": "^4.17.18",
|
||||
"sass": "^1.89.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^11.1.0",
|
||||
"mime-types": "^3.0.1",
|
||||
"moment-timezone": "^0.5.48",
|
||||
"moment-timezone": "^0.6.0",
|
||||
"mongodb": "^6.17.0",
|
||||
"mongodb-uri": "^0.9.7",
|
||||
"nanoid": "^5.1.5",
|
||||
@@ -60,7 +60,7 @@
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/koa-static-cache": "^5.1.4",
|
||||
"@types/lodash": "^4.17.17",
|
||||
"@types/lodash": "^4.17.18",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/mongodb-uri": "^0.9.4",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Context } from '../context';
|
||||
import { ForbiddenError } from '../error';
|
||||
import * as SystemModel from '../model/system';
|
||||
import TokenModel from '../model/token';
|
||||
import UserModel from '../model/user';
|
||||
import { ConnectionHandler } from '../service/server';
|
||||
import { Logger } from '../utils';
|
||||
|
||||
const logger = new Logger('connection');
|
||||
|
||||
class WebsocketEventsConnectionManagerHandler extends ConnectionHandler {
|
||||
noCheckPermView = true;
|
||||
category = '#gateway';
|
||||
private id = Math.random().toString(16).substring(2);
|
||||
private privileged = false;
|
||||
private channels: Set<string> = new Set();
|
||||
private disposables: Map<string, () => void> = new Map();
|
||||
|
||||
async prepare() {
|
||||
const secret = SystemModel.get('websocket.secret');
|
||||
const token = this.request.headers['x-hydro-websocket-gateway'];
|
||||
if (token) {
|
||||
if (!secret || token !== secret) throw new ForbiddenError('Invalid token');
|
||||
logger.info('Gateway websocket connection from %s, id=%s', this.request.ip, this.id);
|
||||
this.privileged = true;
|
||||
}
|
||||
await this.ctx.parallel('subscription/init', this, this.privileged);
|
||||
}
|
||||
|
||||
async accept(channel: string) {
|
||||
if (this.channels.has(channel)) return;
|
||||
this.channels.add(channel);
|
||||
const d: (() => void)[] = [];
|
||||
const disposeAll = () => {
|
||||
for (const dispose of d) dispose();
|
||||
};
|
||||
this.disposables.set(channel, disposeAll);
|
||||
await this.ctx.parallel('subscription/enable', channel, this, this.privileged, (disposable) => {
|
||||
d.push(disposable);
|
||||
});
|
||||
}
|
||||
|
||||
async message(payload: any) {
|
||||
try {
|
||||
if (['resume', 'subscribe'].includes(payload.operation)) await this.subscribe(payload);
|
||||
if (payload.operation === 'unsubscribe') await this.unsubscribe(payload);
|
||||
} catch (e) {
|
||||
logger.error('Failed to process message %o: %o', payload, e);
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribe(payload: any) {
|
||||
for (const channel of payload.channels || []) {
|
||||
try {
|
||||
if (!this.channels.has(channel)) continue;
|
||||
this.channels.delete(channel);
|
||||
this.disposables.get(channel)?.();
|
||||
this.disposables.delete(channel);
|
||||
} catch (e) {
|
||||
logger.error('Error unsubscribing from channel %s: %s', channel, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async subscribe(payload: any) {
|
||||
const accept = [];
|
||||
const reject = [];
|
||||
const session = payload.credential
|
||||
? await TokenModel.get(payload.credential, TokenModel.TYPE_SESSION)
|
||||
: null;
|
||||
const op = payload.operation || '';
|
||||
if (op === 'resume' && !this.privileged) {
|
||||
this.send({ operation: 'resume_failed' });
|
||||
return;
|
||||
}
|
||||
const user = op === 'resume' ? null : await UserModel.getById('system', session?.uid || 0);
|
||||
for (const channel of payload.channels || []) {
|
||||
try {
|
||||
const result = op === 'resume'
|
||||
? { ok: true, channel }
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
: await this.ctx.bail('subscription/subscribe', channel, user, this.privileged ? payload.metadata || {} : {});
|
||||
if (result?.ok) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.accept(result.channel);
|
||||
accept.push(result.channel);
|
||||
} else reject.push(channel);
|
||||
} catch (e) {
|
||||
logger.error('Error subscribing to channel %s for user %s: %s', channel, user?._id, e);
|
||||
reject.push(channel);
|
||||
}
|
||||
}
|
||||
if (op !== 'resume') {
|
||||
this.send({
|
||||
operation: 'verify',
|
||||
accept,
|
||||
reject,
|
||||
request_id: payload.request_id,
|
||||
subscription_id: payload.subscription_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
if (this.privileged) {
|
||||
logger.info('Gateway websocket disconnected from %s, id=%s', this.request.ip, this.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.Connection('websocket_gateway', '/websocket', WebsocketEventsConnectionManagerHandler);
|
||||
}
|
||||
@@ -234,11 +234,11 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler {
|
||||
await this.limitRate('add_discussion', 3600, 60);
|
||||
await contest.addClarification(domainId, tid, this.user._id, content, this.request.ip, subject);
|
||||
if (!this.user.own(this.tdoc)) {
|
||||
await Promise.all([this.tdoc.owner, ...this.tdoc.maintainer].map((uid) => message.send(1, uid, JSON.stringify({
|
||||
await message.send(1, this.tdoc.maintainer.concat(this.tdoc.owner), JSON.stringify({
|
||||
message: 'Contest {0} has a new clarification about {1}, please go to contest management to reply.',
|
||||
params: [this.tdoc.title, subject > 0 ? `#${this.tdoc.pids.indexOf(subject) + 1}` : 'the contest'],
|
||||
url: this.url('contest_manage', { tid }),
|
||||
}), message.FLAG_I18N | message.FLAG_UNREAD)));
|
||||
}), message.FLAG_I18N | message.FLAG_UNREAD);
|
||||
}
|
||||
this.back();
|
||||
}
|
||||
@@ -462,11 +462,11 @@ export class ContestManagementHandler extends ContestManagementBaseHandler {
|
||||
const flag = contest.isOngoing(this.tdoc) ? message.FLAG_ALERT : message.FLAG_UNREAD;
|
||||
await Promise.all([
|
||||
contest.addClarification(domainId, tid, 0, content, this.request.ip, subject),
|
||||
...uids.map((uid) => message.send(1, uid, JSON.stringify({
|
||||
message.send(1, uids, JSON.stringify({
|
||||
message: 'Broadcast message from contest {0}:\n{1}',
|
||||
params: [this.tdoc.title, content],
|
||||
url: this.url('contest_problemlist', { tid }),
|
||||
}), flag | message.FLAG_I18N)),
|
||||
}), flag | message.FLAG_I18N),
|
||||
]);
|
||||
}
|
||||
this.back();
|
||||
|
||||
@@ -238,7 +238,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
|
||||
message: 'User {0} mentioned you in {1:link}',
|
||||
params: [this.user.uname, `/d/${domainId}${this.request.path}`],
|
||||
});
|
||||
await Promise.all(uids.map((i) => message.send(1, i, msg, message.FLAG_RICHTEXT | message.FLAG_UNREAD)));
|
||||
await message.send(1, uids, msg, message.FLAG_RICHTEXT | message.FLAG_UNREAD);
|
||||
const drid = await discussion.addReply(domainId, did, this.user._id, content, this.request.ip);
|
||||
this.back({ drid });
|
||||
}
|
||||
@@ -255,7 +255,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
|
||||
message: 'User {0} mentioned you in {1:link}',
|
||||
params: [this.user.uname, `/d/${domainId}${this.request.path}`],
|
||||
});
|
||||
await Promise.all(uids.map((i) => message.send(1, i, msg, message.FLAG_RICHTEXT | message.FLAG_UNREAD)));
|
||||
await message.send(1, uids, msg, message.FLAG_RICHTEXT | message.FLAG_UNREAD);
|
||||
await discussion.addTailReply(domainId, drid, this.user._id, content, this.request.ip);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
NotFoundError, PermissionError, UserAlreadyExistError,
|
||||
UserNotFoundError, ValidationError, VerifyPasswordError,
|
||||
} from '../error';
|
||||
import { DomainDoc, MessageDoc, Setting } from '../interface';
|
||||
import { DomainDoc, Setting } from '../interface';
|
||||
import avatar, { validate } from '../lib/avatar';
|
||||
import * as mail from '../lib/mail';
|
||||
import { verifyTFA } from '../lib/verifyTFA';
|
||||
@@ -29,7 +29,7 @@ import token from '../model/token';
|
||||
import * as training from '../model/training';
|
||||
import user from '../model/user';
|
||||
import {
|
||||
ConnectionHandler, Handler, param, query, requireSudo, subscribe, Types,
|
||||
Handler, param, query, requireSudo, Types,
|
||||
} from '../service/server';
|
||||
import { camelCase, md5 } from '../utils';
|
||||
|
||||
@@ -608,18 +608,6 @@ class HomeMessagesHandler extends Handler {
|
||||
}
|
||||
}
|
||||
|
||||
class HomeMessagesConnectionHandler extends ConnectionHandler {
|
||||
category = '#message';
|
||||
|
||||
@subscribe('user/message')
|
||||
async onMessageReceived(uid: number, mdoc: MessageDoc) {
|
||||
if (uid !== this.user._id) return;
|
||||
const udoc = (await user.getById(this.args.domainId, mdoc.from))!;
|
||||
udoc.avatarUrl = avatar(udoc.avatar, 64);
|
||||
this.send({ udoc, mdoc });
|
||||
}
|
||||
}
|
||||
|
||||
export const inject = { geoip: { required: false }, oauth: {} };
|
||||
export function apply(ctx: Context) {
|
||||
ctx.Route('homepage', '/', HomeHandler);
|
||||
@@ -630,5 +618,38 @@ export function apply(ctx: Context) {
|
||||
ctx.Route('home_domain', '/home/domain', HomeDomainHandler, PRIV.PRIV_USER_PROFILE);
|
||||
ctx.Route('home_domain_create', '/home/domain/create', HomeDomainCreateHandler, PRIV.PRIV_CREATE_DOMAIN);
|
||||
ctx.Route('home_messages', '/home/messages', HomeMessagesHandler, PRIV.PRIV_USER_PROFILE);
|
||||
ctx.Connection('home_messages_conn', '/home/messages-conn', HomeMessagesConnectionHandler, PRIV.PRIV_USER_PROFILE);
|
||||
|
||||
async function notifyMessage(uid: number[], mdoc: any, h) {
|
||||
const udoc = (await user.getById('system', mdoc.from))!;
|
||||
return {
|
||||
operation: 'event',
|
||||
channels: uid.map((u) => `message:${u}`),
|
||||
payload: { udoc: { ...udoc.serialize(h) as any, avatarUrl: avatar(udoc.avatar, 128) }, mdoc },
|
||||
};
|
||||
}
|
||||
|
||||
ctx.on('subscription/init', (h, privileged) => {
|
||||
if (!privileged) return;
|
||||
h.ctx.on('user/message', async (uid, mdoc) => {
|
||||
h.send(await notifyMessage(uid, mdoc, h));
|
||||
});
|
||||
});
|
||||
|
||||
ctx.on('subscription/enable', (channel, h, privileged) => {
|
||||
if (!channel.startsWith('message:') || privileged) return;
|
||||
const uid = +channel.split(':')[1];
|
||||
h.ctx.on('user/message', async (uids, mdoc) => {
|
||||
if (!uids.includes(uid)) return;
|
||||
h.send(await notifyMessage([uid], mdoc, h));
|
||||
});
|
||||
});
|
||||
|
||||
ctx.on('subscription/subscribe', (channel, udoc) => { // eslint-disable-line consistent-return
|
||||
if (channel === 'message' && udoc.hasPriv(PRIV.PRIV_USER_PROFILE)) {
|
||||
return {
|
||||
ok: true,
|
||||
channel: `message:${udoc._id}`,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -18,17 +18,19 @@ class MessageModel {
|
||||
|
||||
@ArgMethod
|
||||
static async send(
|
||||
from: number, to: number,
|
||||
from: number, to: number | number[],
|
||||
content: string, flag: number = MessageModel.FLAG_UNREAD,
|
||||
) {
|
||||
const _id = new ObjectId();
|
||||
const mdoc: MessageDoc = {
|
||||
_id, from, to, content, flag,
|
||||
if (!Array.isArray(to)) to = [to];
|
||||
const base = {
|
||||
_id, from, content, flag,
|
||||
};
|
||||
await MessageModel.coll.insertOne(mdoc);
|
||||
if (from !== to) bus.broadcast('user/message', to, mdoc);
|
||||
if (!to.length) return base;
|
||||
await MessageModel.coll.insertMany(to.map((t) => ({ ...base, to: t })));
|
||||
bus.broadcast('user/message', to, base);
|
||||
if (flag & MessageModel.FLAG_UNREAD) await user.inc(to, 'unreadMsg', 1);
|
||||
return mdoc;
|
||||
return base;
|
||||
}
|
||||
|
||||
static async sendInfo(to: number, content: string) {
|
||||
@@ -36,7 +38,7 @@ class MessageModel {
|
||||
const mdoc: MessageDoc = {
|
||||
_id, from: 1, to, content, flag: MessageModel.FLAG_INFO | MessageModel.FLAG_I18N,
|
||||
};
|
||||
bus.broadcast('user/message', to, mdoc);
|
||||
bus.broadcast('user/message', [to], mdoc);
|
||||
}
|
||||
|
||||
static async get(_id: ObjectId) {
|
||||
|
||||
@@ -296,13 +296,14 @@ class UserModel {
|
||||
}
|
||||
|
||||
@ArgMethod
|
||||
static async inc(_id: number, field: string, n: number = 1) {
|
||||
if (_id < -999) return null;
|
||||
const udoc = await coll.findOne({ _id });
|
||||
if (!udoc) throw new UserNotFoundError(_id);
|
||||
await coll.updateOne({ _id }, { $inc: { [field]: n } });
|
||||
deleteUserCache(udoc);
|
||||
return udoc;
|
||||
static async inc(_id: number | number[], field: string, n: number = 1) {
|
||||
const ids = (Array.isArray(_id) ? _id : [_id]).filter((i) => i >= -999);
|
||||
if (!ids.length) return null;
|
||||
const udocs = await coll.find({ _id: { $in: ids } }).toArray();
|
||||
if (udocs.length !== ids.length) throw new UserNotFoundError(_id);
|
||||
await coll.updateMany({ _id: { $in: ids } }, { $inc: { [field]: n } });
|
||||
for (const udoc of udocs) deleteUserCache(udoc);
|
||||
return udocs;
|
||||
}
|
||||
|
||||
@ArgMethod
|
||||
|
||||
@@ -6,6 +6,7 @@ export { default as moment, isMoment } from 'moment-timezone';
|
||||
|
||||
export {
|
||||
WebService, Router, HandlerCommon, httpServer,
|
||||
Query, Mutation, Subscription, Apis,
|
||||
} from '@hydrooj/framework';
|
||||
|
||||
export * from './pipelineUtils';
|
||||
|
||||
@@ -22,6 +22,12 @@ export interface EventMap {
|
||||
'app/before-reload': (entries: Set<string>) => VoidReturn
|
||||
'app/reload': (entries: Set<string>) => VoidReturn
|
||||
|
||||
'subscription/init': (h: ConnectionHandler<Context>, privileged: boolean) => VoidReturn
|
||||
'subscription/subscribe': (channel: string, user: User, metadata: Record<string, string>) => VoidReturn
|
||||
'subscription/enable': (
|
||||
channel: string, h: ConnectionHandler<Context>, privileged: boolean, onDispose: (disposable: () => void) => void
|
||||
) => VoidReturn
|
||||
|
||||
'app/watch/change': (path: string) => VoidReturn
|
||||
'app/watch/unlink': (path: string) => VoidReturn
|
||||
|
||||
@@ -36,7 +42,7 @@ export interface EventMap {
|
||||
'task/daily': () => void;
|
||||
'task/daily/finish': (pref: Record<string, number>) => void;
|
||||
|
||||
'user/message': (uid: number, mdoc: MessageDoc) => void
|
||||
'user/message': (uid: number[], mdoc: Omit<MessageDoc, 'to'>) => void
|
||||
'user/get': (udoc: User) => void
|
||||
'user/delcache': (content: string | true) => void
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ export async function apply(ctx: Context) {
|
||||
text: v.text,
|
||||
name: v.name,
|
||||
}));
|
||||
if (!h.noCheckPermView && !h.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) h.checkPerm(PERM.PERM_VIEW);
|
||||
if ((!('noCheckPermView' in h) || !h.noCheckPermView) && !h.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) h.checkPerm(PERM.PERM_VIEW);
|
||||
if (h.context.pendingError) throw h.context.pendingError;
|
||||
});
|
||||
on('handler/create/ws', async (h) => {
|
||||
|
||||
@@ -89,9 +89,7 @@ const messagePage = new AutoloadPage('messagePage', (pagename) => {
|
||||
action: () => window.open('/home/messages', '_blank'),
|
||||
}).show();
|
||||
}
|
||||
const url = new URL(`${UiContext.ws_prefix}home/messages-conn`, window.location.href);
|
||||
// TODO handle a better way for cookie
|
||||
if (url.host !== window.location.host) url.searchParams.append('sid', document.cookie.split('sid=')[1].split(';')[0]);
|
||||
const url = new URL(`${UiContext.ws_prefix}websocket`, window.location.href);
|
||||
const endpoint = url.toString().replace('http', 'ws');
|
||||
if (window.SharedWorker) {
|
||||
try {
|
||||
@@ -129,9 +127,19 @@ const messagePage = new AutoloadPage('messagePage', (pagename) => {
|
||||
localStorage.setItem('page.master', selfId);
|
||||
const masterChannel = new BroadcastChannel('hydro-messages');
|
||||
const sock = new Sock(endpoint);
|
||||
sock.onopen = () => {
|
||||
sock.send(JSON.stringify({
|
||||
operation: 'subscribe',
|
||||
request_id: Math.random().toString(16).substring(2),
|
||||
credential: document.cookie.split('sid=')[1].split(';')[0],
|
||||
channels: ['message'],
|
||||
}));
|
||||
};
|
||||
sock.onmessage = async (message) => {
|
||||
const payload = JSON.parse(message.data);
|
||||
masterChannel.postMessage({ type: 'message', payload });
|
||||
if (payload.operation === 'event') {
|
||||
masterChannel.postMessage({ type: 'message', payload: payload.payload });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -28,19 +28,47 @@ const ack = {};
|
||||
function broadcastMsg(message: any) {
|
||||
for (const p of ports) p.postMessage(message);
|
||||
}
|
||||
|
||||
function onMessage(payload: any) {
|
||||
broadcastMsg({ type: 'message', payload });
|
||||
let acked = false;
|
||||
ack[payload.mdoc._id] = () => { acked = true; };
|
||||
setTimeout(() => {
|
||||
delete ack[payload.mdoc._id];
|
||||
if (acked) return;
|
||||
if (payload.mdoc.flag & FLAG_INFO) return;
|
||||
if (Notification?.permission !== 'granted') {
|
||||
console.log('Notification permission denied');
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-new
|
||||
new Notification(
|
||||
payload.udoc.uname || 'Hydro Notification',
|
||||
{
|
||||
tag: `message-${payload.mdoc._id}`,
|
||||
icon: payload.udoc.avatarUrl || '/android-chrome-192x192.png',
|
||||
body: payload.mdoc.content,
|
||||
},
|
||||
);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function initConn(path: string, port: MessagePort, cookie: any) {
|
||||
ports.add(port);
|
||||
if (cookie !== lcookie) conn?.close();
|
||||
else if (conn && conn.readyState === conn.OPEN) return;
|
||||
console.log('Init connection');
|
||||
lcookie = cookie.split('sid=')[1].split(';')[0];
|
||||
if (conn) return;
|
||||
const url = new URL(path, location.origin);
|
||||
if (cookie) url.searchParams.set('sid', cookie.split('sid=')[1].split(';')[0]);
|
||||
lcookie = cookie;
|
||||
console.log('Init connection for', path);
|
||||
conn?.close();
|
||||
conn = new ReconnectingWebsocket(url.toString());
|
||||
conn = new ReconnectingWebsocket(url.toString().replace('http', 'ws'));
|
||||
conn.onopen = () => {
|
||||
console.log('Connected to', path);
|
||||
console.log('Connected');
|
||||
broadcastMsg({ type: 'open' });
|
||||
conn.send(JSON.stringify({
|
||||
'operation': 'subscribe',
|
||||
'request_id': Math.random().toString(16).substring(2),
|
||||
'credential': lcookie,
|
||||
'channels': ['message'],
|
||||
}));
|
||||
};
|
||||
conn.onerror = () => broadcastMsg({ type: 'error' });
|
||||
conn.onclose = (ev) => broadcastMsg({ type: 'close', error: ev.reason });
|
||||
@@ -54,28 +82,8 @@ function initConn(path: string, port: MessagePort, cookie: any) {
|
||||
if (['PermissionError', 'PrivilegeError'].includes(payload.error)) {
|
||||
broadcastMsg({ type: 'close', error: payload.error });
|
||||
conn.close();
|
||||
} else {
|
||||
broadcastMsg({ type: 'message', payload });
|
||||
let acked = false;
|
||||
ack[payload.mdoc._id] = () => { acked = true; };
|
||||
setTimeout(() => {
|
||||
delete ack[payload.mdoc._id];
|
||||
if (acked) return;
|
||||
if (payload.mdoc.flag & FLAG_INFO) return;
|
||||
if (Notification?.permission !== 'granted') {
|
||||
console.log('Notification permission denied');
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-new
|
||||
new Notification(
|
||||
payload.udoc.uname || 'Hydro Notification',
|
||||
{
|
||||
tag: `message-${payload.mdoc._id}`,
|
||||
icon: payload.udoc.avatarUrl || '/android-chrome-192x192.png',
|
||||
body: payload.mdoc.content,
|
||||
},
|
||||
);
|
||||
}, 5000);
|
||||
} else if (payload.operation === 'event') {
|
||||
onMessage(payload.payload);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@fontsource/source-code-pro": "^5.2.6",
|
||||
"@fontsource/ubuntu-mono": "^5.2.6",
|
||||
"@hydrooj/utils": "workspace:^",
|
||||
"@sentry/browser": "^9.28.0",
|
||||
"@sentry/browser": "^9.30.0",
|
||||
"@sentry/webpack-plugin": "^3.5.0",
|
||||
"@simplewebauthn/browser": "13.1.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
@@ -66,7 +66,7 @@
|
||||
"jquery.easing": "^1.4.1",
|
||||
"jquery.transit": "^0.9.12",
|
||||
"matchmedia-polyfill": "^0.3.2",
|
||||
"md-editor-rt": "^5.6.1",
|
||||
"md-editor-rt": "^5.7.0",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"moment": "^2.30.1",
|
||||
"monaco-editor": "0.52.2",
|
||||
|
||||
@@ -315,6 +315,10 @@ function ProblemSelectionDisplay(props) { // eslint-disable-line
|
||||
setPids((o) => o.filter((i) => !all.includes(i)));
|
||||
});
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
(window as any).__getPids = () => pids;
|
||||
(window as any).__setPids = (newIds: string[]) => setPids(newIds);
|
||||
}, [pids]);
|
||||
|
||||
const updateCheckboxSelection = React.useCallback(() => {
|
||||
for (const i of $('[data-checkbox-group="problem"]:checked')) {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<li class="menu__item"><a class="menu__link" href="javascript:showSignInDialog();">
|
||||
<span class="icon icon-add"></span> {{ _('Login to Create a Discussion') }}</a></li>
|
||||
{% elif not handler.user._dudoc.join %}
|
||||
<li class="menu__item"><a class="menu__link" href="/domain/join?target={{ pdoc.domainId }}">
|
||||
<li class="menu__item"><a class="menu__link" href="/domain/join?target={{ tdoc.domainId }}">
|
||||
<span class="icon icon-add"></span> {{ _('Join Domain to Create a Discussion') }}</a></li>
|
||||
{% else %}
|
||||
<li class="menu__item"><a class="menu__link disabled" href="javascript:;">
|
||||
@@ -55,7 +55,7 @@
|
||||
<span class="icon icon-add"></span> {{ _('Login to Attend Contest') }}
|
||||
</a>
|
||||
{% elif not handler.user._dudoc.join %}
|
||||
<a class="menu__link" href="/domain/join?target={{ pdoc.domainId }}">
|
||||
<a class="menu__link" href="/domain/join?target={{ tdoc.domainId }}">
|
||||
<span class="icon icon-add"></span> {{ _('Join Domain to Attend Contest') }}
|
||||
</a>
|
||||
{% else %}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"reggol": "^2.1.0",
|
||||
"search-query-parser": "^1.6.0",
|
||||
"systeminformation": "^5.27.1"
|
||||
"systeminformation": "^5.27.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4"
|
||||
|
||||
Reference in new issue
Block a user