diff --git a/hydro/entry/worker.js b/hydro/entry/worker.js index c3cb0f132..cb314fc05 100644 --- a/hydro/entry/worker.js +++ b/hydro/entry/worker.js @@ -55,6 +55,10 @@ async function load() { await script(pending, fail, active); pending = []; await server.start(); + setInterval(() => { + process.send({ event: 'stat', count: global.Hydro.stat.reqCount }); + global.Hydro.stat.reqCount = 0; + }, 30 * 1000); return { active, fail }; } diff --git a/hydro/handler/domain.js b/hydro/handler/domain.js index 6fa4c2107..2c4fcc867 100644 --- a/hydro/handler/domain.js +++ b/hydro/handler/domain.js @@ -1,6 +1,7 @@ const user = require('../model/user'); const domain = require('../model/domain'); const system = require('../model/system'); +const { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } = require('../model/setting'); const paginate = require('../lib/paginate'); const { Route, Handler } = require('../service/server'); const { PERM_MANAGE } = require('../permission'); @@ -67,14 +68,16 @@ class DomainEditHandler extends ManageHandler { ['domain_edit', null], ]; this.response.template = 'domain_edit.html'; - this.response.body = { domain: this.domain, path }; + this.response.body = { current: this.domain, settings: DOMAIN_SETTINGS, path }; } - async post({ - domainId, name, gravatar, bulletin, - }) { - await domain.edit(domainId, { name, gravatar, bulletin }); - this.response.redirect = '/domain/dashboard'; + async post(args) { + const $set = {}; + for (const key of args) { + if (DOMAIN_SETTINGS_BY_KEY[key]) $set[key] = args[key]; + } + await domain.edit(args.domainId, $set); + this.response.redirect = this.url('domain_dashboard'); } } diff --git a/hydro/handler/user.js b/hydro/handler/user.js index e105fdff6..fadfb5d09 100644 --- a/hydro/handler/user.js +++ b/hydro/handler/user.js @@ -1,3 +1,4 @@ +const axios = require('axios'); const { Route, Handler } = require('../service/server.js'); const user = require('../model/user'); const token = require('../model/token'); @@ -7,7 +8,7 @@ const misc = require('../lib/misc'); const { UserAlreadyExistError, InvalidTokenError, VerifyPasswordError, UserNotFoundError, LoginError, SystemError, - PermissionError, BlacklistedError, + PermissionError, BlacklistedError, UserFacingError, } = require('../error'); class UserLoginHandler extends Handler { @@ -25,8 +26,7 @@ class UserLoginHandler extends Handler { if (udoc.ban) throw new BlacklistedError(uname); this.session.uid = udoc._id; this.session.rememberme = rememberme; - const referer = this.request.headers.referer || '/'; - this.response.redirect = referer.endsWith('/login') ? '/' : referer; + this.response.redirect = this.request.referer.endsWith('/login') ? '/' : this.request.referer; } } @@ -161,8 +161,76 @@ class UserSearchHandler extends Handler { } } +class OauthHandler extends Handler { + async get({ type }) { + if (type === 'github') { + const [appid, [state]] = await Promise.all([ + system.get('oauth.githubappid'), + token.add(token.TYPE_OAUTH, 600, { redirect: this.request.referer }), + ]); + this.response.redirect = `https://github.com/login/oauth/authorize?client_id=${appid}&state=${state}`; + } + } +} + +class OauthCallbackHandler extends Handler { + async get({ type, state, code }) { + if (type === 'github') { + const [appid, secret, url, s] = await Promise.all([ + system.get('oauth.githubappid'), + system.get('oauth.githubsecret'), + system.get('server.url'), + token.get(state, token.TYPE_OAUTH), + ]); + const res = await axios.post('https://github.com/login/oauth/access_token', { + client_id: appid, + client_secret: secret, + code, + redirect_uri: `${url}oauth/github/callback`, + state, + }, { headers: { accept: 'application/json' } }); + if (res.data.error) { + throw new UserFacingError( + res.data.error, res.data.error_description, res.data.error_uri, + ); + } + const t = res.data.access_token; + const userInfo = await axios.get('https://api.github.com/user', { + headers: { + Authorization: `token ${t}`, + }, + }); + const { + email: mail, bio, name, login, + } = userInfo.data; + const udoc = await user.getByEmail('system', mail, true); + if (udoc) { + this.session.uid = udoc._id; + await token.del(s, token.TYPE_OAUTH); + } else { + let uname; + const nudoc = await user.getByUname('system', name, true); + if (!nudoc) uname = name; + else { + const ludoc = await user.getByUname('system', login, true); + if (!ludoc) uname = login; + else uname = String.random(16); + } + const uid = await user.create({ + mail, uname, password: String.random(32), regip: this.request.ip, + }); + await user.setById(uid, { bio, oauth: 'github' }); + this.session.uid = uid; + } + this.response.redirect = s.redirect; + } + } +} + async function apply() { Route('user_login', '/login', UserLoginHandler); + Route('user_oauth', '/oauth/:type', OauthHandler); + Route('user_oauth_callback', '/oauth/:type/callback', OauthCallbackHandler); Route('user_register', '/register', UserRegisterHandler); Route('user_register_with_code', '/register/:code', UserRegisterWithCodeHandler); Route('user_logout', '/logout', UserLogoutHandler); diff --git a/hydro/lib/misc.js b/hydro/lib/misc.js index 8e5b13aa2..1dc56d3a4 100644 --- a/hydro/lib/misc.js +++ b/hydro/lib/misc.js @@ -1,8 +1,7 @@ -const ansiup = require('ansi_up'); +const { default: AnsiUp } = require('ansi_up'); const md5 = require('./md5'); -// eslint-disable-next-line new-cap -const AU = new ansiup.default(); +const AU = new AnsiUp(); function ansiToHtml(str) { return AU.ansi_to_html(str); diff --git a/hydro/lib/testdata.convert.ini.js b/hydro/lib/testdata.convert.ini.js index 83f8e357f..00a809e30 100644 --- a/hydro/lib/testdata.convert.ini.js +++ b/hydro/lib/testdata.convert.ini.js @@ -6,12 +6,27 @@ function convert(ini) { const res = { cases: [] }; for (let i = 1; i <= count; i++) { const [input, output, time, score, memory] = f[i].split('|'); - const cur = { input, output, score }; - if (parseInt(time) !== 1) cur.time = `${time}s`; - if (!Number.isNaN(parseInt(memory))) cur.memory = parseInt(memory) / 1024; + const cur = { input, output, score: parseInt(score) }; + cur.time = `${time}s`; + if (!Number.isNaN(parseInt(memory))) cur.memory = `${Math.floor(parseInt(memory) / 1024)}m`; + else cur.memory = '128m'; res.cases.push(cur); } - return yaml.safeDump(res); + if (res.cases.length > 0) { + const { time, score, memory } = res.cases[0]; + for (let i = 1; i < res.cases.length; i++) { + if (res.cases[i].time !== time + || res.cases[i].score !== score + || res.cases[i].memory !== memory) { + return yaml.safeDump(res); + } + } + } + return yaml.safeDump({ + time: res.cases[0].time, + score: res.cases[0].score, + memory: res.cases[0].memory, + }); } global.Hydro.lib['testdata.convert.ini'] = module.exports = convert; diff --git a/hydro/loader.js b/hydro/loader.js index b0f2ed5d3..1ecb220cb 100644 --- a/hydro/loader.js +++ b/hydro/loader.js @@ -58,24 +58,30 @@ async function executeCommand(input) { async function messageHandler(worker, msg) { if (!msg) msg = worker; - if (msg.event && msg.event === 'restart') { - console.log('Restarting'); - await stopWorker(); - console.log('Worker stopped'); - await startWorker(msg.count); - } else if (msg.event && msg.event === 'run') { - await executeCommand(msg.command); + if (msg.event) { + if (msg.event === 'stat') { + global.Hydro.stat.reqCount += msg.count; + } else if (msg.event === 'restart') { + console.log('Restarting'); + await stopWorker(); + console.log('Worker stopped'); + await startWorker(msg.count); + } else if (msg.event === 'run') { + await executeCommand(msg.command); + } } } async function load() { global.nodeModules = { + 'adm-zip': require('adm-zip'), + axios: require('axios'), bson: require('bson'), 'js-yaml': require('js-yaml'), mongodb: require('mongodb'), }; global.Hydro = { - config: {}, + stat: { reqCount: 0 }, handler: {}, service: {}, model: {}, diff --git a/hydro/model/file.js b/hydro/model/file.js index a099ee4a7..8e4b3cc7f 100644 --- a/hydro/model/file.js +++ b/hydro/model/file.js @@ -1,5 +1,5 @@ const fs = require('fs'); -const { ForbiddenError } = require('../error'); +const { ForbiddenError, NotFoundError } = require('../error'); const db = require('../service/db'); const gridfs = require('../service/gridfs'); const hash = require('../lib/hash.hydro'); @@ -55,6 +55,7 @@ async function dec(_id) { async function get(_id, secret) { const file = await coll.findOne({ _id }); + if (!file) throw new NotFoundError(_id); if (typeof secret !== 'undefined') { const timestamp = _timestamp(); if (!(hash(file.secret, timestamp.toString()) === secret)) { diff --git a/hydro/model/setting.js b/hydro/model/setting.js index b6f01a43d..bacbec2d8 100644 --- a/hydro/model/setting.js +++ b/hydro/model/setting.js @@ -12,9 +12,11 @@ const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]); const PREFERENCE_SETTINGS = []; const ACCOUNT_SETTINGS = []; +const DOMAIN_SETTINGS = []; const SYSTEM_SETTINGS = []; const SETTINGS = []; const SETTINGS_BY_KEY = {}; +const DOMAIN_SETTINGS_BY_KEY = {}; const SYSTEM_SETTINGS_BY_KEY = {}; const Setting = ( @@ -39,6 +41,12 @@ const AccountSetting = (...settings) => { SETTINGS_BY_KEY[setting.key] = setting; } }; +const DomainSetting = (...settings) => { + for (const setting of settings) { + DOMAIN_SETTINGS.push(setting); + DOMAIN_SETTINGS_BY_KEY[setting.key] = setting; + } +}; const SystemSetting = (...settings) => { for (const setting of settings) { SYSTEM_SETTINGS.push(setting); @@ -47,8 +55,9 @@ const SystemSetting = (...settings) => { }; PreferenceSetting( + // TODO generate by global.Hydro.locales Setting('setting_display', 'viewLang', builtin.VIEW_LANGS.map((i) => [i.code, i.name]), - options.default_locale, 'select', 'UI Language'), + 'zh_CN', 'select', 'UI Language'), Setting('setting_display', 'timezone', timezones, 'Asia/Shanghai', 'select', 'Timezone'), Setting('setting_usage', 'codeLang', builtin.LANG_TEXTS, @@ -73,6 +82,12 @@ AccountSetting( 'Choose the background image in your profile page.'), ); +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'), +); + SystemSetting( Setting('setting_basic', 'user.register', null, false, 'checkbox', 'Allow register'), Setting('setting_basic', 'user.create_domain', null, true, 'checkbox', 'Allow domain creation'), @@ -87,6 +102,8 @@ SystemSetting( Setting('setting_server', 'server.host', null, null, 'text', 'Server Host'), Setting('setting_server', 'server.url', null, null, 'text', 'Server BaseURL'), Setting('setting_server', 'server.port', null, 8888, 'number', 'Server Port'), + Setting('setting_oauth', 'oauth.githubappid', null, null, 'text', 'Github Oauth AppID'), + Setting('setting_oauth', 'oauth.githubsecret', null, null, 'text', 'Github Oauth Secret'), Setting('setting_constant', 'PROBLEM_PER_PAGE', null, 100, 'number', 'Problems per Page'), Setting('setting_constant', 'CONTEST_PER_PAGE', null, 20, 'number', 'Contests per Page'), Setting('setting_constant', 'DISCUSSION_PER_PAGE', null, 50, 'number', 'Discussion per Page'), @@ -100,6 +117,7 @@ global.Hydro.model.setting = module.exports = { Setting, PreferenceSetting, AccountSetting, + DomainSetting, SystemSetting, PREFERENCE_SETTINGS, ACCOUNT_SETTINGS, @@ -107,4 +125,6 @@ global.Hydro.model.setting = module.exports = { SETTINGS_BY_KEY, SYSTEM_SETTINGS, SYSTEM_SETTINGS_BY_KEY, + DOMAIN_SETTINGS, + DOMAIN_SETTINGS_BY_KEY, }; diff --git a/hydro/model/token.js b/hydro/model/token.js index 5f7f6f2c8..e26de3027 100644 --- a/hydro/model/token.js +++ b/hydro/model/token.js @@ -97,6 +97,7 @@ global.Hydro.model.token = module.exports = { TYPE_CSRF_TOKEN: 1, TYPE_REGISTER: 2, TYPE_CHANGEMAIL: 3, + TYPE_OAUTH: 4, ensureIndexes, add, diff --git a/hydro/model/user.js b/hydro/model/user.js index ed58396a0..96627d59e 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.js @@ -2,6 +2,7 @@ const builtin = require('./builtin'); const document = require('./document'); const system = require('./system'); const token = require('./token'); +const setting = require('./setting'); const { UserNotFoundError, UserAlreadyExistError, LoginError } = require('../error'); const perm = require('../permission'); const pwhash = require('../lib/hash.hydro'); @@ -19,14 +20,16 @@ class USER { this.salt = () => udoc.salt; this.hash = () => udoc.hash; this.priv = udoc.priv; - this.timeZone = udoc.timeZone || 'Asia/Shanghai'; - this.viewLang = udoc.viewLang || 'zh_CN'; - this.codeLang = udoc.codeLang || 'c'; - this.codeTemplate = udoc.codeTemplate || ''; + + for (const key in setting.SETTINGS_BY_KEY) { + if (udoc[key]) this[key] = udoc[key]; + else if (setting.SETTINGS_BY_KEY[key].value) { + this[key] = setting.SETTINGS_BY_KEY[key].value; + } + } + this.regat = udoc.regat; this.loginat = udoc.loginat; - this.bio = udoc.bio || ''; - this.gravatar = udoc.gravatar || ''; this.ban = udoc.ban || false; this.nAccept = dudoc.nAccept || 0; this.nSubmit = dudoc.nSubmit || 0; @@ -76,10 +79,13 @@ async function getList(domainId, uids) { return r; } -async function getByUname(domainId, uname) { +async function getByUname(domainId, uname, ignoreMissing = false) { const unameLower = uname.trim().toLowerCase(); const udoc = await coll.findOne({ unameLower }); - if (!udoc) throw new UserNotFoundError(uname); + if (!udoc) { + if (ignoreMissing) return null; + throw new UserNotFoundError(uname); + } const dudoc = await getInDomain(domainId, udoc); return new USER(udoc, dudoc); } @@ -97,9 +103,10 @@ async function getByEmail(domainId, mail, ignoreMissing = false) { function setPassword(uid, password) { const salt = String.random(); - return coll.findOneAndUpdate({ _id: uid }, { - $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' }, - }); + return coll.findOneAndUpdate( + { _id: uid }, + { $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' } }, + ); } function setById(uid, args) { @@ -114,11 +121,10 @@ async function changePassword(uid, currentPassword, newPassword) { const udoc = await getById(uid); udoc.checkPassword(currentPassword); const salt = String.random(); - return await coll.findOneAndUpdate({ - _id: udoc._id, - }, { - $set: { salt, hash: pwhash.hash(newPassword, salt), hashType: 'hydro' }, - }); + return await coll.findOneAndUpdate( + { _id: udoc._id }, + { $set: { salt, hash: pwhash(newPassword, salt), hashType: 'hydro' } }, + ); } async function inc(_id, field, n = 1) { @@ -139,11 +145,14 @@ async function incDomain(domainId, uid, field, n = 1) { return dudoc; } +/** + * @returns {Promise} uid + */ async function create({ uid, mail, uname, password, regip = '127.0.0.1', priv = perm.PRIV_NONE, }) { const salt = String.random(); - if (!uid) uid = system.inc('user'); + if (!uid) uid = await system.inc('user'); try { await coll.insertOne({ _id: uid, @@ -164,6 +173,7 @@ async function create({ } catch (e) { throw new UserAlreadyExistError([uid, uname, mail]); } + return uid; } function getMulti(params) { diff --git a/hydro/service/monitor.js b/hydro/service/monitor.js index 96cb79653..a44e65783 100644 --- a/hydro/service/monitor.js +++ b/hydro/service/monitor.js @@ -7,9 +7,10 @@ async function update() { const [mid, $set] = await sysinfo.update(); await coll.updateOne( { mid, type: 'server' }, - { $set: { ...$set, updateAt: new Date() } }, + { $set: { ...$set, updateAt: new Date(), reqCount: global.Hydro.stat.reqCount } }, { upsert: true }, ); + global.Hydro.stat.reqCount = 0; } async function postInit() { @@ -19,7 +20,7 @@ async function postInit() { { $set: { ...info, updateAt: new Date(), type: 'server' } }, { upsert: true }, ); - setInterval(update, 3 * 60 * 1000); + setInterval(update, 60 * 1000); } global.Hydro.service.monitor = module.exports = { postInit }; diff --git a/hydro/service/server.js b/hydro/service/server.js index fca5c395c..f3a2cd783 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -133,6 +133,7 @@ class Handler { files: ctx.request.files, query: ctx.query, path: ctx.path, + referer: ctx.request.headers.referer || '/', }; this.response = { body: '', @@ -348,6 +349,7 @@ class Handler { } async function handle(ctx, HandlerClass, permission) { + global.Hydro.stat.reqCount++; const h = new HandlerClass(ctx); try { const method = ctx.method.toLowerCase(); diff --git a/module/migrate-vijos/script.js b/module/migrate-vijos/script.js index 055229f3e..cfa209c3c 100644 --- a/module/migrate-vijos/script.js +++ b/module/migrate-vijos/script.js @@ -1,9 +1,12 @@ /* eslint-disable no-await-in-loop */ +const fs = require('fs'); +const os = require('os'); + const { mongodb } = global.nodeModules; const dst = global.Hydro.service.db; const { file, problem, discussion } = global.Hydro.model; +const { readConfig } = global.Hydro.lib; -// TODO read config file // TODO output enhancement const map = {}; @@ -23,8 +26,8 @@ const tasks = { hashType: doc.hash.split('|')[0] === 'vj4' ? 'hydro' : doc.hash.split('|')[0], priv: doc.priv === -1 ? 1 : 0, gravatar: doc.gravatar, - email: doc.mail, - emailLower: doc.mail_lower, + mail: doc.mail, + mailLower: doc.mail_lower, regat: doc.regat, regip: doc.regip, loginat: doc.loginat, @@ -44,6 +47,7 @@ const tasks = { num_submit: 'nSubmit', num_accept: 'nAccept', difficulty: 'difficulty', + difficulty_admin: null, pname: 'pid', title: 'title', content: 'content', @@ -153,6 +157,7 @@ const tasks = { field: 'donePids', processer: (pids) => pids.map((id) => pid(id)), }, + rp: null, }, record: async (doc) => { const testCases = []; @@ -297,7 +302,54 @@ async function domainUser(src, report) { } } -async function fixPid(report) { +async function fix(doc) { + await dst.collection('document').updateOne( + { _id: doc._id }, + { $set: { pid: doc.pid || doc.docId.toString() } }, + ); + if (doc.data && doc.data.generationTime) { + await file.inc(doc.data); + const r = await file.get(doc.data); + const p = `${os.tmpdir()}/hydro/migrate.vijos.${doc._id}.zip`; + const w = fs.createWriteStream(p); + await new Promise((resolve, reject) => { + w.on('finish', resolve); + w.on('error', reject); + r.pipe(w); + }); + const config = await readConfig(p); + await dst.collection('document').updateOne( + { _id: doc._id }, + { $set: { config } }, + ); + fs.unlinkSync(p); + } else if (doc.data) { + const pdoc = await problem.get(doc.data.domain, pid(doc.data.pid)); + await Promise.all([ + file.inc(pdoc.data), + dst.collection('document').updateOne( + { _id: doc._id }, + { $set: { data: pdoc.data } }, + ), + ]); + const r = await file.get(pdoc.data); + const p = `${os.tmpdir()}/hydro/migrate.vijos.${doc._id}.zip`; + const w = fs.createWriteStream(p); + await new Promise((resolve, reject) => { + w.on('finish', resolve); + w.on('error', reject); + r.pipe(w); + }); + const config = await readConfig(p); + await dst.collection('document').updateOne( + { _id: doc._id }, + { $set: { config } }, + ); + fs.unlinkSync(p); + } +} + +async function fixProblem(report) { const count = await dst.collection('document').find({ docType: 10 }).count(); await report({ progress: 1, message: `Fix pid: ${count}` }); const total = Math.floor(count / 50); @@ -305,28 +357,13 @@ async function fixPid(report) { const docs = await dst.collection('document') .find({ docType: 10 }).skip(i * 50).limit(50) .toArray(); - const t = []; for (const doc of docs) { - t.push( - dst.collection('document').updateOne( - { _id: doc._id }, - { $set: { pid: doc.pid || doc.docId.toString() } }, - ), - ); - if (doc.data && doc.data.generationTime) { - t.push(file.inc(doc.data)); - } else if (doc.data) { - const pdoc = await problem.get(doc.data.domain, pid(doc.data.pid)); - t.push( - file.inc(pdoc.data), - dst.collection('document').updateOne( - { _id: doc._id }, - { $set: { data: pdoc.data } }, - ), - ); + try { + await fix(doc); + } catch (e) { + await report({ message: e.toString() }); } } - await Promise.all(t); await report({ progress: Math.round(100 * ((i + 1) / (total + 1))) }); } } @@ -437,7 +474,7 @@ async function migrateVijos({ await dst.collection('user').deleteMany({ _id: { $nin: [0, 1] } }); const d = ['user', 'document', 'document.status', 'record', 'file']; for (const i of d) await task(i, src, report); - await fixPid(report); + await fixProblem(report); await discussionNode(src, report); await domainUser(src, report); return true; diff --git a/package.json b/package.json index 37ae2b531..db42f6846 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.2.1", + "version": "2.3.0", "main": "hydro/loader.js", "repository": "https://github.com/hydro-dev/Hydro.git", "author": "masnn", diff --git a/templates/domain_edit.html b/templates/domain_edit.html index 63b4fd8a9..1d966b3f0 100644 --- a/templates/domain_edit.html +++ b/templates/domain_edit.html @@ -3,43 +3,7 @@
- {{ form.form_text({ - label:'ID', - name:'id', - value:domain._id, - disabled:true, - required:true - }) }} - {{ form.form_text({ - label:'Name', - name:'name', - value:domain.name, - autofocus:true, - required:true - }) }} - {{ form.form_text({ - label:'Gravatar Email', - help_text:'Will be used as the system icon.', - name:'gravatar', - value:domain.gravatar - }) }} - {{ form.form_textarea({ - columns:12, - label:'Bulletin', - name:'bulletin', - value:domain.bulletin, - markdown:true, - required:false - }) }} -
- - - -
+ {% include "partials/setting.html" with context %}
diff --git a/templates/user_detail.html b/templates/user_detail.html index 1953dcb91..1e8d22932 100644 --- a/templates/user_detail.html +++ b/templates/user_detail.html @@ -13,7 +13,7 @@
- +

diff --git a/yarn.lock b/yarn.lock index b13293350..3fec359fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5098,7 +5098,7 @@ snyk-try-require@1.3.1, snyk-try-require@^1.1.1, snyk-try-require@^1.3.1: lru-cache "^4.0.0" then-fs "^2.0.0" -snyk@1.346.0, snyk@^1.346.0: +snyk@^1.346.0: version "1.346.0" resolved "https://registry.yarnpkg.com/snyk/-/snyk-1.346.0.tgz#c94646a7ca109a329095419542df92d10df9709f" integrity sha512-uwJDC0CRefppngm1J1M+Sja5CVfdmzLYkxGYp3ZYXrzvd912pgzrdYm8ciUL5a8x1/C+2L0iZ09W68daSuObhA==