mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
+nginx.conf; update scratchpad
This commit is contained in:
1 parent
6ad41dc2ae
commit
2d5eef8d08
28 files changed
+206
-230
No files matched your search
+12
-12
@@ -2,7 +2,7 @@
|
||||
const { isClass } = require('./utils');
|
||||
|
||||
interface IHydroError {
|
||||
new(...args: any[]): HydroError
|
||||
new(...args: any[]): HydroError
|
||||
}
|
||||
|
||||
const Err = (name: string, ...info: Array<IHydroError | Function | string | number>) => {
|
||||
@@ -29,19 +29,19 @@ const Err = (name: string, ...info: Array<IHydroError | Function | string | numb
|
||||
};
|
||||
|
||||
export class HydroError extends Error {
|
||||
params: any[];
|
||||
params: any[];
|
||||
|
||||
code: number;
|
||||
code: number;
|
||||
|
||||
constructor(...params: any[]) {
|
||||
super();
|
||||
this.params = params;
|
||||
}
|
||||
constructor(...params: any[]) {
|
||||
super();
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
msg() {
|
||||
return 'HydroError';
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
msg() {
|
||||
return 'HydroError';
|
||||
}
|
||||
}
|
||||
|
||||
export const UserFacingError = Err('UserFacingError', HydroError, 'UserFacingError', 400);
|
||||
@@ -85,7 +85,7 @@ export const CsrfTokenError = Err('CsrfTokenError', ForbiddenError, 'CsrfTokenEr
|
||||
export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.');
|
||||
export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.');
|
||||
export const RecordNotFoundError = Err('RecordNotFoundError', NotFoundError, 'Record {0} not found.');
|
||||
export const ProblemDataNotFoundError = Err('ProblemDataNotFoundError', NotFoundError, 'Data of problem {1} not found.');
|
||||
export const ProblemDataNotFoundError = Err('ProblemDataNotFoundError', NotFoundError, 'Data of problem {0} not found.');
|
||||
export const MessageNotFoundError = Err('MessageNotFoundError', NotFoundError, 'Message {0} not found.');
|
||||
export const DocumentNotFoundError = Err('DocumentNotFoundError', NotFoundError, 'Document {2} not found.');
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import * as system from '../model/system';
|
||||
import {
|
||||
Route, Handler, Types, param,
|
||||
} from '../service/server';
|
||||
import * as bus from '../service/bus';
|
||||
|
||||
const ContestHandler = contest.ContestHandlerMixin(Handler);
|
||||
|
||||
@@ -281,7 +282,11 @@ class ContestProblemSubmitHandler extends ContestProblemHandler {
|
||||
lang,
|
||||
code,
|
||||
}, true);
|
||||
await contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId);
|
||||
const [rdoc] = await Promise.all([
|
||||
record.get(domainId, rid),
|
||||
contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId),
|
||||
]);
|
||||
bus.publish('record_change', rdoc);
|
||||
if (!this.canShowRecord(this.tdoc)) {
|
||||
this.response.body = { tid };
|
||||
this.response.redirect = this.url('contest_detail', { tid });
|
||||
|
||||
@@ -11,6 +11,7 @@ import { PenaltyRules } from '../interface';
|
||||
import {
|
||||
Route, Handler, Types, param,
|
||||
} from '../service/server';
|
||||
import * as bus from '../service/bus';
|
||||
import { PERM } from '../model/builtin';
|
||||
import * as system from '../model/system';
|
||||
import * as user from '../model/user';
|
||||
@@ -175,7 +176,7 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
|
||||
@param('code', Types.String)
|
||||
@param('lang', Types.String)
|
||||
async post(domainId: string, tid: ObjectID, pid: number, code: string, lang: string) {
|
||||
this.limitRate('add_record', 60, 100);
|
||||
this.limitRate('add_record', 3600, 100);
|
||||
const tsdoc = await contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK);
|
||||
if (!tsdoc.attend) throw new HomeworkNotAttendedError(tid);
|
||||
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
|
||||
@@ -183,8 +184,12 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
|
||||
const rid = await record.add(domainId, {
|
||||
pid, lang, code, uid: this.user._id, tid, hidden: true, ttype: document.TYPE_HOMEWORK,
|
||||
}, true);
|
||||
await contest.updateStatus(domainId, tid, this.user._id,
|
||||
rid, pid, false, 0, document.TYPE_HOMEWORK);
|
||||
const [rdoc] = await Promise.all([
|
||||
record.get(domainId, rid),
|
||||
contest.updateStatus(domainId, tid, this.user._id,
|
||||
rid, pid, false, 0, document.TYPE_HOMEWORK),
|
||||
]);
|
||||
bus.publish('record_change', rdoc);
|
||||
this.response.body = { tid, rid };
|
||||
if (this.canShowRecord(this.tdoc)) this.response.redirect = this.url('record_detail', { rid });
|
||||
else this.response.redirect = this.url('homework_detail', { tid });
|
||||
|
||||
@@ -140,6 +140,7 @@ class UiSettingsHandler extends Handler {
|
||||
header_background_2x,
|
||||
});
|
||||
this.response.type = 'text/css';
|
||||
this.ctx.set('nolog', '1');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-10
@@ -19,6 +19,7 @@ import {
|
||||
import {
|
||||
NoProblemError, ProblemDataNotFoundError, BadRequestError,
|
||||
SolutionNotFoundError,
|
||||
ProblemNotFoundError,
|
||||
} from '../error';
|
||||
import { Pdoc, User, Rdoc } from '../interface';
|
||||
|
||||
@@ -220,7 +221,11 @@ class ProblemSubmitHandler extends ProblemDetailHandler {
|
||||
const rid = await record.add(domainId, {
|
||||
uid: this.user._id, lang, code, pid: this.pdoc.docId,
|
||||
}, true);
|
||||
await domain.incUserInDomain(domainId, this.user._id, 'nSubmit');
|
||||
const [rdoc] = await Promise.all([
|
||||
record.get(domainId, rid),
|
||||
domain.incUserInDomain(domainId, this.user._id, 'nSubmit'),
|
||||
]);
|
||||
bus.publish('record_change', rdoc);
|
||||
this.response.body = { rid };
|
||||
this.response.redirect = this.url('record_detail', { rid });
|
||||
}
|
||||
@@ -229,13 +234,14 @@ class ProblemSubmitHandler extends ProblemDetailHandler {
|
||||
class ProblemPretestHandler extends ProblemDetailHandler {
|
||||
@param('lang', Types.String)
|
||||
@param('code', Types.String)
|
||||
@param('input', Types.String)
|
||||
async post(domainId: string, lang: string, code: string, input: string) {
|
||||
this.limitRate('add_record', 60, 100);
|
||||
@param('input', Types.String, true)
|
||||
async post(domainId: string, lang: string, code: string, input: string = '') {
|
||||
this.limitRate('add_record', 3600, 100);
|
||||
const rid = await record.add(domainId, {
|
||||
uid: this.user._id, lang, code, pid: this.pdoc.docId, input,
|
||||
}, true);
|
||||
await record.judge(domainId, rid);
|
||||
const rdoc = await record.get(domainId, rid);
|
||||
bus.publish('record_change', rdoc);
|
||||
this.response.body = { rid };
|
||||
}
|
||||
}
|
||||
@@ -247,7 +253,9 @@ class ProblemPretestConnectionHandler extends ConnectionHandler {
|
||||
|
||||
@param('pid', Types.String)
|
||||
async prepare(domainId: string, pid: string) {
|
||||
this.pid = pid;
|
||||
const pdoc = await problem.get(domainId, pid);
|
||||
if (!pdoc) throw new ProblemNotFoundError(domainId, pid);
|
||||
this.pid = pdoc.docId.toString();
|
||||
this.domainId = domainId;
|
||||
bus.subscribe(['record_change'], this, 'onRecordChange');
|
||||
}
|
||||
@@ -310,11 +318,11 @@ class ProblemSettingsHandler extends ProblemManageHandler {
|
||||
|
||||
@param('pid', Types.String, null, parsePid)
|
||||
@param('hidden', Types.Boolean)
|
||||
@param('category', Types.String, null, parseCategory)
|
||||
@param('tag', Types.String, null, parseCategory)
|
||||
@param('category', Types.String, true, null, parseCategory)
|
||||
@param('tag', Types.String, true, null, parseCategory)
|
||||
async postSetting(
|
||||
domainId: string, pid: string | number, hidden = false,
|
||||
category: string[], tag: string[],
|
||||
category: string[] = [], tag: string[] = [],
|
||||
) {
|
||||
const pdoc = await problem.get(domainId, pid);
|
||||
await problem.edit(domainId, pdoc.docId, { hidden, category, tag });
|
||||
@@ -554,7 +562,7 @@ export async function apply() {
|
||||
Route('problem_solution_raw', '/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler);
|
||||
Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler);
|
||||
Route('problem_create', '/problem/create', ProblemCreateHandler);
|
||||
Connection('problem_pretest_conn', '/p/:pid/pretest-conn', ProblemPretestConnectionHandler);
|
||||
Connection('problem_pretest_conn', '/conn/pretest', ProblemPretestConnectionHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.problem = apply;
|
||||
+1
-1
@@ -42,7 +42,7 @@ export interface User extends Dictionary<any> {
|
||||
priv: number,
|
||||
regat: Date,
|
||||
loginat: Date,
|
||||
perm: bigint,
|
||||
perm: () => bigint,
|
||||
role: string,
|
||||
regip: () => string,
|
||||
loginip: () => string,
|
||||
|
||||
@@ -503,14 +503,6 @@ export const CATEGORIES = {
|
||||
],
|
||||
};
|
||||
|
||||
export const FOOTER_EXTRA_HTMLS = [];
|
||||
|
||||
export const VIEW_LANGS = [
|
||||
{ code: 'zh_CN', name: '简体中文' },
|
||||
{ code: 'zh_TW', name: '正體中文' },
|
||||
{ code: 'en', name: 'English' },
|
||||
];
|
||||
|
||||
export const LANG_TEXTS = {
|
||||
c: 'C',
|
||||
cc: 'C++',
|
||||
@@ -623,8 +615,6 @@ global.Hydro.model.builtin = {
|
||||
BUILTIN_ROLES,
|
||||
DEFAULT_NODES,
|
||||
CATEGORIES,
|
||||
VIEW_LANGS,
|
||||
FOOTER_EXTRA_HTMLS,
|
||||
LANG_TEXTS,
|
||||
LANG_HIGHLIGHT_ID,
|
||||
STATUS,
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function add(
|
||||
domainId: string, pid: string = null, title: string, content: string, owner: number,
|
||||
tag: string[] = [], category: string[] = [], data: ObjectID = null, hidden = false,
|
||||
) {
|
||||
const d = await domain.inc(domainId, 'pid_counter', 1);
|
||||
const d = await domain.inc(domainId, 'pidCounter', 1);
|
||||
if (!pid) pid = d.pidCounter.toString();
|
||||
return await document.add(
|
||||
domainId, content, owner, document.TYPE_PROBLEM, d.pidCounter, null, null,
|
||||
|
||||
@@ -46,7 +46,7 @@ export interface JudgeTask {
|
||||
type?: string,
|
||||
}
|
||||
|
||||
export async function add(domainId: string, data: RdocBase, addTask: boolean) {
|
||||
export async function add(domainId: string, data: RdocBase, addTask: boolean): Promise<ObjectID> {
|
||||
_.defaults(data, {
|
||||
status: STATUS.STATUS_WAITING,
|
||||
type: 'judge',
|
||||
|
||||
+11
-9
@@ -13,7 +13,7 @@ for (const country of countries) {
|
||||
for (const t of tz) tzs.add(t);
|
||||
}
|
||||
const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]) as [string, string][];
|
||||
const langRange: Dictionary<string> = {};
|
||||
export const langRange: Dictionary<string> = {};
|
||||
|
||||
for (const lang in global.Hydro.locales) {
|
||||
langRange[lang] = global.Hydro.locales[lang].__langname;
|
||||
@@ -78,8 +78,8 @@ export const SystemSetting = (...settings: _Setting[]) => {
|
||||
PreferenceSetting(
|
||||
Setting('setting_display', 'viewLang', langRange, 'zh_CN', 'select', 'UI Language'),
|
||||
Setting('setting_display', 'timeZone', timezones, 'Asia/Shanghai', 'select', 'Timezone'),
|
||||
Setting('setting_usage', 'codeLang', builtin.LANG_TEXTS, null, 'select', 'Default Code Language'),
|
||||
Setting('setting_usage', 'codeTemplate', null, null, 'textarea', 'Default Code Template',
|
||||
Setting('setting_usage', 'codeLang', builtin.LANG_TEXTS, 'c', 'select', 'Default Code Language'),
|
||||
Setting('setting_usage', 'codeTemplate', null, '', 'textarea', 'Default Code Template',
|
||||
'If left blank, the built-in template of the corresponding language will be used.'),
|
||||
);
|
||||
|
||||
@@ -87,7 +87,7 @@ AccountSetting(
|
||||
Setting('setting_info', 'gravatar', null, null, 'text', 'Gravatar Email',
|
||||
'We use <a href="https://en.gravatar.com/" target="_blank">Gravatar</a> to present your avatar icon.'),
|
||||
Setting('setting_info', 'qq', null, null, 'text', 'QQ'),
|
||||
Setting('setting_info', 'gender', builtin.USER_GENDER_RANGE, null, 'select', 'Gender'),
|
||||
Setting('setting_info', 'gender', builtin.USER_GENDER_RANGE, builtin.USER_GENDER_OTHER, 'select', 'Gender'),
|
||||
Setting('setting_info', 'bio', null, null, 'markdown', 'Bio'),
|
||||
Setting('setting_customize', 'backgroundImage', null,
|
||||
'/components/profile/backgrounds/1.jpg', 'text', 'Profile Background Image',
|
||||
@@ -99,11 +99,11 @@ DomainSetting(
|
||||
Setting('setting_domain', 'name', null, 'New domain', 'text', 'name'),
|
||||
Setting('setting_domain', 'gravatar', null, '', 'text', 'gravatar', 'Will be used as the domain icon.'),
|
||||
Setting('setting_domain', 'bulletin', null, '', 'markdown', 'Bulletin'),
|
||||
Setting('storage', 'pid_counter', null, 0, 'number', 'Problem ID Counter', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('storage', 'nAccept', null, 0, 'number', 'nAccept', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('storage', 'nSubmit', null, 0, 'number', 'nSubmit', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('storage', 'nLike', null, 0, 'number', 'nLike', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('storage', 'rating', null, 1500, 'number', 'rating', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'pidCounter', null, 0, 'number', 'Problem ID Counter', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'nAccept', null, 0, 'number', 'nAccept', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'nSubmit', null, 0, 'number', 'nSubmit', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'nLike', null, 0, 'number', 'nLike', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'rating', null, 1500, 'number', 'rating', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
);
|
||||
|
||||
SystemSetting(
|
||||
@@ -126,6 +126,7 @@ SystemSetting(
|
||||
Setting('setting_session', 'session.saved_expire_seconds', null, 3600 * 24 * 30, 'number', 'Saved session expire seconds'),
|
||||
Setting('setting_session', 'session.unsaved_expire_seconds', null, 3600 * 3, 'number', 'Unsaved session expire seconds'),
|
||||
Setting('setting_ui', 'ui.name', null, 'Hydro', 'text', 'Site Name'),
|
||||
Setting('setting_ui', 'ui.footer_extra_html', null, '', 'textarea', 'Footer Extra HTML'),
|
||||
Setting('setting_ui', 'ui.header_logo', null, '/components/header/header-logo.png', 'text', 'Header Logo'),
|
||||
Setting('setting_ui', 'ui.header_logo_2x', null, '/components/header/header-logo@2x.png', 'text', 'Header Logo@2x'),
|
||||
Setting('setting_ui', 'ui.header_background', null, '/components/header/header-background.png', 'text', 'Header Background'),
|
||||
@@ -168,6 +169,7 @@ global.Hydro.postInit.push(
|
||||
);
|
||||
|
||||
global.Hydro.model.setting = {
|
||||
langRange,
|
||||
Setting,
|
||||
PreferenceSetting,
|
||||
AccountSetting,
|
||||
|
||||
+3
-3
@@ -43,7 +43,7 @@ export class User implements _User {
|
||||
|
||||
loginat: Date;
|
||||
|
||||
perm: bigint;
|
||||
perm: () => bigint;
|
||||
|
||||
role: string;
|
||||
|
||||
@@ -67,7 +67,7 @@ export class User implements _User {
|
||||
this.regip = () => udoc.regip;
|
||||
this.loginat = udoc.loginat;
|
||||
this.loginip = () => udoc.loginip;
|
||||
this.perm = dudoc.perm;
|
||||
this.perm = () => dudoc.perm;
|
||||
this.role = dudoc.role || 'default';
|
||||
|
||||
for (const key in setting.SETTINGS_BY_KEY) {
|
||||
@@ -86,7 +86,7 @@ export class User implements _User {
|
||||
}
|
||||
|
||||
hasPerm(p: bigint) {
|
||||
return (this.perm & p) === p;
|
||||
return (this.perm() & p) === p;
|
||||
}
|
||||
|
||||
hasPriv(p: number) {
|
||||
|
||||
+21
-22
@@ -12,7 +12,7 @@ import Body from 'koa-body';
|
||||
import Router from 'koa-router';
|
||||
import cache from 'koa-static-cache';
|
||||
import sockjs from 'sockjs';
|
||||
import serialize from 'serialize-javascript';
|
||||
import serialize, { SerializeJSOptions } from 'serialize-javascript';
|
||||
import parse from 'yargs-parser';
|
||||
import { lrucache } from '../utils';
|
||||
import { User, DomainDoc } from '../interface';
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
NotFoundError,
|
||||
} from '../error';
|
||||
import { render } from '../lib/template';
|
||||
import hash from '../lib/hash.hydro';
|
||||
import * as misc from '../lib/misc';
|
||||
import * as user from '../model/user';
|
||||
import * as domain from '../model/domain';
|
||||
@@ -30,10 +31,8 @@ import * as system from '../model/system';
|
||||
import * as blacklist from '../model/blacklist';
|
||||
import * as token from '../model/token';
|
||||
import * as opcount from '../model/opcount';
|
||||
import hash from '../lib/hash.hydro';
|
||||
|
||||
const argv = parse(process.argv.slice(2));
|
||||
let enableLog = true;
|
||||
const app = new Koa();
|
||||
let server;
|
||||
const router = new Router();
|
||||
@@ -43,14 +42,6 @@ interface H {
|
||||
[key: string]: any,
|
||||
}
|
||||
|
||||
interface IHandler {
|
||||
// @ts-ignore
|
||||
new(ctx: Koa.Context): Handler;
|
||||
}
|
||||
interface IConnectionHandler {
|
||||
// @ts-ignore
|
||||
new(conn: sockjs.Connection): ConnectionHandler;
|
||||
}
|
||||
type MethodDecorator = (target: any, name: string, obj: any) => any;
|
||||
type Converter = (value: any) => any;
|
||||
type Validator = (value: any) => boolean;
|
||||
@@ -268,7 +259,6 @@ export class Handler {
|
||||
}
|
||||
|
||||
async renderHTML(name: string, context: any): Promise<string> {
|
||||
if (enableLog) console.time(name);
|
||||
const UserContext = {
|
||||
...this.user,
|
||||
gravatar: misc.gravatar(this.user.gravatar || '', 128),
|
||||
@@ -281,7 +271,6 @@ export class Handler {
|
||||
_: this.translate.bind(this),
|
||||
...context,
|
||||
});
|
||||
if (enableLog) console.timeEnd(name);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -406,6 +395,7 @@ export class Handler {
|
||||
),
|
||||
]);
|
||||
this.csrfToken = this.getCsrfToken(this.session._id || String.random(32));
|
||||
this.UIContext.csrfToken = this.csrfToken;
|
||||
}
|
||||
|
||||
async finish() {
|
||||
@@ -421,12 +411,23 @@ export class Handler {
|
||||
}
|
||||
|
||||
async renderBody() {
|
||||
if (this.response.redirect) {
|
||||
this.response.body = this.response.body || {};
|
||||
this.response.body.url = this.response.redirect;
|
||||
}
|
||||
if (this.response.type) return;
|
||||
if (this.request.json || this.response.redirect || !this.response.template) {
|
||||
this.response.body = serialize(this.response.body);
|
||||
if (
|
||||
this.request.json || this.response.redirect
|
||||
|| this.request.query.noTemplate || !this.response.template) {
|
||||
try {
|
||||
this.response.body = JSON.stringify(this.response.body);
|
||||
} catch (e) {
|
||||
const opt: SerializeJSOptions = { ignoreFunction: true };
|
||||
if (this.request.query.noTemplate) opt.space = 2;
|
||||
this.response.body = serialize(this.response.body, opt);
|
||||
}
|
||||
this.response.type = 'application/json';
|
||||
} else if (this.response.body || this.response.template) {
|
||||
if (this.request.query.noTemplate || this.request.json) return;
|
||||
const templateName = this.request.query.template || this.response.template;
|
||||
if (templateName) {
|
||||
this.response.body = this.response.body || {};
|
||||
@@ -442,10 +443,6 @@ export class Handler {
|
||||
this.ctx.response.status = 302;
|
||||
this.ctx.redirect(this.response.redirect);
|
||||
} else {
|
||||
if (this.response.redirect) {
|
||||
this.response.body = this.response.body || {};
|
||||
this.response.body.url = this.response.redirect;
|
||||
}
|
||||
if (this.response.body != null) {
|
||||
this.ctx.response.body = this.response.body;
|
||||
this.ctx.response.status = this.response.status || 200;
|
||||
@@ -757,8 +754,10 @@ export function Middleware(middleware: Koa.Middleware) {
|
||||
export async function start() {
|
||||
const [disableLog, port] = await system.getMany(['server.log', 'server.port']);
|
||||
if (!disableLog) {
|
||||
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
|
||||
} else enableLog = false;
|
||||
app.use(morgan(':method :url :status :res[content-length] - :response-time ms', {
|
||||
skip: (req, res) => res.hasHeader('nolog'),
|
||||
}));
|
||||
}
|
||||
app.use(router.routes()).use(router.allowedMethods());
|
||||
Route('notfound_handler', '*', Handler);
|
||||
server.listen(argv.port || port);
|
||||
|
||||
Reference in new issue
Block a user