From 0c5123e3a65a5fe189bb4eb319f72303ae2e7c00 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Thu, 18 Jun 2026 14:27:50 +1000 Subject: [PATCH] Admin: accept mp4/webm uploads and auto-convert mp4 to transparent webm+webp - Allow .mp4/.webm in addition to image types; raise upload limit to 64MB - MP4 uploads are luma-keyed to a VP9+alpha WebM plus an animated WebP fallback via lib/ghost-media.js; the raw MP4 is discarded - Pre-made .webm uploads are stored directly - All prior media (image/webm/webp) is cleaned up on replace and on delete - WebP doubles as the still thumbnail for converted ghosts --- routes/admin.js | 186 +----------------------------------------------- 1 file changed, 1 insertion(+), 185 deletions(-) diff --git a/routes/admin.js b/routes/admin.js index 0fff467..d3cbe1d 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -1,185 +1 @@ -import { Router } from 'express'; -import multer from 'multer'; -import { randomBytes } from 'node:crypto'; -import { mkdirSync, existsSync, unlinkSync } from 'node:fs'; -import { dirname, extname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import db from '../db/index.js'; -import { requireAuth } from './auth-middleware.js'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const UPLOAD_DIR = join(__dirname, '..', process.env.UPLOAD_DIR || 'uploads'); -mkdirSync(UPLOAD_DIR, { recursive: true }); - -const router = Router(); -router.use(requireAuth); // everything here requires a valid JWT - -const ALLOWED = new Set(['.gif', '.png', '.jpg', '.jpeg', '.webp']); -const storage = multer.diskStorage({ - destination: (_req, _file, cb) => cb(null, UPLOAD_DIR), - filename: (_req, file, cb) => { - const ext = extname(file.originalname).toLowerCase(); - cb(null, `${Date.now()}-${randomBytes(6).toString('hex')}${ext}`); - }, -}); -const upload = multer({ - storage, - limits: { fileSize: 8 * 1024 * 1024 }, - fileFilter: (_req, file, cb) => { - const ext = extname(file.originalname).toLowerCase(); - cb(ALLOWED.has(ext) ? null : new Error('unsupported file type'), ALLOWED.has(ext)); - }, -}); - -const toInt = (v, d = 0) => (Number.isFinite(+v) ? parseInt(v, 10) : d); - -/* ---------------- Ghosts ---------------- */ - -router.get('/ghosts', (req, res) => { - res.json({ ghosts: db.prepare('SELECT * FROM ghosts ORDER BY type, rarity, name').all() }); -}); - -router.post('/ghosts', (req, res) => { - const b = req.body || {}; - if (!b.name || !b.type || !b.rarity) { - return res.status(400).json({ error: 'name, type, rarity required' }); - } - const info = db - .prepare( - `INSERT INTO ghosts - (name, display_name, type, rarity, speed, range, charge_shot, - health, damage, ability, is_boss, set_number, set_name, enabled) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)` - ) - .run( - b.name, b.displayName || b.name, b.type, toInt(b.rarity, 1), - toInt(b.speed), toInt(b.range), toInt(b.chargeShot), - toInt(b.health, 300), toInt(b.damage, 150), b.ability || null, - b.isBoss ? 1 : 0, b.setNumber || null, b.setName || null, - b.enabled === false ? 0 : 1 - ); - res.status(201).json({ id: info.lastInsertRowid }); -}); - -router.patch('/ghosts/:id', (req, res) => { - const ghost = db.prepare('SELECT * FROM ghosts WHERE id = ?').get(req.params.id); - if (!ghost) return res.status(404).json({ error: 'not found' }); - const b = req.body || {}; - const map = { - name: 'name', displayName: 'display_name', type: 'type', rarity: 'rarity', - speed: 'speed', range: 'range', chargeShot: 'charge_shot', - health: 'health', damage: 'damage', ability: 'ability', - isBoss: 'is_boss', setNumber: 'set_number', setName: 'set_name', enabled: 'enabled', - }; - const sets = []; - const vals = []; - for (const [k, col] of Object.entries(map)) { - if (k in b) { - sets.push(`${col} = ?`); - let v = b[k]; - if (k === 'isBoss' || k === 'enabled') v = v ? 1 : 0; - vals.push(v); - } - } - if (!sets.length) return res.json({ ok: true, unchanged: true }); - vals.push(req.params.id); - db.prepare(`UPDATE ghosts SET ${sets.join(', ')} WHERE id = ?`).run(...vals); - res.json({ ok: true }); -}); - -router.post('/ghosts/:id/image', upload.single('image'), (req, res) => { - const ghost = db.prepare('SELECT * FROM ghosts WHERE id = ?').get(req.params.id); - if (!ghost) return res.status(404).json({ error: 'not found' }); - if (!req.file) return res.status(400).json({ error: 'no file' }); - // remove old image file if present - if (ghost.image_path) { - const old = join(UPLOAD_DIR, ghost.image_path); - if (existsSync(old)) try { unlinkSync(old); } catch { /* ignore */ } - } - db.prepare('UPDATE ghosts SET image_path = ? WHERE id = ?').run(req.file.filename, ghost.id); - res.json({ ok: true, image: `/uploads/${req.file.filename}` }); -}); - -router.delete('/ghosts/:id', (req, res) => { - const ghost = db.prepare('SELECT * FROM ghosts WHERE id = ?').get(req.params.id); - if (!ghost) return res.status(404).json({ error: 'not found' }); - if (ghost.image_path) { - const p = join(UPLOAD_DIR, ghost.image_path); - if (existsSync(p)) try { unlinkSync(p); } catch { /* ignore */ } - } - db.prepare('DELETE FROM ghosts WHERE id = ?').run(ghost.id); - res.json({ ok: true }); -}); - -/* ---------------- Sets ---------------- */ - -router.get('/sets', (req, res) => { - const sets = db.prepare('SELECT * FROM sets ORDER BY set_number, set_name').all(); - const getRoster = db.prepare( - `SELECT g.id, g.name, g.type, g.rarity, g.is_boss - FROM ghosts g JOIN set_ghosts sg ON sg.ghost_id = g.id - WHERE sg.set_id = ? ORDER BY g.is_boss DESC, g.rarity DESC, g.name` - ); - res.json({ - sets: sets.map((s) => ({ ...s, roster: getRoster.all(s.id) })), - }); -}); - -router.post('/sets', (req, res) => { - const b = req.body || {}; - if (!b.code || !b.setName) return res.status(400).json({ error: 'code and setName required' }); - try { - const info = db - .prepare( - 'INSERT INTO sets (code, set_number, set_name, boss_ghost_id, enabled) VALUES (?,?,?,?,?)' - ) - .run(b.code, b.setNumber || null, b.setName, b.bossGhostId || null, b.enabled === false ? 0 : 1); - res.status(201).json({ id: info.lastInsertRowid }); - } catch (e) { - if (String(e).includes('UNIQUE')) return res.status(409).json({ error: 'code already exists' }); - throw e; - } -}); - -router.patch('/sets/:id', (req, res) => { - const set = db.prepare('SELECT * FROM sets WHERE id = ?').get(req.params.id); - if (!set) return res.status(404).json({ error: 'not found' }); - const b = req.body || {}; - const map = { - code: 'code', setNumber: 'set_number', setName: 'set_name', - bossGhostId: 'boss_ghost_id', enabled: 'enabled', - }; - const sets = []; const vals = []; - for (const [k, col] of Object.entries(map)) { - if (k in b) { - sets.push(`${col} = ?`); - vals.push(k === 'enabled' ? (b[k] ? 1 : 0) : b[k]); - } - } - if (!sets.length) return res.json({ ok: true, unchanged: true }); - vals.push(req.params.id); - db.prepare(`UPDATE sets SET ${sets.join(', ')} WHERE id = ?`).run(...vals); - res.json({ ok: true }); -}); - -router.put('/sets/:id/roster', (req, res) => { - const set = db.prepare('SELECT * FROM sets WHERE id = ?').get(req.params.id); - if (!set) return res.status(404).json({ error: 'not found' }); - const ids = Array.isArray(req.body?.ghostIds) ? req.body.ghostIds : []; - const tx = db.transaction(() => { - db.prepare('DELETE FROM set_ghosts WHERE set_id = ?').run(set.id); - const link = db.prepare('INSERT OR IGNORE INTO set_ghosts (set_id, ghost_id) VALUES (?, ?)'); - for (const gid of ids) link.run(set.id, gid); - }); - tx(); - res.json({ ok: true, count: ids.length }); -}); - -router.delete('/sets/:id', (req, res) => { - const set = db.prepare('SELECT * FROM sets WHERE id = ?').get(req.params.id); - if (!set) return res.status(404).json({ error: 'not found' }); - db.prepare('DELETE FROM sets WHERE id = ?').run(set.id); - res.json({ ok: true }); -}); - -export default router; +aW1wb3J0IHsgUm91dGVyIH0gZnJvbSAnZXhwcmVzcyc7CmltcG9ydCBtdWx0ZXIgZnJvbSAnbXVsdGVyJzsKaW1wb3J0IHsgcmFuZG9tQnl0ZXMgfSBmcm9tICdub2RlOmNyeXB0byc7CmltcG9ydCB7IG1rZGlyU3luYywgZXhpc3RzU3luYywgdW5saW5rU3luYyB9IGZyb20gJ25vZGU6ZnMnOwppbXBvcnQgeyBkaXJuYW1lLCBleHRuYW1lLCBqb2luIH0gZnJvbSAnbm9kZTpwYXRoJzsKaW1wb3J0IHsgZmlsZVVSTFRvUGF0aCB9IGZyb20gJ25vZGU6dXJsJzsKaW1wb3J0IGRiIGZyb20gJy4uL2RiL2luZGV4LmpzJzsKaW1wb3J0IHsgcmVxdWlyZUF1dGggfSBmcm9tICcuL2F1dGgtbWlkZGxld2FyZS5qcyc7CmltcG9ydCB7IGNvbnZlcnRHaG9zdE1wNCB9IGZyb20gJy4uL2xpYi9naG9zdC1tZWRpYS5qcyc7Cgpjb25zdCBfX2Rpcm5hbWUgPSBkaXJuYW1lKGZpbGVVUkxUb1BhdGgoaW1wb3J0Lm1ldGEudXJsKSk7CmNvbnN0IFVQTE9BRF9ESVIgPSBqb2luKF9fZGlybmFtZSwgJy4uJywgcHJvY2Vzcy5lbnYuVVBMT0FEX0RJUiB8fCAndXBsb2FkcycpOwpta2RpclN5bmMoVVBMT0FEX0RJUiwgeyByZWN1cnNpdmU6IHRydWUgfSk7Cgpjb25zdCByb3V0ZXIgPSBSb3V0ZXIoKTsKcm91dGVyLnVzZShyZXF1aXJlQXV0aCk7IC8vIGV2ZXJ5dGhpbmcgaGVyZSByZXF1aXJlcyBhIHZhbGlkIEpXVAoKY29uc3QgQUxMT1dFRCA9IG5ldyBTZXQoWycuZ2lmJywgJy5wbmcnLCAnLmpwZycsICcuanBlZycsICcud2VicCcsICcud2VibScsICcubXA0J10pOwpjb25zdCBWSURFT19FWFRTID0gbmV3IFNldChbJy5tcDQnLCAnLndlYm0nXSk7CmNvbnN0IHN0b3JhZ2UgPSBtdWx0ZXIuZGlza1N0b3JhZ2UoewogIGRlc3RpbmF0aW9uOiAoX3JlcSwgX2ZpbGUsIGNiKSA9PiBjYihudWxsLCBVUExPQURfRElSKSwKICBmaWxlbmFtZTogKF9yZXEsIGZpbGUsIGNiKSA9PiB7CiAgICBjb25zdCBleHQgPSBleHRuYW1lKGZpbGUub3JpZ2luYWxuYW1lKS50b0xvd2VyQ2FzZSgpOwogICAgY2IobnVsbCwgYCR7RGF0ZS5ub3coKX0tJHtyYW5kb21CeXRlcyg2KS50b1N0cmluZygnaGV4Jyl9JHtleHR9YCk7CiAgfSwKfSk7CmNvbnN0IHVwbG9hZCA9IG11bHRlcih7CiAgc3RvcmFnZSwKICBsaW1pdHM6IHsgZmlsZVNpemU6IDY0ICogMTAyNCAqIDEwMjQgfSwgLy8gNjRNQiDigJQgc291cmNlIE1QNHMgYXJlIGxhcmdlciB0aGFuIEdJRnMKICBmaWxlRmlsdGVyOiAoX3JlcSwgZmlsZSwgY2IpID0+IHsKICAgIGNvbnN0IGV4dCA9IGV4dG5hbWUoZmlsZS5vcmlnaW5hbG5hbWUpLnRvTG93ZXJDYXNlKCk7CiAgICBjYihBTExPV0VELmhhcyhleHQpID8gbnVsbCA6IG5ldyBFcnJvcigndW5zdXBwb3J0ZWQgZmlsZSB0eXBlJyksIEFMTE9XRUQuaGFzKGV4dCkpOwogIH0sCn0pOwoKLy8gUmVtb3ZlIGFuIHVwbG9hZGVkIGZpbGUgYnkgYmFyZSBmaWxlbmFtZSwgaWdub3JpbmcgZXJyb3JzLgpmdW5jdGlvbiByZW1vdmVVcGxvYWQoZmlsZW5hbWUpIHsKICBpZiAoIWZpbGVuYW1lKSByZXR1cm47CiAgY29uc3QgcCA9IGpvaW4oVVBMT0FEX0RJUiwgZmlsZW5hbWUpOwogIGlmIChleGlzdHNTeW5jKHApKSB0cnkgeyB1bmxpbmtTeW5jKHApOyB9IGNhdGNoIHsgLyogaWdub3JlICovIH0KfQoKY29uc3QgdG9JbnQgPSAodiwgZCA9IDApID0+IChOdW1iZXIuaXNGaW5pdGUoK3YpID8gcGFyc2VJbnQodiwgMTApIDogZCk7CgovKiAtLS0tLS0tLS0tLS0tLS0tIEdob3N0cyAtLS0tLS0tLS0tLS0tLS0tICovCgpyb3V0ZXIuZ2V0KCcvZ2hvc3RzJywgKHJlcSwgcmVzKSA9PiB7CiAgcmVzLmpzb24oeyBnaG9zdHM6IGRiLnByZXBhcmUoJ1NFTEVDVCAqIEZST00gZ2hvc3RzIE9SREVSIEJZIHR5cGUsIHJhcml0eSwgbmFtZScpLmFsbCgpIH0pOwp9KTsKCnJvdXRlci5wb3N0KCcvZ2hvc3RzJywgKHJlcSwgcmVzKSA9PiB7CiAgY29uc3QgYiA9IHJlcS5ib2R5IHx8IHt9OwogIGlmICghYi5uYW1lIHx8ICFiLnR5cGUgfHwgIWIucmFyaXR5KSB7CiAgICByZXR1cm4gcmVzLnN0YXR1cyg0MDApLmpzb24oeyBlcnJvcjogJ25hbWUsIHR5cGUsIHJhcml0eSByZXF1aXJlZCcgfSk7CiAgfQogIGNvbnN0IGluZm8gPSBkYgogICAgLnByZXBhcmUoCiAgICAgIGBJTlNFUlQgSU5UTyBnaG9zdHMKICAgICAgICAobmFtZSwgZGlzcGxheV9uYW1lLCB0eXBlLCByYXJpdHksIHNwZWVkLCByYW5nZSwgY2hhcmdlX3Nob3QsCiAgICAgICAgIGhlYWx0aCwgZGFtYWdlLCBhYmlsaXR5LCBpc19ib3NzLCBzZXRfbnVtYmVyLCBzZXRfbmFtZSwgZW5hYmxlZCkKICAgICAgIFZBTFVFUyAoPyw/LD8sPyw/LD8sPyw/LD8sPyw/LD8sPyw/KWAKICAgICkKICAgIC5ydW4oCiAgICAgIGIubmFtZSwgYi5kaXNwbGF5TmFtZSB8fCBiLm5hbWUsIGIudHlwZSwgdG9JbnQoYi5yYXJpdHksIDEpLAogICAgICB0b0ludChiLnNwZWVkKSwgdG9JbnQoYi5yYW5nZSksIHRvSW50KGIuY2hhcmdlU2hvdCksCiAgICAgIHRvSW50KGIuaGVhbHRoLCAzMDApLCB0b0ludChiLmRhbWFnZSwgMTUwKSwgYi5hYmlsaXR5IHx8IG51bGwsCiAgICAgIGIuaXNCb3NzID8gMSA6IDAsIGIuc2V0TnVtYmVyIHx8IG51bGwsIGIuc2V0TmFtZSB8fCBudWxsLAogICAgICBiLmVuYWJsZWQgPT09IGZhbHNlID8gMCA6IDEKICAgICk7CiAgcmVzLnN0YXR1cygyMDEpLmpzb24oeyBpZDogaW5mby5sYXN0SW5zZXJ0Um93aWQgfSk7Cn0pOwoKcm91dGVyLnBhdGNoKCcvZ2hvc3RzLzppZCcsIChyZXEsIHJlcykgPT4gewogIGNvbnN0IGdob3N0ID0gZGIucHJlcGFyZSgnU0VMRUNUICogRlJPTSBnaG9zdHMgV0hFUkUgaWQgPSA/JykuZ2V0KHJlcS5wYXJhbXMuaWQpOwogIGlmICghZ2hvc3QpIHJldHVybiByZXMuc3RhdHVzKDQwNCkuanNvbih7IGVycm9yOiAnbm90IGZvdW5kJyB9KTsKICBjb25zdCBiID0gcmVxLmJvZHkgfHwge307CiAgY29uc3QgbWFwID0gewogICAgbmFtZTogJ25hbWUnLCBkaXNwbGF5TmFtZTogJ2Rpc3BsYXlfbmFtZScsIHR5cGU6ICd0eXBlJywgcmFyaXR5OiAncmFyaXR5JywKICAgIHNwZWVkOiAnc3BlZWQnLCByYW5nZTogJ3JhbmdlJywgY2hhcmdlU2hvdDogJ2NoYXJnZV9zaG90JywKICAgIGhlYWx0aDogJ2hlYWx0aCcsIGRhbWFnZTogJ2RhbWFnZScsIGFiaWxpdHk6ICdhYmlsaXR5JywKICAgIGlzQm9zczogJ2lzX2Jvc3MnLCBzZXROdW1iZXI6ICdzZXRfbnVtYmVyJywgc2V0TmFtZTogJ3NldF9uYW1lJywgZW5hYmxlZDogJ2VuYWJsZWQnLAogIH07CiAgY29uc3Qgc2V0cyA9IFtdOwogIGNvbnN0IHZhbHMgPSBbXTsKICBmb3IgKGNvbnN0IFtrLCBjb2xdIG9mIE9iamVjdC5lbnRyaWVzKG1hcCkpIHsKICAgIGlmIChrIGluIGIpIHsKICAgICAgc2V0cy5wdXNoKGAke2NvbH0gPSA/YCk7CiAgICAgIGxldCB2ID0gYltrXTsKICAgICAgaWYgKGsgPT09ICdpc0Jvc3MnIHx8IGsgPT09ICdlbmFibGVkJykgdiA9IHYgPyAxIDogMDsKICAgICAgdmFscy5wdXNoKHYpOwogICAgfQogIH0KICBpZiAoIXNldHMubGVuZ3RoKSByZXR1cm4gcmVzLmpzb24oeyBvazogdHJ1ZSwgdW5jaGFuZ2VkOiB0cnVlIH0pOwogIHZhbHMucHVzaChyZXEucGFyYW1zLmlkKTsKICBkYi5wcmVwYXJlKGBVUERBVEUgZ2hvc3RzIFNFVCAke3NldHMuam9pbignLCAnKX0gV0hFUkUgaWQgPSA/YCkucnVuKC4uLnZhbHMpOwogIHJlcy5qc29uKHsgb2s6IHRydWUgfSk7Cn0pOwoKcm91dGVyLnBvc3QoJy9naG9zdHMvOmlkL2ltYWdlJywgdXBsb2FkLnNpbmdsZSgnaW1hZ2UnKSwgYXN5bmMgKHJlcSwgcmVzKSA9PiB7CiAgY29uc3QgZ2hvc3QgPSBkYi5wcmVwYXJlKCdTRUxFQ1QgKiBGUk9NIGdob3N0cyBXSEVSRSBpZCA9ID8nKS5nZXQocmVxLnBhcmFtcy5pZCk7CiAgaWYgKCFnaG9zdCkgewogICAgaWYgKHJlcS5maWxlKSByZW1vdmVVcGxvYWQocmVxLmZpbGUuZmlsZW5hbWUpOwogICAgcmV0dXJuIHJlcy5zdGF0dXMoNDA0KS5qc29uKHsgZXJyb3I6ICdub3QgZm91bmQnIH0pOwogIH0KICBpZiAoIXJlcS5maWxlKSByZXR1cm4gcmVzLnN0YXR1cyg0MDApLmpzb24oeyBlcnJvcjogJ25vIGZpbGUnIH0pOwoKICBjb25zdCBleHQgPSBleHRuYW1lKHJlcS5maWxlLmZpbGVuYW1lKS50b0xvd2VyQ2FzZSgpOwoKICAvLyBDbGVhciBhbnkgcHJldmlvdXMgbWVkaWEgKGltYWdlICsgdmlkZW8gc3ByaXRlcykgYmVmb3JlIHJlY29yZGluZyB0aGUgbmV3IHNldC4KICBjb25zdCBjbGVhbnVwT2xkID0gKCkgPT4gewogICAgcmVtb3ZlVXBsb2FkKGdob3N0LmltYWdlX3BhdGgpOwogICAgcmVtb3ZlVXBsb2FkKGdob3N0LndlYm1fcGF0aCk7CiAgICByZW1vdmVVcGxvYWQoZ2hvc3Qud2VicF9wYXRoKTsKICB9OwoKICBpZiAoZXh0ID09PSAnLm1wNCcpIHsKICAgIC8vIENvbnZlcnQgdGhlIHNvdXJjZSBNUDQgdG8gYSB0cmFuc3BhcmVudCBXZWJNIChWUDkrYWxwaGEpIHBsdXMgYSBXZWJQCiAgICAvLyBmYWxsYmFjayB2aWEgbHVtYSBrZXlpbmcuIFRoZSBvcmlnaW5hbCBNUDQgaXMgcmVtb3ZlZCBhZnRlcndhcmRzLgogICAgbGV0IG91dDsKICAgIHRyeSB7CiAgICAgIG91dCA9IGF3YWl0IGNvbnZlcnRHaG9zdE1wNChVUExPQURfRElSLCByZXEuZmlsZS5maWxlbmFtZSk7CiAgICB9IGNhdGNoIChlKSB7CiAgICAgIHJlbW92ZVVwbG9hZChyZXEuZmlsZS5maWxlbmFtZSk7CiAgICAgIHJldHVybiByZXMuc3RhdHVzKDUwMCkuanNvbih7IGVycm9yOiAnY29udmVyc2lvbiBmYWlsZWQnLCBkZXRhaWw6IGUubWVzc2FnZSB9KTsKICAgIH0KICAgIHJlbW92ZVVwbG9hZChyZXEuZmlsZS5maWxlbmFtZSk7IC8vIGRpc2NhcmQgdGhlIHJhdyBtcDQKICAgIGlmICghb3V0LndlYm0gJiYgIW91dC53ZWJwKSB7CiAgICAgIHJldHVybiByZXMuc3RhdHVzKDUwMCkuanNvbih7IGVycm9yOiAnY29udmVyc2lvbiBwcm9kdWNlZCBubyBvdXRwdXQgKGlzIGZmbXBlZyBpbnN0YWxsZWQ/KScgfSk7CiAgICB9CiAgICBjbGVhbnVwT2xkKCk7CiAgICAvLyB3ZWJwIGRvdWJsZXMgYXMgdGhlIHN0aWxsL3RodW1ibmFpbCBpbWFnZSB3aGVyZSBwcmVzZW50LgogICAgZGIucHJlcGFyZSgnVVBEQVRFIGdob3N0cyBTRVQgd2VibV9wYXRoID0gPywgd2VicF9wYXRoID0gPywgaW1hZ2VfcGF0aCA9ID8gV0hFUkUgaWQgPSA/JykKICAgICAgLnJ1bihvdXQud2VibSwgb3V0LndlYnAsIG91dC53ZWJwLCBnaG9zdC5pZCk7CiAgICByZXR1cm4gcmVzLmpzb24oewogICAgICBvazogdHJ1ZSwKICAgICAgd2VibTogb3V0LndlYm0gPyBgL3VwbG9hZHMvJHtvdXQud2VibX1gIDogbnVsbCwKICAgICAgd2VicDogb3V0LndlYnAgPyBgL3VwbG9hZHMvJHtvdXQud2VicH1gIDogbnVsbCwKICAgICAgaW1hZ2U6IG91dC53ZWJwID8gYC91cGxvYWRzLyR7b3V0LndlYnB9YCA6IG51bGwsCiAgICB9KTsKICB9CgogIGlmIChleHQgPT09ICcud2VibScpIHsKICAgIC8vIFByZS1tYWRlIHRyYW5zcGFyZW50IFdlYk0gdXBsb2FkZWQgZGlyZWN0bHkg4oCUIHN0b3JlIGFzLWlzLgogICAgY2xlYW51cE9sZCgpOwogICAgZGIucHJlcGFyZSgnVVBEQVRFIGdob3N0cyBTRVQgd2VibV9wYXRoID0gPywgd2VicF9wYXRoID0gTlVMTCwgaW1hZ2VfcGF0aCA9IE5VTEwgV0hFUkUgaWQgPSA/JykKICAgICAgLnJ1bihyZXEuZmlsZS5maWxlbmFtZSwgZ2hvc3QuaWQpOwogICAgcmV0dXJuIHJlcy5qc29uKHsgb2s6IHRydWUsIHdlYm06IGAvdXBsb2Fkcy8ke3JlcS5maWxlLmZpbGVuYW1lfWAgfSk7CiAgfQoKICAvLyBQbGFpbiBpbWFnZSAoZ2lmL3BuZy9qcGcvd2VicCkg4oCUIHRoZSBvcmlnaW5hbCBiaWxsYm9hcmQgcGF0aC4gQ2xlYXIgYW55CiAgLy8gcHJldmlvdXMgdmlkZW8gc3ByaXRlcyBzbyB0aGUgZ2hvc3QgZmFsbHMgYmFjayBjbGVhbmx5IHRvIHRoZSBpbWFnZS4KICBjbGVhbnVwT2xkKCk7CiAgZGIucHJlcGFyZSgnVVBEQVRFIGdob3N0cyBTRVQgaW1hZ2VfcGF0aCA9ID8sIHdlYm1fcGF0aCA9IE5VTEwsIHdlYnBfcGF0aCA9IE5VTEwgV0hFUkUgaWQgPSA/JykKICAgIC5ydW4ocmVxLmZpbGUuZmlsZW5hbWUsIGdob3N0LmlkKTsKICByZXMuanNvbih7IG9rOiB0cnVlLCBpbWFnZTogYC91cGxvYWRzLyR7cmVxLmZpbGUuZmlsZW5hbWV9YCB9KTsKfSk7Cgpyb3V0ZXIuZGVsZXRlKCcvZ2hvc3RzLzppZCcsIChyZXEsIHJlcykgPT4gewogIGNvbnN0IGdob3N0ID0gZGIucHJlcGFyZSgnU0VMRUNUICogRlJPTSBnaG9zdHMgV0hFUkUgaWQgPSA/JykuZ2V0KHJlcS5wYXJhbXMuaWQpOwogIGlmICghZ2hvc3QpIHJldHVybiByZXMuc3RhdHVzKDQwNCkuanNvbih7IGVycm9yOiAnbm90IGZvdW5kJyB9KTsKICByZW1vdmVVcGxvYWQoZ2hvc3QuaW1hZ2VfcGF0aCk7CiAgcmVtb3ZlVXBsb2FkKGdob3N0LndlYm1fcGF0aCk7CiAgcmVtb3ZlVXBsb2FkKGdob3N0LndlYnBfcGF0aCk7CiAgZGIucHJlcGFyZSgnREVMRVRFIEZST00gZ2hvc3RzIFdIRVJFIGlkID0gPycpLnJ1bihnaG9zdC5pZCk7CiAgcmVzLmpzb24oeyBvazogdHJ1ZSB9KTsKfSk7CgovKiAtLS0tLS0tLS0tLS0tLS0tIFNldHMgLS0tLS0tLS0tLS0tLS0tLSAqLwoKcm91dGVyLmdldCgnL3NldHMnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBzZXRzID0gZGIucHJlcGFyZSgnU0VMRUNUICogRlJPTSBzZXRzIE9SREVSIEJZIHNldF9udW1iZXIsIHNldF9uYW1lJykuYWxsKCk7CiAgY29uc3QgZ2V0Um9zdGVyID0gZGIucHJlcGFyZSgKICAgIGBTRUxFQ1QgZy5pZCwgZy5uYW1lLCBnLnR5cGUsIGcucmFyaXR5LCBnLmlzX2Jvc3MKICAgICBGUk9NIGdob3N0cyBnIEpPSU4gc2V0X2dob3N0cyBzZyBPTiBzZy5naG9zdF9pZCA9IGcuaWQKICAgICBXSEVSRSBzZy5zZXRfaWQgPSA/IE9SREVSIEJZIGcuaXNfYm9zcyBERVNDLCBnLnJhcml0eSBERVNDLCBnLm5hbWVgCiAgKTsKICByZXMuanNvbih7CiAgICBzZXRzOiBzZXRzLm1hcCgocykgPT4gKHsgLi4ucywgcm9zdGVyOiBnZXRSb3N0ZXIuYWxsKHMuaWQpIH0pKSwKICB9KTsKfSk7Cgpyb3V0ZXIucG9zdCgnL3NldHMnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBiID0gcmVxLmJvZHkgfHwge307CiAgaWYgKCFiLmNvZGUgfHwgIWIuc2V0TmFtZSkgcmV0dXJuIHJlcy5zdGF0dXMoNDAwKS5qc29uKHsgZXJyb3I6ICdjb2RlIGFuZCBzZXROYW1lIHJlcXVpcmVkJyB9KTsKICB0cnkgewogICAgY29uc3QgaW5mbyA9IGRiCiAgICAgIC5wcmVwYXJlKAogICAgICAgICdJTlNFUlQgSU5UTyBzZXRzIChjb2RlLCBzZXRfbnVtYmVyLCBzZXRfbmFtZSwgYm9zc19naG9zdF9pZCwgZW5hYmxlZCkgVkFMVUVTICg/LD8sPyw/LD8pJwogICAgICApCiAgICAgIC5ydW4oYi5jb2RlLCBiLnNldE51bWJlciB8fCBudWxsLCBiLnNldE5hbWUsIGIuYm9zc0dob3N0SWQgfHwgbnVsbCwgYi5lbmFibGVkID09PSBmYWxzZSA/IDAgOiAxKTsKICAgIHJlcy5zdGF0dXMoMjAxKS5qc29uKHsgaWQ6IGluZm8ubGFzdEluc2VydFJvd2lkIH0pOwogIH0gY2F0Y2ggKGUpIHsKICAgIGlmIChTdHJpbmcoZSkuaW5jbHVkZXMoJ1VOSVFVRScpKSByZXR1cm4gcmVzLnN0YXR1cyg0MDkpLmpzb24oeyBlcnJvcjogJ2NvZGUgYWxyZWFkeSBleGlzdHMnIH0pOwogICAgdGhyb3cgZTsKICB9Cn0pOwoKcm91dGVyLnBhdGNoKCcvc2V0cy86aWQnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBzZXQgPSBkYi5wcmVwYXJlKCdTRUxFQ1QgKiBGUk9NIHNldHMgV0hFUkUgaWQgPSA/JykuZ2V0KHJlcS5wYXJhbXMuaWQpOwogIGlmICghc2V0KSByZXR1cm4gcmVzLnN0YXR1cyg0MDQpLmpzb24oeyBlcnJvcjogJ25vdCBmb3VuZCcgfSk7CiAgY29uc3QgYiA9IHJlcS5ib2R5IHx8IHt9OwogIGNvbnN0IG1hcCA9IHsKICAgIGNvZGU6ICdjb2RlJywgc2V0TnVtYmVyOiAnc2V0X251bWJlcicsIHNldE5hbWU6ICdzZXRfbmFtZScsCiAgICBib3NzR2hvc3RJZDogJ2Jvc3NfZ2hvc3RfaWQnLCBlbmFibGVkOiAnZW5hYmxlZCcsCiAgfTsKICBjb25zdCBzZXRzID0gW107IGNvbnN0IHZhbHMgPSBbXTsKICBmb3IgKGNvbnN0IFtrLCBjb2xdIG9mIE9iamVjdC5lbnRyaWVzKG1hcCkpIHsKICAgIGlmIChrIGluIGIpIHsKICAgICAgc2V0cy5wdXNoKGAke2NvbH0gPSA/YCk7CiAgICAgIHZhbHMucHVzaChrID09PSAnZW5hYmxlZCcgPyAoYltrXSA/IDEgOiAwKSA6IGJba10pOwogICAgfQogIH0KICBpZiAoIXNldHMubGVuZ3RoKSByZXR1cm4gcmVzLmpzb24oeyBvazogdHJ1ZSwgdW5jaGFuZ2VkOiB0cnVlIH0pOwogIHZhbHMucHVzaChyZXEucGFyYW1zLmlkKTsKICBkYi5wcmVwYXJlKGBVUERBVEUgc2V0cyBTRVQgJHtzZXRzLmpvaW4oJywgJyl9IFdIRVJFIGlkID0gP2ApLnJ1biguLi52YWxzKTsKICByZXMuanNvbih7IG9rOiB0cnVlIH0pOwp9KTsKCnJvdXRlci5wdXQoJy9zZXRzLzppZC9yb3N0ZXInLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBzZXQgPSBkYi5wcmVwYXJlKCdTRUxFQ1QgKiBGUk9NIHNldHMgV0hFUkUgaWQgPSA/JykuZ2V0KHJlcS5wYXJhbXMuaWQpOwogIGlmICghc2V0KSByZXR1cm4gcmVzLnN0YXR1cyg0MDQpLmpzb24oeyBlcnJvcjogJ25vdCBmb3VuZCcgfSk7CiAgY29uc3QgaWRzID0gQXJyYXkuaXNBcnJheShyZXEuYm9keT8uZ2hvc3RJZHMpID8gcmVxLmJvZHkuZ2hvc3RJZHMgOiBbXTsKICBjb25zdCB0eCA9IGRiLnRyYW5zYWN0aW9uKCgpID0+IHsKICAgIGRiLnByZXBhcmUoJ0RFTEVURSBGUk9NIHNldF9naG9zdHMgV0hFUkUgc2V0X2lkID0gPycpLnJ1bihzZXQuaWQpOwogICAgY29uc3QgbGluayA9IGRiLnByZXBhcmUoJ0lOU0VSVCBPUiBJR05PUkUgSU5UTyBzZXRfZ2hvc3RzIChzZXRfaWQsIGdob3N0X2lkKSBWQUxVRVMgKD8sID8pJyk7CiAgICBmb3IgKGNvbnN0IGdpZCBvZiBpZHMpIGxpbmsucnVuKHNldC5pZCwgZ2lkKTsKICB9KTsKICB0eCgpOwogIHJlcy5qc29uKHsgb2s6IHRydWUsIGNvdW50OiBpZHMubGVuZ3RoIH0pOwp9KTsKCnJvdXRlci5kZWxldGUoJy9zZXRzLzppZCcsIChyZXEsIHJlcykgPT4gewogIGNvbnN0IHNldCA9IGRiLnByZXBhcmUoJ1NFTEVDVCAqIEZST00gc2V0cyBXSEVSRSBpZCA9ID8nKS5nZXQocmVxLnBhcmFtcy5pZCk7CiAgaWYgKCFzZXQpIHJldHVybiByZXMuc3RhdHVzKDQwNCkuanNvbih7IGVycm9yOiAnbm90IGZvdW5kJyB9KTsKICBkYi5wcmVwYXJlKCdERUxFVEUgRlJPTSBzZXRzIFdIRVJFIGlkID0gPycpLnJ1bihzZXQuaWQpOwogIHJlcy5qc29uKHsgb2s6IHRydWUgfSk7Cn0pOwoKZXhwb3J0IGRlZmF1bHQgcm91dGVyOwo= \ No newline at end of file