update bus.subscribe

pull/10/head
undefined 4 years ago
parent 875bf696e2
commit d077019e0d

@ -15,11 +15,6 @@ jobs:
- run: |
yarn
yarn build
rm -rf node_modules
- uses: actions/upload-artifact@v1
with:
name: hydro
path: .
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}

@ -2,11 +2,11 @@
Hydro是一个高效的信息学在线测评系统。特点易于部署轻量功能强大且易于扩展。
[中文文档](https://hydro-dev.github.io)
[中文文档](https://hydro.js.org/)
[Hydro UI 传送门](https://github.com/hydro-dev/ui-default)
如果您认为本项目有价值,欢迎 star 。
相关文档若说明的不够详细,请提交 Pull Request或联系开发组说明。
相关文档若说明的不够详细,请提交 Pull Request 或联系开发组说明。
bug和功能建议请在 Issues 提出。
[在 Gitpod 打开已配置完成的测试环境](https://gitpod.io/#https://github.com/hydro-dev/Hydro)

@ -1,9 +1,8 @@
@requireCsrfToken
discussion前端权限节点显示异常?
web端运行脚本异常?
讨论回复通知
举报功能
SETTINGS_PRIVACY.allowRegisteredUsers
model.setting.ui.name
pdoc.secretConfig?
move to @hydrooj/core?
message.markAsRead

@ -35,7 +35,10 @@ export async function load(call: Entry) {
try {
require('../options').default();
} catch (e) {
await call({ entry: 'setup', newProcess: true });
await call({ entry: 'setup', newProcess: true }).catch((err) => {
console.error('Cannot start setup process.', err);
process.exit(1);
});
}
const bus = require('../service/bus');
await new Promise((resolve) => {
@ -54,21 +57,8 @@ export async function load(call: Entry) {
const ins = require('../script/upgrade0_1');
await ins.run({ username: 'Root', password: 'rootroot' });
}
for (const i in global.Hydro.service) {
if (global.Hydro.service[i].postInit) {
try {
await global.Hydro.service[i].postInit();
} catch (e) {
console.error(e);
}
}
}
for (const postInit of global.Hydro.postInit) {
try {
await postInit();
} catch (e) {
console.error(e);
}
await postInit().catch(console.error);
}
return await modelSystem.get('server.worker');
}

@ -206,18 +206,30 @@ class HomeSettingsHandler extends Handler {
this.response.body.settings = setting.PREFERENCE_SETTINGS;
} else if (category === 'account') {
this.response.body.settings = setting.ACCOUNT_SETTINGS;
} else if (category === 'domain') {
this.response.body.settings = setting.DOMAIN_USER_SETTINGS;
} else throw new NotFoundError();
}
async post(args: any) {
const $set = {};
for (const key in args) {
if (setting.SETTINGS_BY_KEY[key]
&& !(setting.SETTINGS_BY_KEY[key].flag & setting.FLAG_DISABLED)) {
$set[key] = args[key];
if (args.category === 'domain') {
for (const key in args) {
if (setting.DOMAIN_USER_SETTINGS_BY_KEY[key]
&& !(setting.DOMAIN_USER_SETTINGS_BY_KEY[key].flag & setting.FLAG_DISABLED)) {
$set[key] = args[key];
}
}
await domain.setUserInDomain(args.domainId, this.user._id, $set);
} else {
for (const key in args) {
if (setting.SETTINGS_BY_KEY[key]
&& !(setting.SETTINGS_BY_KEY[key].flag & setting.FLAG_DISABLED)) {
$set[key] = args[key];
}
}
await user.setById(this.user._id, $set);
}
await user.setById(this.user._id, $set);
this.back();
}
}
@ -322,7 +334,6 @@ class HomeMessagesHandler extends Handler {
if (!udoc) throw new UserNotFoundError(uid);
if (udoc.gravatar) udoc.gravatar = misc.gravatar(udoc.gravatar);
const mdoc = await message.send(this.user._id, uid, content, message.FLAG_UNREAD);
// TODO(twd2): improve here: projection\
this.back({ mdoc, udoc });
}
@ -345,8 +356,10 @@ class HomeMessagesHandler extends Handler {
}
class HomeMessagesConnectionHandler extends ConnectionHandler {
id: string;
async prepare() {
bus.subscribe([`user_message-${this.user._id}`], this, 'onMessageReceived');
bus.subscribe([`user_message-${this.user._id}`], this.onMessageReceived.bind(this));
}
async onMessageReceived(e: any) {
@ -354,7 +367,7 @@ class HomeMessagesConnectionHandler extends ConnectionHandler {
}
async cleanup() {
bus.unsubscribe([`user_message-${this.user._id}`], this, 'onMessageReceived');
bus.unsubscribe([`user_message-${this.user._id}`], this.id);
}
}

@ -8,9 +8,7 @@ class NotFoundHandler extends Handler {
throw new NotFoundError();
}
async get() { }
async post() { }
all() { }
}
export async function apply() {

@ -284,6 +284,8 @@ class ProblemPretestConnectionHandler extends ConnectionHandler {
domainId: string;
id: string;
@param('pid', Types.String)
async prepare(domainId: string, pid: string) {
const pdoc = await problem.get(domainId, pid);
@ -291,7 +293,7 @@ class ProblemPretestConnectionHandler extends ConnectionHandler {
if (!pdoc) throw new ProblemNotFoundError(domainId, pid);
this.pid = pdoc.docId.toString();
this.domainId = domainId;
bus.subscribe(['record_change'], this, 'onRecordChange');
this.id = bus.subscribe(['record_change'], this.onRecordChange.bind(this));
}
async onRecordChange(data) {
@ -313,7 +315,7 @@ class ProblemPretestConnectionHandler extends ConnectionHandler {
}
async cleanup() {
bus.unsubscribe(['record_change'], this, 'onRecordChange');
bus.unsubscribe(['record_change'], this.id);
}
}

@ -113,7 +113,7 @@ class RecordMainConnectionHandler extends RecordConnectionHandler {
return;
}
}
bus.subscribe(['record_change'], this, 'onRecordChange');
this.id = bus.subscribe(['record_change'], this.onRecordChange.bind(this));
}
async message(msg) {
@ -129,7 +129,7 @@ class RecordMainConnectionHandler extends RecordConnectionHandler {
}
async cleanup() {
bus.unsubscribe(['record_change'], this, 'onRecordChange');
bus.unsubscribe(['record_change'], this.id);
}
async onRecordChange(data) {
@ -157,7 +157,7 @@ class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(Connecti
}
}
this.rid = rid.toString();
bus.subscribe(['record_change'], this, 'onRecordChange');
this.id = bus.subscribe(['record_change'], this.onRecordChange.bind(this));
this.onRecordChange({ value: { rdoc } });
}
@ -175,7 +175,7 @@ class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(Connecti
}
async cleanup() {
bus.unsubscribe(['record_change'], this, 'onRecordChange');
bus.unsubscribe(['record_change'], this.id);
}
}

@ -397,7 +397,7 @@ declare global {
locales: Dict<Dict<string>>,
postInit: Array<() => Promise<any>>,
},
onDestory: Array<() => void>,
onDestory: Array<() => void | Promise<void>>,
addons: string[],
}
}

@ -7,7 +7,7 @@ import { Setting as _Setting } from '../interface';
type SettingDict = Dictionary<_Setting>;
const countries = moment.tz.countries();
const tzs: Set<string> = new Set();
const tzs = new Set();
for (const country of countries) {
const tz = moment.tz.zonesForCountry(country);
for (const t of tz) tzs.add(t);
@ -100,6 +100,10 @@ DomainSetting(
Setting('setting_domain', 'gravatar', null, '', 'text', 'gravatar', 'Will be used as the domain icon.'),
Setting('setting_domain', 'bulletin', null, '', 'markdown', 'Bulletin'),
Setting('setting_storage', 'pidCounter', null, 0, 'number', 'Problem ID Counter', null, FLAG_HIDDEN | FLAG_DISABLED),
);
DomainUserSetting(
Setting('setting_info', 'displayName', null, null, 'string', 'display name'),
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),
@ -121,7 +125,7 @@ SystemSetting(
Setting('setting_server', 'server.cdn', null, '/', 'text', 'CDN Prefix', 'Ends with /'),
Setting('setting_server', 'server.port', null, 8888, 'number', 'Server Port'),
Setting('setting_server', 'server.xff', null, null, 'text', 'IP Header', 'e.g. x-forwarded-for (lowercase)'),
Setting('setting_session', 'session.keys', null, [String.random(32)], 'text', 'session.keys', null, FLAG_DISABLED | FLAG_HIDDEN),
Setting('setting_session', 'session.keys', null, [String.random(32)], 'text', 'session.keys', null, FLAG_HIDDEN),
Setting('setting_session', 'session.secure', null, false, 'boolean', 'session.secure'),
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'),
@ -130,17 +134,17 @@ SystemSetting(
Setting('setting_oauth', 'oauth.googleappid', null, null, 'text', 'Google Oauth ClientID', null),
Setting('setting_oauth', 'oauth.googlesecret', null, null, 'text', 'Google Oauth Secret', null, FLAG_SECRET),
Setting('setting_proxy', 'proxy', null, null, 'text', 'Proxy Server URL'),
Setting('setting_constant', 'PROBLEM_PER_PAGE', null, 100, 'number', 'Problems per Page', null, FLAG_HIDDEN),
Setting('setting_constant', 'CONTEST_PER_PAGE', null, 20, 'number', 'Contests per Page', null, FLAG_HIDDEN),
Setting('setting_constant', 'DISCUSSION_PER_PAGE', null, 50, 'number', 'Discussion per Page', null, FLAG_HIDDEN),
Setting('setting_constant', 'RECORD_PER_PAGE', null, 100, 'number', 'Record per Page', null, FLAG_HIDDEN),
Setting('setting_constant', 'SOLUTION_PER_PAGE', null, 20, 'number', 'Solutions per Page', null, FLAG_HIDDEN),
Setting('setting_constant', 'TRAINING_PER_PAGE', null, 10, 'number', 'Training per Page', null, FLAG_HIDDEN),
Setting('setting_constant', 'REPLY_PER_PAGE', null, 50, 'number', 'Reply per Page', null, FLAG_HIDDEN),
Setting('setting_constant', 'HOMEWORK_ON_MAIN', null, 5, 'number', 'HOMEWORK_ON_MAIN', null, FLAG_HIDDEN),
Setting('setting_constant', 'DISCUSSION_ON_MAIN', null, 20, 'number', 'DISCUSSION_ON_MAIN', null, FLAG_HIDDEN),
Setting('setting_constant', 'CONTEST_ON_MAIN', null, 10, 'number', 'CONTEST_ON_MAIN', null, FLAG_HIDDEN),
Setting('setting_constant', 'TRAINING_ON_MAIN', null, 10, 'number', 'TRAINING_ON_MAIN', null, FLAG_HIDDEN),
Setting('setting_storage', 'PROBLEM_PER_PAGE', null, 100, 'number', 'Problems per Page', null, FLAG_HIDDEN),
Setting('setting_storage', 'CONTEST_PER_PAGE', null, 20, 'number', 'Contests per Page', null, FLAG_HIDDEN),
Setting('setting_storage', 'DISCUSSION_PER_PAGE', null, 50, 'number', 'Discussion per Page', null, FLAG_HIDDEN),
Setting('setting_storage', 'RECORD_PER_PAGE', null, 100, 'number', 'Record per Page', null, FLAG_HIDDEN),
Setting('setting_storage', 'SOLUTION_PER_PAGE', null, 20, 'number', 'Solutions per Page', null, FLAG_HIDDEN),
Setting('setting_storage', 'TRAINING_PER_PAGE', null, 10, 'number', 'Training per Page', null, FLAG_HIDDEN),
Setting('setting_storage', 'REPLY_PER_PAGE', null, 50, 'number', 'Reply per Page', null, FLAG_HIDDEN),
Setting('setting_storage', 'HOMEWORK_ON_MAIN', null, 5, 'number', 'HOMEWORK_ON_MAIN', null, FLAG_HIDDEN),
Setting('setting_storage', 'DISCUSSION_ON_MAIN', null, 20, 'number', 'DISCUSSION_ON_MAIN', null, FLAG_HIDDEN),
Setting('setting_storage', 'CONTEST_ON_MAIN', null, 10, 'number', 'CONTEST_ON_MAIN', null, FLAG_HIDDEN),
Setting('setting_storage', 'TRAINING_ON_MAIN', null, 10, 'number', 'TRAINING_ON_MAIN', null, FLAG_HIDDEN),
Setting('setting_storage', 'db.ver', null, 1, 'number', 'Database version', null, FLAG_DISABLED | FLAG_HIDDEN),
Setting('setting_storage', 'user', null, 1, 'number', 'User Counter', null, FLAG_DISABLED | FLAG_HIDDEN),
);

@ -1,33 +1,24 @@
import { EventEmitter } from 'events';
import cluster from 'cluster';
const bus = new EventEmitter();
const bus = {};
export function subscribe(events, handler, funcName) {
if (!funcName) {
for (const event of events) bus.on(event, handler);
} else {
handler.__bus = (...args) => {
handler[funcName].call(handler, ...args);
};
for (const event of events) bus.on(event, handler.__bus);
export function subscribe(events: string[], handler: any) {
const id = String.random(16);
for (const event of events) {
if (!bus[event]) bus[event] = {};
bus[event][id] = handler;
}
return id;
}
export function unsubscribe(events, handler, funcName) {
if (!funcName) {
for (const event of events) bus.off(event, handler);
} else {
handler.__bus = (...args) => {
handler[funcName].call(handler, ...args);
};
// FIXME doesn't work
for (const event of events) bus.off(event, handler.__bus);
delete handler.__bus;
export function unsubscribe(events: string[], id: string) {
for (const event of events) {
if (!bus[event]) bus[event] = {};
delete bus[event][id];
}
}
export function publish(event, payload, isMaster = true) {
export function publish(event: string, payload: any, isMaster = true) {
// Process forked by pm2 would also have process.send
if (isMaster && process.send && !cluster.isMaster) {
process.send({
@ -36,7 +27,9 @@ export function publish(event, payload, isMaster = true) {
payload,
});
} else {
bus.emit(event, { value: payload, event });
if (!bus[event]) bus[event] = {};
const funcs = Object.keys(bus[event]);
Promise.all(funcs.map((func) => bus[event][func]()));
}
}

@ -532,7 +532,7 @@ async function handle(ctx, HandlerClass, checker) {
} else if (typeof h.post !== 'function') {
throw new MethodNotAllowedError(method);
}
} else if (typeof h[method] !== 'function') {
} else if (typeof h[method] !== 'function' && typeof h.all !== 'function') {
throw new MethodNotAllowedError(method);
}
@ -623,50 +623,25 @@ export class ConnectionHandler {
return res;
}
async limitRate(op, periodSecs, maxOperations) {
async limitRate(op: string, periodSecs: number, maxOperations: number) {
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
}
translate(str) {
translate(str: string) {
return str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : '';
}
renderTitle(str) {
renderTitle(str: string) {
return `${this.translate(str)} - Hydro`;
}
checkPerm(...args: Array<bigint[] | bigint>) {
for (const i in args) {
if (args[i] instanceof Array) {
let p = false;
for (const j in args) {
if (this.user.hasPerm(args[i][j])) {
p = true;
break;
}
}
if (!p) throw new PermissionError([args[i]]);
} else if (!this.user.hasPerm(args[i])) {
throw new PermissionError([[args[i]]]);
}
}
checkPerm(...args: bigint[]) {
if (!this.user.hasPerm(...args)) throw new PermissionError(...args);
}
checkPriv(...args: Array<number[] | number>) {
for (const i in args) {
if (args[i] instanceof Array) {
let p = false;
for (const j in args) {
if (this.user.hasPriv(args[i][j])) {
p = true;
break;
}
}
if (!p) throw new PrivilegeError([args[i]]);
} else if (!this.user.hasPriv(args[i])) {
throw new PrivilegeError([[args[i]]]);
}
}
checkPriv(...args: number[]) {
// @ts-ignore
if (!this.user.hasPriv(...args)) throw new PrivilegeError(...args);
}
url(name: string, kwargs = {}) {

@ -1,6 +1,6 @@
{
"name": "hydrooj",
"version": "2.10.11",
"version": "2.10.12",
"main": "dist/loader.js",
"bin": "bin/hydrooj.js",
"repository": "https://github.com/hydro-dev/Hydro.git",
@ -69,7 +69,6 @@
"eslint-config-airbnb-base": "^14.2.0",
"eslint-import-resolver-typescript": "^2.0.0",
"eslint-plugin-import": "^2.20.2",
"ts-node": "^8.10.2",
"typescript": "^3.9.7"
},
"scripts": {

@ -8,4 +8,3 @@ function fix(file) {
}
fix(path.resolve(__dirname, '..', 'yarn.lock'));
fix(path.resolve(__dirname, '..', 'ui', 'yarn.lock'));

@ -492,11 +492,6 @@ anymatch@~3.1.1:
normalize-path "^3.0.0"
picomatch "^2.0.4"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@ -591,11 +586,6 @@ bson@^1.1.4:
resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.4.tgz#f76870d799f15b854dffb7ee32f0a874797f7e89"
integrity sha512-S/yKGU1syOMzO86+dGpg2qGoDL0zvzcb262G+gqEy6TgP6rt6z6qxSFX/8X6vLC91P7G7C3nLs0+bvDzmvBA3Q==
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
@ -890,11 +880,6 @@ detect-browser@^5.1.1:
resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.1.1.tgz#a800db91d3fd60d0861669f5984f1be9ffbe009c"
integrity sha512-5n2aWI57qC3kZaK4j2zYsG6L1LrxgLptGCNhMQgdKhVn6cSdcq43pp6xHPfTHG3TYM6myF4tIPWiZtfdVDgb9w==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
doctrine@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
@ -1796,11 +1781,6 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
markdown-it-anchor@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz#d549acd64856a8ecd1bea58365ef385effbac744"
@ -2515,15 +2495,7 @@ socks@~2.3.2:
ip "1.1.5"
smart-buffer "^4.1.0"
source-map-support@^0.5.17:
version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.6.0, source-map@~0.6.1:
source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@ -2751,17 +2723,6 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
ts-node@^8.10.2:
version "8.10.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d"
integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==
dependencies:
arg "^4.1.0"
diff "^4.0.1"
make-error "^1.1.1"
source-map-support "^0.5.17"
yn "3.1.1"
tsconfig-paths@^3.9.0:
version "3.9.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"
@ -2984,8 +2945,3 @@ ylru@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f"
integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==

Loading…
Cancel
Save