commit 3ab6f6cc39ae5745cb2fdaa6442c5aa443d38173 Author: Ven Date: Tue May 12 03:17:27 2026 +0700 1 diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..82c6ea0 Binary files /dev/null and b/.DS_Store differ diff --git a/SmartCoin/.env.example b/SmartCoin/.env.example new file mode 100644 index 0000000..1379e84 --- /dev/null +++ b/SmartCoin/.env.example @@ -0,0 +1,9 @@ +PAYMENT_ACCOUNT_NAME="Your Account Name" +KBZPAY_NUMBER="09..." +AYAPAY_NUMBER="09..." +AYAPAY_QR_PATH="/aya-pay-qr.png" +ADMIN_TOKEN="replace-with-a-long-random-token" +TELEGRAM_BOT_TOKEN="123456:ABC..." +TELEGRAM_CHAT_ID="123456789" +ORDER_EXPIRY_SECONDS=600 +PORT=8000 diff --git a/SmartCoin/.gitignore b/SmartCoin/.gitignore new file mode 100644 index 0000000..baad3c9 --- /dev/null +++ b/SmartCoin/.gitignore @@ -0,0 +1,21 @@ +orders.db +orders.db-shm +orders.db-wal +uploads/ +backend/database/*.db +backend/database/*.db-shm +backend/database/*.db-wal +backend/uploads/ +backend/.env +backend/node_modules/ +backend/sessions/ +.pycache/ +__pycache__/ +.DS_Store +node_modules/ +notification/node_modules/ +notification/.env +notification/logs/ +live-checking/.env +live-checking/logs/ +live-checking/state/ diff --git a/SmartCoin/README.md b/SmartCoin/README.md new file mode 100644 index 0000000..1610ba8 --- /dev/null +++ b/SmartCoin/README.md @@ -0,0 +1,107 @@ +# Game Top-Up Automation System + +Manual payment flow for KBZPay / AYA Pay: + +1. User places an order. +2. The system creates an `ORD...` order ID and adds random digits to the product price. +3. User pays the exact amount and uploads a screenshot. +4. Telegram sends you the order, amount, method, screenshot, and action buttons. +5. You verify the payment inside KBZPay / AYA Pay. +6. Confirm, reject, or mark completed from Telegram or the admin panel. +7. Confirmed orders are added to the automation queue. + +## Project Structure + +```text +frontend/ Customer and admin HTML/CSS/JS +backend/ Queue, database, uploads, and automation worker foundation +bot/ Telegram bot expansion area +notification/ Email alert system +live-checking/ Health, balance, queue, browser, and session monitoring +logs/ Runtime logs +``` + +## Run + +```bash +python3 server.py +``` + +Open `http://127.0.0.1:8000`. + +## Environment + +Set these before running in production: + +```bash +export PAYMENT_ACCOUNT_NAME="Your Name" +export KBZPAY_NUMBER="09..." +export AYAPAY_NUMBER="09..." +export AYAPAY_QR_PATH="/aya-pay-qr.png" +export ADMIN_TOKEN="use-a-long-random-token" +export TELEGRAM_BOT_TOKEN="123456:ABC..." +export TELEGRAM_CHAT_ID="123456789" +``` + +Optional: + +```bash +export ORDER_EXPIRY_SECONDS=600 +export PORT=8000 +``` + +## Automation Worker + +The worker is dry-run by default. It reads confirmed orders from the queue and marks them completed without touching Smile.one until real browser automation is connected. + +```bash +cd backend +npm run worker:once +npm run worker +``` + +Set `AUTOMATION_DRY_RUN=false` in `backend/.env` only after the Smile.one browser automation adapter is implemented and tested. + +## Telegram Webhook + +Point your Telegram bot webhook to: + +```text +https://your-domain.com/api/telegram/webhook +``` + +The bot buttons use callback data: + +- `confirm:ORDER_ID` marks the order `paid` +- `reject:ORDER_ID` marks the order `rejected` +- `complete:ORDER_ID` marks the order `completed` + +## Database + +The app stores SQLite data at `backend/database/orders.db` with: + +- `orders` +- `transaction_logs` +- `order_queue` +- `smile_accounts` + +Uploads are stored in `backend/uploads/` and served from `/uploads/...`. + +Put the AYA Pay banking QR image at `frontend/aya-pay-qr.png`, or set `AYAPAY_QR_PATH` to the public URL/path for your QR image. + +## Live Checking + +```bash +cd live-checking +npm run check +npm start +``` + +Dry-run mode is enabled by default so checks log alerts before real email credentials are configured. + +## Important Fraud Rules + +- Never trust screenshots alone. +- Always verify inside your KBZPay / AYA Pay wallet app. +- Reject unclear screenshots, edited screenshots, wrong amounts, stale payments, or mismatched payment methods. +- Use the transaction log when reviewing disputes. diff --git a/SmartCoin/backend/.env.example b/SmartCoin/backend/.env.example new file mode 100644 index 0000000..ff63afc --- /dev/null +++ b/SmartCoin/backend/.env.example @@ -0,0 +1,16 @@ +SMARTCOIN_API_BASE=http://127.0.0.1:8000 +ADMIN_TOKEN=test-token + +AUTOMATION_DRY_RUN=true +AUTOMATION_INTERVAL_SECONDS=20 +AUTOMATION_MAX_ATTEMPTS=2 +AUTOMATION_DELAY_MS=2500 + +SMILE_ROUTING_STRATEGY=balance-aware + +PUPPETEER_HEADLESS=false +PUPPETEER_SLOW_MO_MS=100 +PUPPETEER_SESSION_DIR=./sessions +PUPPETEER_NAVIGATION_TIMEOUT_MS=45000 +PUPPETEER_EXECUTABLE_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome +SMILE_BASE_URL=https://www.smile.one/ diff --git a/SmartCoin/backend/automation/browserSession.js b/SmartCoin/backend/automation/browserSession.js new file mode 100644 index 0000000..44752aa --- /dev/null +++ b/SmartCoin/backend/automation/browserSession.js @@ -0,0 +1,105 @@ +const fs = require("fs"); +const path = require("path"); +const { loadEnv } = require("../utils/env"); +const logger = require("../utils/logger"); + +loadEnv(); + +function boolFromEnv(name, fallback) { + const raw = process.env[name]; + if (raw === undefined) return fallback; + return ["1", "true", "yes", "on"].includes(raw.toLowerCase()); +} + +function numberFromEnv(name, fallback) { + const value = Number(process.env[name]); + return Number.isFinite(value) ? value : fallback; +} + +function resolveFromBackend(rawPath) { + if (path.isAbsolute(rawPath)) return rawPath; + return path.join(__dirname, "..", rawPath); +} + +function getBrowserConfig(accountEmail = "default") { + const sessionRoot = resolveFromBackend(process.env.PUPPETEER_SESSION_DIR || "./sessions"); + const safeAccount = accountEmail.replace(/[^a-z0-9._-]/gi, "_"); + return { + headless: boolFromEnv("PUPPETEER_HEADLESS", false), + slowMo: numberFromEnv("PUPPETEER_SLOW_MO_MS", 100), + navigationTimeoutMs: numberFromEnv("PUPPETEER_NAVIGATION_TIMEOUT_MS", 45000), + executablePath: + process.env.PUPPETEER_EXECUTABLE_PATH || + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + userDataDir: path.join(sessionRoot, safeAccount), + baseUrl: process.env.SMILE_BASE_URL || "https://www.smile.one/", + }; +} + +async function launchBrowser(accountEmail) { + const puppeteer = require("puppeteer-core"); + const config = getBrowserConfig(accountEmail); + fs.mkdirSync(config.userDataDir, { recursive: true }); + + const browser = await puppeteer.launch({ + headless: config.headless, + executablePath: config.executablePath, + slowMo: config.slowMo, + userDataDir: config.userDataDir, + defaultViewport: { + width: 1366, + height: 900, + }, + args: [ + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + ], + }); + + const page = await browser.newPage(); + page.setDefaultNavigationTimeout(config.navigationTimeoutMs); + page.setDefaultTimeout(config.navigationTimeoutMs); + + logger.info("Puppeteer browser launched", { + accountEmail, + headless: config.headless, + userDataDir: config.userDataDir, + }); + + return { + browser, + page, + config, + }; +} + +async function smokeCheck() { + const session = await launchBrowser("smoke-check"); + try { + await session.page.goto(session.config.baseUrl, { + waitUntil: "domcontentloaded", + }); + logger.info("Puppeteer smoke check loaded Smile.one", { + url: session.page.url(), + title: await session.page.title(), + }); + } finally { + await session.browser.close(); + } +} + +if (require.main === module) { + smokeCheck().catch((error) => { + logger.error("Puppeteer smoke check failed", { + error: error.message, + stack: error.stack, + }); + process.exit(1); + }); +} + +module.exports = { + getBrowserConfig, + launchBrowser, +}; diff --git a/SmartCoin/backend/automation/smileOneAutomation.js b/SmartCoin/backend/automation/smileOneAutomation.js new file mode 100644 index 0000000..32d3dde --- /dev/null +++ b/SmartCoin/backend/automation/smileOneAutomation.js @@ -0,0 +1,60 @@ +const logger = require("../utils/logger"); +const { launchBrowser } = require("./browserSession"); +const selectors = require("./smileOneSelectors"); + +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function purchaseTopup({ order, account, dryRun, delayMs }) { + logger.info("Smile.one automation started", { + orderId: order.order_id, + account: account.email, + dryRun, + }); + + await delay(delayMs); + + if (dryRun) { + logger.info("Smile.one dry-run purchase completed", { + orderId: order.order_id, + game: order.game, + product: order.product, + userGameId: order.user_game_id, + }); + return { + ok: true, + dryRun: true, + reference: `DRY-${order.order_id}`, + }; + } + + validateSelectors(); + const session = await launchBrowser(account.email); + try { + await session.page.goto(session.config.baseUrl, { + waitUntil: "domcontentloaded", + }); + await delay(delayMs); + + throw new Error("Smile.one selectors are configured, but the purchase flow still needs site-specific steps"); + } finally { + await session.browser.close(); + } +} + +function validateSelectors() { + const missing = []; + for (const [groupName, group] of Object.entries(selectors)) { + for (const [name, value] of Object.entries(group)) { + if (!value) missing.push(`${groupName}.${name}`); + } + } + if (missing.length > 0) { + throw new Error(`Smile.one selectors are not configured: ${missing.join(", ")}`); + } +} + +module.exports = { + purchaseTopup, +}; diff --git a/SmartCoin/backend/automation/smileOneSelectors.js b/SmartCoin/backend/automation/smileOneSelectors.js new file mode 100644 index 0000000..8670a3c --- /dev/null +++ b/SmartCoin/backend/automation/smileOneSelectors.js @@ -0,0 +1,18 @@ +module.exports = { + login: { + emailInput: "", + passwordInput: "", + submitButton: "", + loggedInMarker: "", + }, + product: { + gameSearchInput: "", + userIdInput: "", + packageButton: "", + confirmButton: "", + successMarker: "", + }, + balance: { + amountText: "", + }, +}; diff --git a/SmartCoin/backend/controllers/.gitkeep b/SmartCoin/backend/controllers/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/SmartCoin/backend/controllers/.gitkeep @@ -0,0 +1 @@ + diff --git a/SmartCoin/backend/database/schema.sql b/SmartCoin/backend/database/schema.sql new file mode 100644 index 0000000..355d94a --- /dev/null +++ b/SmartCoin/backend/database/schema.sql @@ -0,0 +1,42 @@ +CREATE TABLE IF NOT EXISTS orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id TEXT NOT NULL UNIQUE, + game TEXT NOT NULL, + user_game_id TEXT NOT NULL, + product TEXT NOT NULL, + amount INTEGER NOT NULL, + payment_method TEXT, + transaction_id TEXT, + screenshot TEXT, + status TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS transaction_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id TEXT NOT NULL, + action TEXT NOT NULL, + detail TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS order_queue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id TEXT NOT NULL UNIQUE, + status TEXT NOT NULL CHECK(status IN ('waiting', 'processing', 'completed', 'failed')), + attempts INTEGER NOT NULL DEFAULT 0, + last_error TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS smile_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + cookies TEXT, + balance INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL CHECK(status IN ('active', 'paused', 'failed')), + last_used TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/SmartCoin/backend/package-lock.json b/SmartCoin/backend/package-lock.json new file mode 100644 index 0000000..bef2976 --- /dev/null +++ b/SmartCoin/backend/package-lock.json @@ -0,0 +1,936 @@ +{ + "name": "smartcoin-automation-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "smartcoin-automation-backend", + "version": "1.0.0", + "dependencies": { + "puppeteer-core": "^24.8.2" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.1.tgz", + "integrity": "sha512-zmS4RTK9fbrc++WlAJhxYbfz3IjDeOmkK/CwwbLmk7ydfS9e2CiEeRJHEPvjDVElO/bwXbidwGA37Bsm6LzCnQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz", + "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1608973", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", + "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer-core": { + "version": "24.43.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.0.tgz", + "integrity": "sha512-cCRNXsUlhyPoKDz6+TiSpfZpRS3mD6Y1YFKhkdr6ik6TMfuJb7fAtXq9ThUFc4sphxObDk3BuAvdxc1Y6YOnqQ==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.1", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT", + "optional": true + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/SmartCoin/backend/package.json b/SmartCoin/backend/package.json new file mode 100644 index 0000000..d97c869 --- /dev/null +++ b/SmartCoin/backend/package.json @@ -0,0 +1,15 @@ +{ + "name": "smartcoin-automation-backend", + "version": "1.0.0", + "private": true, + "description": "Queue and automation worker foundation for SmartCoin game top-up processing.", + "main": "workers/queueWorker.js", + "scripts": { + "worker": "node workers/queueWorker.js", + "worker:once": "node workers/queueWorker.js once", + "browser:check": "node automation/browserSession.js" + }, + "dependencies": { + "puppeteer-core": "^24.8.2" + } +} diff --git a/SmartCoin/backend/routes/.gitkeep b/SmartCoin/backend/routes/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/SmartCoin/backend/routes/.gitkeep @@ -0,0 +1 @@ + diff --git a/SmartCoin/backend/services/apiClient.js b/SmartCoin/backend/services/apiClient.js new file mode 100644 index 0000000..d4e7b67 --- /dev/null +++ b/SmartCoin/backend/services/apiClient.js @@ -0,0 +1,34 @@ +function getConfig() { + return { + baseUrl: process.env.SMARTCOIN_API_BASE || "http://127.0.0.1:8000", + adminToken: process.env.ADMIN_TOKEN || "test-token", + }; +} + +async function request(path, options = {}) { + const config = getConfig(); + const separator = path.includes("?") ? "&" : "?"; + const url = `${config.baseUrl}${path}${separator}token=${encodeURIComponent(config.adminToken)}`; + let response; + try { + response = await fetch(url, { + headers: { + "Content-Type": "application/json", + ...(options.headers || {}), + }, + ...options, + }); + } catch (error) { + const cause = error.cause?.message || error.message; + throw new Error(`Cannot reach SmartCoin API at ${config.baseUrl}: ${cause}`); + } + const payload = await response.json(); + if (!response.ok) { + throw new Error(payload.error || `Request failed with HTTP ${response.status}`); + } + return payload; +} + +module.exports = { + request, +}; diff --git a/SmartCoin/backend/services/smileAccountRouter.js b/SmartCoin/backend/services/smileAccountRouter.js new file mode 100644 index 0000000..ddecbce --- /dev/null +++ b/SmartCoin/backend/services/smileAccountRouter.js @@ -0,0 +1,28 @@ +function activeAccounts(accounts) { + return accounts.filter((account) => account.status === "active"); +} + +function selectAccount(accounts, order, strategy = "balance-aware") { + const candidates = activeAccounts(accounts); + if (candidates.length === 0) { + throw new Error("No active Smile.one accounts are available"); + } + + if (strategy === "round-robin") { + return candidates + .slice() + .sort((left, right) => { + const leftUsed = left.last_used || ""; + const rightUsed = right.last_used || ""; + return leftUsed.localeCompare(rightUsed); + })[0]; + } + + return candidates + .filter((account) => Number(account.balance || 0) >= Number(order.amount || 0)) + .sort((left, right) => Number(right.balance || 0) - Number(left.balance || 0))[0] || candidates[0]; +} + +module.exports = { + selectAccount, +}; diff --git a/SmartCoin/backend/utils/env.js b/SmartCoin/backend/utils/env.js new file mode 100644 index 0000000..cb64e44 --- /dev/null +++ b/SmartCoin/backend/utils/env.js @@ -0,0 +1,32 @@ +const fs = require("fs"); +const path = require("path"); + +function loadEnvFile(filePath) { + if (!fs.existsSync(filePath)) return; + + for (const line of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + + const index = trimmed.indexOf("="); + if (index === -1) continue; + + const key = trimmed.slice(0, index).trim(); + let value = trimmed.slice(index + 1).trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + if (!process.env[key]) process.env[key] = value; + } +} + +function loadEnv() { + loadEnvFile(path.join(__dirname, "..", ".env")); +} + +module.exports = { + loadEnv, +}; diff --git a/SmartCoin/backend/utils/logger.js b/SmartCoin/backend/utils/logger.js new file mode 100644 index 0000000..2e7c632 --- /dev/null +++ b/SmartCoin/backend/utils/logger.js @@ -0,0 +1,31 @@ +const fs = require("fs"); +const path = require("path"); + +const logDir = path.join(__dirname, "..", "..", "logs"); +const logFile = path.join(logDir, "automation-worker.log"); + +function write(level, message, meta = {}) { + fs.mkdirSync(logDir, { recursive: true }); + const entry = { + timestamp: new Date().toISOString(), + level, + message, + ...meta, + }; + const line = JSON.stringify(entry); + fs.appendFileSync(logFile, `${line}\n`); + const printer = level === "error" ? console.error : console.log; + printer(line); +} + +module.exports = { + info(message, meta) { + write("info", message, meta); + }, + warn(message, meta) { + write("warn", message, meta); + }, + error(message, meta) { + write("error", message, meta); + }, +}; diff --git a/SmartCoin/backend/workers/queueWorker.js b/SmartCoin/backend/workers/queueWorker.js new file mode 100644 index 0000000..301c568 --- /dev/null +++ b/SmartCoin/backend/workers/queueWorker.js @@ -0,0 +1,141 @@ +const fs = require("fs"); +const path = require("path"); +const { purchaseTopup } = require("../automation/smileOneAutomation"); +const { request } = require("../services/apiClient"); +const { selectAccount } = require("../services/smileAccountRouter"); +const { loadEnv } = require("../utils/env"); +const logger = require("../utils/logger"); + +loadEnv(); + +function boolFromEnv(name, fallback) { + const raw = process.env[name]; + if (raw === undefined) return fallback; + return ["1", "true", "yes", "on"].includes(raw.toLowerCase()); +} + +function numberFromEnv(name, fallback) { + const value = Number(process.env[name]); + return Number.isFinite(value) ? value : fallback; +} + +function getConfig() { + return { + dryRun: boolFromEnv("AUTOMATION_DRY_RUN", true), + intervalSeconds: numberFromEnv("AUTOMATION_INTERVAL_SECONDS", 20), + maxAttempts: numberFromEnv("AUTOMATION_MAX_ATTEMPTS", 2), + delayMs: numberFromEnv("AUTOMATION_DELAY_MS", 2500), + routingStrategy: process.env.SMILE_ROUTING_STRATEGY || "balance-aware", + queueStateFile: path.join(__dirname, "..", "..", "live-checking", "state", "queue-status.json"), + }; +} + +function writeQueueState(state) { + fs.mkdirSync(path.dirname(state.file), { recursive: true }); + fs.writeFileSync( + state.file, + JSON.stringify( + { + ok: state.ok, + pending: state.pending, + failed: state.failed, + message: state.message, + timestamp: new Date().toISOString(), + }, + null, + 2 + ) + ); +} + +async function markQueue(orderId, action, detail = "") { + return request(`/api/admin/queue/${encodeURIComponent(orderId)}/${action}`, { + method: "POST", + body: JSON.stringify({ detail }), + }); +} + +async function processNextOrder() { + const config = getConfig(); + const [{ queue }, { accounts }] = await Promise.all([ + request("/api/admin/queue"), + request("/api/admin/smile-accounts"), + ]); + const waiting = queue.filter((item) => item.status === "waiting" && item.attempts < config.maxAttempts); + const failed = queue.filter((item) => item.status === "failed").length; + + writeQueueState({ + file: config.queueStateFile, + ok: failed === 0, + pending: waiting.length, + failed, + message: failed > 0 ? "One or more automation jobs failed" : "", + }); + + if (waiting.length === 0) { + logger.info("No waiting automation jobs", { failed }); + return { processed: false }; + } + + const order = waiting[0]; + await markQueue(order.order_id, "process", "automation worker picked order"); + + try { + const account = selectAccount(accounts, order, config.routingStrategy); + const result = await purchaseTopup({ + order, + account, + dryRun: config.dryRun, + delayMs: config.delayMs, + }); + await markQueue(order.order_id, "complete", result.reference || "automation completed"); + logger.info("Automation job completed", { + orderId: order.order_id, + reference: result.reference, + dryRun: result.dryRun, + }); + return { processed: true, orderId: order.order_id }; + } catch (error) { + await markQueue(order.order_id, "fail", error.message); + logger.error("Automation job failed", { + orderId: order.order_id, + error: error.message, + }); + return { processed: false, orderId: order.order_id, error: error.message }; + } +} + +async function runLoop() { + const config = getConfig(); + logger.info("Automation worker started", { + intervalSeconds: config.intervalSeconds, + dryRun: config.dryRun, + }); + + while (true) { + try { + await processNextOrder(); + } catch (error) { + logger.error("Automation worker cycle failed", { + error: error.message, + }); + } + await new Promise((resolve) => setTimeout(resolve, config.intervalSeconds * 1000)); + } +} + +if (require.main === module) { + const once = process.argv[2] === "once"; + const runner = once ? processNextOrder : runLoop; + runner().catch((error) => { + logger.error("Automation worker failed", { + error: error.message, + stack: error.stack, + }); + process.exit(1); + }); +} + +module.exports = { + processNextOrder, +}; diff --git a/SmartCoin/bot/README.md b/SmartCoin/bot/README.md new file mode 100644 index 0000000..62326ff --- /dev/null +++ b/SmartCoin/bot/README.md @@ -0,0 +1,10 @@ +# Bot + +Telegram payment verification currently lives in the main server webhook. + +Future bot work should stay in this folder: + +- payment verification callbacks +- process / completed / retry buttons +- admin-only commands +- customer status notifications diff --git a/SmartCoin/diagrams/game-topup-automation-flowcharts.drawio b/SmartCoin/diagrams/game-topup-automation-flowcharts.drawio new file mode 100644 index 0000000..075c4e8 --- /dev/null +++ b/SmartCoin/diagrams/game-topup-automation-flowcharts.drawio @@ -0,0 +1,793 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SmartCoin/diagrams/game-topup-automation-roadmap.drawio b/SmartCoin/diagrams/game-topup-automation-roadmap.drawio new file mode 100644 index 0000000..757d260 --- /dev/null +++ b/SmartCoin/diagrams/game-topup-automation-roadmap.drawio @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SmartCoin/diagrams/generate-system-flowcharts.js b/SmartCoin/diagrams/generate-system-flowcharts.js new file mode 100644 index 0000000..ba3138b --- /dev/null +++ b/SmartCoin/diagrams/generate-system-flowcharts.js @@ -0,0 +1,361 @@ +const fs = require("fs"); +const path = require("path"); + +const outFile = path.join(__dirname, "game-topup-automation-flowcharts.drawio"); + +function esc(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function node(id, value, x, y, w, h, style) { + return ` + + `; +} + +function edge(id, from, to, label = "", color = "#64748b") { + return ` + + `; +} + +const styles = { + title: + "rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;spacing=12;", + subtitle: + "rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#cbd5e1;fontColor=#334155;fontSize=13;align=center;verticalAlign=middle;spacing=8;", + start: + "ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;", + end: + "ellipse;whiteSpace=wrap;html=1;fillColor=#e0f2fe;strokeColor=#0284c7;fontColor=#0c4a6e;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;", + process: + "rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#eff6ff;strokeColor=#2563eb;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;", + backend: + "rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;", + automation: + "rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;", + payment: + "rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;", + notify: + "rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;", + warning: + "rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fff7ed;strokeColor=#ea580c;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;", + decision: + "rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;", + data: + "shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;", + external: + "rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#ecfeff;strokeColor=#0891b2;fontColor=#164e63;fontSize=13;align=center;verticalAlign=middle;spacing=8;", + manual: + "rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f1f5f9;strokeColor=#64748b;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;", +}; + +function diagram(name, cells, width = 1400, height = 900) { + return ` + + + + +${cells.join("\n")} + + + `; +} + +function titleCells(prefix, title, subtitle) { + return [ + node(`${prefix}-title`, `${title}`, 250, 30, 900, 70, styles.title), + node(`${prefix}-sub`, subtitle, 330, 115, 740, 44, styles.subtitle), + ]; +} + +const diagrams = []; + +diagrams.push( + diagram("1. Overall System Architecture", [ + ...titleCells("arch", "Overall System Architecture", "Master diagram: customer order, payment verification, queue automation, monitoring, and notifications."), + node("arch-customer", "Customer
Places order
Uploads payment proof
Tracks status", 50, 230, 170, 105, styles.start), + node("arch-website", "Website
Frontend pages
Order form
Tracking UI", 280, 230, 170, 105, styles.process), + node("arch-api", "Backend API
Orders
Payments
Admin routes", 510, 230, 170, 105, styles.backend), + node("arch-db", "Database
orders
smile_accounts
logs
order_queue", 740, 215, 170, 130, styles.data), + node("arch-queue", "Queue
waiting
processing
completed
failed", 970, 230, 170, 105, styles.backend), + node("arch-worker", "Puppeteer Worker
Reads queue
Runs automation
Updates status", 970, 430, 170, 115, styles.automation), + node("arch-smile", "Smile.one
Login/session
Game top-up
Purchase result", 740, 430, 170, 115, styles.external), + node("arch-telegram", "Telegram Bot
Payment proof
Confirm / Reject
Admin alerts", 280, 430, 170, 115, styles.notify), + node("arch-email", "Email System
Low balance
Failures
Critical errors", 510, 620, 170, 115, styles.notify), + node("arch-admin", "Admin
Checks wallet
Controls orders
Retries failures", 50, 430, 170, 115, styles.manual), + edge("arch-e1", "arch-customer", "arch-website"), + edge("arch-e2", "arch-website", "arch-api"), + edge("arch-e3", "arch-api", "arch-db"), + edge("arch-e4", "arch-db", "arch-queue"), + edge("arch-e5", "arch-queue", "arch-worker"), + edge("arch-e6", "arch-worker", "arch-smile"), + edge("arch-e7", "arch-api", "arch-telegram", "payment notification", "#c026d3"), + edge("arch-e8", "arch-telegram", "arch-admin", "admin action", "#c026d3"), + edge("arch-e9", "arch-admin", "arch-api", "confirm / reject", "#64748b"), + edge("arch-e10", "arch-worker", "arch-email", "failure alerts", "#c026d3"), + edge("arch-e11", "arch-smile", "arch-db", "result + balance", "#0891b2"), + ]) +); + +diagrams.push( + diagram("2. Customer Order Flowchart", [ + ...titleCells("order", "Customer Order Flowchart", "How a customer creates an order and receives payment instructions."), + node("order-start", "Start", 90, 260, 120, 70, styles.start), + node("order-game", "Select game
PUBG UC or MLBB Diamonds", 270, 250, 170, 90, styles.process), + node("order-id", "Enter game ID
and server info if needed", 500, 250, 170, 90, styles.process), + node("order-package", "Select package
UC / Diamonds amount", 730, 250, 170, 90, styles.process), + node("order-payment", "Select payment method
KBZPay / AYA Pay", 960, 250, 170, 90, styles.payment), + node("order-api", "Create order API
Validate fields", 270, 450, 170, 90, styles.backend), + node("order-amount", "Generate amount
Base price + random digits
Example: 10000 + 37", 500, 440, 190, 110, styles.backend), + node("order-store", "Store order
Status: pending", 760, 450, 170, 90, styles.data), + node("order-show", "Show payment instructions
Order ID
Exact amount
Wallet number", 990, 440, 190, 110, styles.end), + edge("order-e1", "order-start", "order-game"), + edge("order-e2", "order-game", "order-id"), + edge("order-e3", "order-id", "order-package"), + edge("order-e4", "order-package", "order-payment"), + edge("order-e5", "order-payment", "order-api"), + edge("order-e6", "order-api", "order-amount"), + edge("order-e7", "order-amount", "order-store"), + edge("order-e8", "order-store", "order-show"), + ]) +); + +diagrams.push( + diagram("3. Payment Verification Flowchart", [ + ...titleCells("pay", "Payment Verification Flowchart", "KBZPay / AYA Pay screenshot verification with Telegram admin control."), + node("pay-start", "Customer pays exact amount", 80, 250, 160, 80, styles.start), + node("pay-upload", "Upload screenshot
Transaction ID optional", 300, 250, 170, 90, styles.payment), + node("pay-store-img", "Store image
backend/uploads", 530, 250, 170, 90, styles.data), + node("pay-status", "Update order
Status: waiting_verification", 760, 250, 190, 90, styles.backend), + node("pay-telegram", "Send Telegram notification
Order + screenshot + buttons", 1010, 240, 200, 110, styles.notify), + node("pay-wallet", "Admin checks wallet app
Amount / method / time", 300, 470, 190, 100, styles.manual), + node("pay-valid", "Payment valid?", 560, 460, 150, 120, styles.decision), + node("pay-confirm", "Confirm payment
Status: paid
Add to queue", 800, 450, 190, 110, styles.backend), + node("pay-reject", "Reject payment
Status: rejected
Customer can contact support", 800, 610, 190, 110, styles.warning), + node("pay-log", "Save verification log", 1050, 520, 170, 90, styles.data), + edge("pay-e1", "pay-start", "pay-upload"), + edge("pay-e2", "pay-upload", "pay-store-img"), + edge("pay-e3", "pay-store-img", "pay-status"), + edge("pay-e4", "pay-status", "pay-telegram"), + edge("pay-e5", "pay-telegram", "pay-wallet"), + edge("pay-e6", "pay-wallet", "pay-valid"), + edge("pay-e7", "pay-valid", "pay-confirm", "yes", "#16a34a"), + edge("pay-e8", "pay-valid", "pay-reject", "no", "#dc2626"), + edge("pay-e9", "pay-confirm", "pay-log"), + edge("pay-e10", "pay-reject", "pay-log"), + ]) +); + +diagrams.push( + diagram("4. Queue Processing Flowchart", [ + ...titleCells("queue", "Queue Processing Flowchart", "Safe sequential processing with retry and manual fallback."), + node("queue-new", "Paid order", 80, 260, 140, 70, styles.start), + node("queue-wait", "Waiting queue
status: waiting", 280, 250, 170, 90, styles.backend), + node("queue-worker-check", "Worker checks queue
interval loop", 520, 250, 170, 90, styles.automation), + node("queue-has-order", "Waiting order found?", 760, 235, 150, 120, styles.decision), + node("queue-idle", "No order
sleep and check again", 1010, 170, 170, 90, styles.manual), + node("queue-pick", "Pick one order
lock / mark processing", 1010, 360, 190, 90, styles.automation), + node("queue-process", "Run Puppeteer automation", 760, 520, 190, 90, styles.automation), + node("queue-success", "Success?", 520, 505, 150, 120, styles.decision), + node("queue-done", "Mark completed
Save result", 280, 440, 170, 90, styles.end), + node("queue-retry", "Attempts left?", 280, 610, 150, 120, styles.decision), + node("queue-back", "Retry once
Return to waiting", 520, 660, 170, 90, styles.warning), + node("queue-failed", "Mark failed
Manual fallback
Alert admin", 760, 660, 190, 100, styles.payment), + edge("queue-e1", "queue-new", "queue-wait"), + edge("queue-e2", "queue-wait", "queue-worker-check"), + edge("queue-e3", "queue-worker-check", "queue-has-order"), + edge("queue-e4", "queue-has-order", "queue-idle", "no", "#64748b"), + edge("queue-e5", "queue-idle", "queue-worker-check", "loop", "#64748b"), + edge("queue-e6", "queue-has-order", "queue-pick", "yes", "#16a34a"), + edge("queue-e7", "queue-pick", "queue-process"), + edge("queue-e8", "queue-process", "queue-success"), + edge("queue-e9", "queue-success", "queue-done", "yes", "#16a34a"), + edge("queue-e10", "queue-success", "queue-retry", "no", "#dc2626"), + edge("queue-e11", "queue-retry", "queue-back", "yes", "#ea580c"), + edge("queue-e12", "queue-back", "queue-wait"), + edge("queue-e13", "queue-retry", "queue-failed", "no", "#dc2626"), + ]) +); + +diagrams.push( + diagram("5. Puppeteer Automation Flowchart", [ + ...titleCells("botflow", "Puppeteer Automation Flowchart", "Detailed Smile.one browser automation path with session handling and result capture."), + node("bot-start", "Worker receives order", 60, 220, 160, 70, styles.start), + node("bot-account", "Select Smile account
from pool", 280, 210, 170, 90, styles.automation), + node("bot-launch", "Launch browser
visible test mode first", 510, 210, 170, 90, styles.automation), + node("bot-session", "Load cookies/session
userDataDir per account", 740, 210, 190, 90, styles.automation), + node("bot-open", "Open Smile.one", 990, 210, 170, 90, styles.external), + node("bot-valid", "Session valid?", 1040, 380, 150, 120, styles.decision), + node("bot-login", "Login manually/automated
Save refreshed session", 800, 395, 190, 90, styles.warning), + node("bot-game", "Navigate to game page
PUBG / MLBB", 560, 395, 190, 90, styles.external), + node("bot-fill", "Fill game ID
Validate input", 320, 395, 170, 90, styles.automation), + node("bot-package", "Select package
UC / Diamonds", 80, 395, 170, 90, styles.automation), + node("bot-confirm", "Confirm purchase
Final review", 80, 590, 170, 90, styles.payment), + node("bot-read", "Read result
Success / error text
Reference if available", 320, 580, 190, 110, styles.automation), + node("bot-ok", "Purchase successful?", 580, 575, 160, 120, styles.decision), + node("bot-save-ok", "Save completed status
Update order + queue
Update balance", 810, 560, 200, 120, styles.end), + node("bot-classify", "Classify failure
timeout / CAPTCHA / invalid ID / purchase failed", 810, 735, 220, 110, styles.payment), + node("bot-save-fail", "Save failed status
Retry or manual fallback
Alert admin", 1080, 720, 200, 120, styles.warning), + edge("bot-e1", "bot-start", "bot-account"), + edge("bot-e2", "bot-account", "bot-launch"), + edge("bot-e3", "bot-launch", "bot-session"), + edge("bot-e4", "bot-session", "bot-open"), + edge("bot-e5", "bot-open", "bot-valid"), + edge("bot-e6", "bot-valid", "bot-login", "no", "#ea580c"), + edge("bot-e7", "bot-login", "bot-game"), + edge("bot-e8", "bot-valid", "bot-game", "yes", "#16a34a"), + edge("bot-e9", "bot-game", "bot-fill"), + edge("bot-e10", "bot-fill", "bot-package"), + edge("bot-e11", "bot-package", "bot-confirm"), + edge("bot-e12", "bot-confirm", "bot-read"), + edge("bot-e13", "bot-read", "bot-ok"), + edge("bot-e14", "bot-ok", "bot-save-ok", "yes", "#16a34a"), + edge("bot-e15", "bot-ok", "bot-classify", "no", "#dc2626"), + edge("bot-e16", "bot-classify", "bot-save-fail"), + ], 1400, 950) +); + +diagrams.push( + diagram("6. Smile Account Pool Flowchart", [ + ...titleCells("pool", "Smile Account Pool Flowchart", "Multi-account routing using active status, balance, and round robin usage."), + node("pool-start", "Worker needs account", 80, 270, 160, 70, styles.start), + node("pool-table", "Read smile_accounts table", 300, 250, 170, 100, styles.data), + node("pool-active", "Filter active accounts", 530, 250, 170, 100, styles.automation), + node("pool-any", "Any active account?", 760, 240, 150, 120, styles.decision), + node("pool-alert", "No account available
Pause job
Alert admin", 1000, 190, 190, 100, styles.payment), + node("pool-balance", "Check balance
Enough for package?", 1000, 380, 190, 100, styles.automation), + node("pool-enough", "Enough balance?", 760, 390, 150, 120, styles.decision), + node("pool-route", "Apply routing strategy
Balance-aware or round robin", 530, 420, 190, 100, styles.backend), + node("pool-assign", "Assign account to worker", 300, 430, 170, 90, styles.end), + node("pool-update", "After purchase
Update balance
Update last_used", 80, 420, 170, 110, styles.data), + edge("pool-e1", "pool-start", "pool-table"), + edge("pool-e2", "pool-table", "pool-active"), + edge("pool-e3", "pool-active", "pool-any"), + edge("pool-e4", "pool-any", "pool-alert", "no", "#dc2626"), + edge("pool-e5", "pool-any", "pool-balance", "yes", "#16a34a"), + edge("pool-e6", "pool-balance", "pool-enough"), + edge("pool-e7", "pool-enough", "pool-alert", "no", "#dc2626"), + edge("pool-e8", "pool-enough", "pool-route", "yes", "#16a34a"), + edge("pool-e9", "pool-route", "pool-assign"), + edge("pool-e10", "pool-assign", "pool-update"), + ]) +); + +diagrams.push( + diagram("7. Balance Monitoring Flowchart", [ + ...titleCells("balance", "Balance Monitoring Flowchart", "Periodic Smile.one balance checking with warning and critical alerts."), + node("balance-start", "Periodic checker
every 5 minutes", 80, 270, 170, 80, styles.start), + node("balance-accounts", "Read active Smile accounts", 310, 260, 180, 90, styles.data), + node("balance-read", "Open/read Smile balance
per account", 550, 260, 190, 90, styles.automation), + node("balance-save", "Save latest balance", 800, 260, 170, 90, styles.data), + node("balance-threshold", "Below threshold?", 1030, 245, 150, 120, styles.decision), + node("balance-ok", "Balance OK
No alert
Next interval", 1030, 450, 170, 90, styles.end), + node("balance-level", "Select level
Warning: 50000 MMK
Critical: 10000 MMK", 780, 450, 200, 110, styles.warning), + node("balance-telegram", "Send Telegram alert", 530, 460, 170, 90, styles.notify), + node("balance-email", "Send Email alert", 310, 460, 170, 90, styles.notify), + node("balance-log", "Save monitoring log", 90, 460, 170, 90, styles.data), + edge("balance-e1", "balance-start", "balance-accounts"), + edge("balance-e2", "balance-accounts", "balance-read"), + edge("balance-e3", "balance-read", "balance-save"), + edge("balance-e4", "balance-save", "balance-threshold"), + edge("balance-e5", "balance-threshold", "balance-ok", "no", "#16a34a"), + edge("balance-e6", "balance-threshold", "balance-level", "yes", "#ea580c"), + edge("balance-e7", "balance-level", "balance-telegram"), + edge("balance-e8", "balance-telegram", "balance-email"), + edge("balance-e9", "balance-email", "balance-log"), + ]) +); + +diagrams.push( + diagram("8. Notification System Flowchart", [ + ...titleCells("notify", "Notification System Flowchart", "Telegram and email alerts with typed messages, cooldown, and logging."), + node("notify-trigger", "Trigger event
low balance / failure / critical / session expired", 80, 250, 210, 100, styles.start), + node("notify-type", "Select alert type", 350, 260, 170, 80, styles.notify), + node("notify-cooldown", "Cooldown active?", 580, 245, 150, 120, styles.decision), + node("notify-skip", "Skip duplicate alert
Save skipped log", 810, 180, 190, 90, styles.manual), + node("notify-compose", "Compose message
Order/account details
Error context", 810, 360, 190, 110, styles.notify), + node("notify-tg", "Send Telegram", 570, 470, 170, 90, styles.notify), + node("notify-email", "Send Email", 350, 470, 170, 90, styles.notify), + node("notify-result", "Delivery successful?", 120, 455, 150, 120, styles.decision), + node("notify-log-ok", "Save sent log", 120, 650, 170, 80, styles.data), + node("notify-log-fail", "Save failure log
Keep admin-visible", 350, 650, 190, 90, styles.payment), + edge("notify-e1", "notify-trigger", "notify-type"), + edge("notify-e2", "notify-type", "notify-cooldown"), + edge("notify-e3", "notify-cooldown", "notify-skip", "yes", "#64748b"), + edge("notify-e4", "notify-cooldown", "notify-compose", "no", "#16a34a"), + edge("notify-e5", "notify-compose", "notify-tg"), + edge("notify-e6", "notify-tg", "notify-email"), + edge("notify-e7", "notify-email", "notify-result"), + edge("notify-e8", "notify-result", "notify-log-ok", "yes", "#16a34a"), + edge("notify-e9", "notify-result", "notify-log-fail", "no", "#dc2626"), + ]) +); + +diagrams.push( + diagram("9. Error Handling Flowchart", [ + ...titleCells("error", "Error Handling Flowchart", "Failure classification, retry, manual fallback, and admin alerting."), + node("error-start", "Failure detected", 80, 250, 160, 70, styles.start), + node("error-classify", "Classify error
Browser crash
CAPTCHA
Timeout
Invalid ID
Purchase failure", 300, 220, 200, 130, styles.payment), + node("error-browser", "Browser crash?
Restart browser/session", 560, 190, 180, 100, styles.warning), + node("error-captcha", "CAPTCHA?
Pause automation
Manual login required", 560, 340, 180, 100, styles.warning), + node("error-retry", "Retry available?", 820, 245, 150, 120, styles.decision), + node("error-run-retry", "Retry once
Same or new account", 1060, 190, 180, 90, styles.automation), + node("error-success", "Retry successful?", 1060, 350, 150, 120, styles.decision), + node("error-complete", "Mark completed", 820, 520, 170, 80, styles.end), + node("error-failed", "Mark failed
Manual fallback", 560, 520, 180, 90, styles.payment), + node("error-alert", "Alert admin
Telegram + Email", 320, 520, 180, 90, styles.notify), + node("error-log", "Save error log
Stack + screenshot
Order context", 80, 500, 180, 120, styles.data), + edge("error-e1", "error-start", "error-classify"), + edge("error-e2", "error-classify", "error-browser"), + edge("error-e3", "error-classify", "error-captcha"), + edge("error-e4", "error-browser", "error-retry"), + edge("error-e5", "error-captcha", "error-retry"), + edge("error-e6", "error-retry", "error-run-retry", "yes", "#ea580c"), + edge("error-e7", "error-run-retry", "error-success"), + edge("error-e8", "error-success", "error-complete", "yes", "#16a34a"), + edge("error-e9", "error-success", "error-failed", "no", "#dc2626"), + edge("error-e10", "error-retry", "error-failed", "no", "#dc2626"), + edge("error-e11", "error-failed", "error-alert"), + edge("error-e12", "error-alert", "error-log"), + ]) +); + +diagrams.push( + diagram("10. Admin Dashboard Flowchart", [ + ...titleCells("admin", "Admin Dashboard Flowchart", "Admin control panel for payments, orders, balances, retries, and logs."), + node("admin-login", "Admin login
Protected route/token", 80, 250, 170, 90, styles.start), + node("admin-dashboard", "Open dashboard", 310, 250, 170, 90, styles.process), + node("admin-orders", "View orders
pending / paid / completed / failed", 540, 170, 190, 100, styles.backend), + node("admin-payments", "Approve payments
Confirm / Reject", 540, 320, 190, 90, styles.payment), + node("admin-balances", "Monitor balances
Warning / critical", 790, 170, 190, 100, styles.external), + node("admin-retry", "Retry failed orders
Return to queue", 790, 320, 190, 90, styles.warning), + node("admin-logs", "View logs
payment / automation / errors", 1040, 170, 190, 100, styles.data), + node("admin-actions", "Admin action API
Update database
Trigger notifications", 1040, 320, 190, 110, styles.backend), + node("admin-done", "System state updated", 650, 560, 190, 80, styles.end), + edge("admin-e1", "admin-login", "admin-dashboard"), + edge("admin-e2", "admin-dashboard", "admin-orders"), + edge("admin-e3", "admin-dashboard", "admin-payments"), + edge("admin-e4", "admin-dashboard", "admin-balances"), + edge("admin-e5", "admin-dashboard", "admin-retry"), + edge("admin-e6", "admin-dashboard", "admin-logs"), + edge("admin-e7", "admin-orders", "admin-actions"), + edge("admin-e8", "admin-payments", "admin-actions"), + edge("admin-e9", "admin-balances", "admin-actions"), + edge("admin-e10", "admin-retry", "admin-actions"), + edge("admin-e11", "admin-logs", "admin-actions"), + edge("admin-e12", "admin-actions", "admin-done"), + ]) +); + +const xml = ` +${diagrams.join("\n")} + +`; + +fs.writeFileSync(outFile, xml); +console.log(outFile); diff --git a/SmartCoin/frontend/admin.html b/SmartCoin/frontend/admin.html new file mode 100644 index 0000000..2a5f468 --- /dev/null +++ b/SmartCoin/frontend/admin.html @@ -0,0 +1,49 @@ + + + + + + Admin - Game Topup + + + + + + + + +
+
+

ADMIN DASHBOARD

+

Private order verification and fulfillment controls.

+
+ +
+

Orders

+
+ + +
+
+
+
+ + + + + diff --git a/SmartCoin/frontend/app.js b/SmartCoin/frontend/app.js new file mode 100644 index 0000000..4bcd351 --- /dev/null +++ b/SmartCoin/frontend/app.js @@ -0,0 +1,341 @@ +let config = null; +let activeOrderId = null; +let timerHandle = null; +let selectedCurrency = "MMK"; + +const $ = (selector) => document.querySelector(selector); +const $$ = (selector) => Array.from(document.querySelectorAll(selector)); + +async function api(path, options = {}) { + const response = await fetch(path, options); + const payload = await response.json(); + if (!response.ok) throw new Error(payload.error || "Request failed"); + return payload; +} + +function money(amount) { + if (selectedCurrency === "USD") return `$${(Number(amount) / 3500).toFixed(2)}`; + return `${Number(amount).toLocaleString()} MMK`; +} + +function showModal(title, body) { + if (!$("#modalBackdrop")) return alert(title); + $("#modalTitle").textContent = title; + $("#modalBody").innerHTML = body; + $("#modalBackdrop").hidden = false; +} + +function closeModal() { + if ($("#modalBackdrop")) $("#modalBackdrop").hidden = true; +} + +function setStatus(status) { + if (!$("#statusBadge")) return; + $("#statusBadge").textContent = status; + $("#statusBadge").className = `badge ${status}`; +} + +function renderProducts() { + if (!config || !$("#gameSelect") || !$("#productSelect")) return; + const game = $("#gameSelect").value; + const products = config.menu[game] || {}; + $("#productSelect").innerHTML = Object.entries(products) + .map(([name, price]) => ``) + .join(""); + renderProductPreview(); + if ($("#selectedGameTitle")) $("#selectedGameTitle").textContent = game || "Choose Product"; +} + +function renderProductPreview() { + if (!config || !$("#productPreview") || !$("#gameSelect") || !$("#productSelect")) return; + const game = $("#gameSelect").value; + const product = $("#productSelect").value; + const price = config.menu[game]?.[product]; + $("#productPreview").textContent = price + ? `${product}: ${money(price)} before unique payment digits` + : "Select a product to see price."; +} + +function renderTrack(order, target) { + if (!target) return; + const completeText = + order.status === "completed" + ? `
Your order has been completed successfully.
` + : ""; + target.innerHTML = ` +
+
+ ${order.order_id} + ${order.status} +
+
+ Game: ${order.game} + User ID: ${order.user_game_id} + Product: ${order.product} + Amount: ${money(order.amount)} + Payment: ${order.payment_method || "-"} + Created: ${order.created_at} +
+ ${completeText} +
+ `; +} + +async function refreshOrder(orderId) { + if (!orderId) return; + const { order } = await api(`/api/orders/${encodeURIComponent(orderId)}`); + setStatus(order.status); + renderTrack(order, $("#trackResult")); +} + +function startTimer(expiresAt) { + if (!$("#timer")) return; + clearInterval(timerHandle); + const tick = () => { + const remaining = Math.max(0, expiresAt - Math.floor(Date.now() / 1000)); + const mins = String(Math.floor(remaining / 60)).padStart(2, "0"); + const secs = String(remaining % 60).padStart(2, "0"); + $("#timer").textContent = `${mins}:${secs}`; + if (remaining === 0) { + clearInterval(timerHandle); + refreshOrder(activeOrderId); + } + }; + tick(); + timerHandle = setInterval(tick, 1000); +} + +function renderAdminOrder(order, token) { + const screenshot = order.screenshot + ? `View screenshot` + : `No screenshot`; + return ` +
+
+ ${order.order_id} + ${order.status} +
+
+ ${order.game} + ${order.user_game_id} + ${order.product} + ${money(order.amount)} + ${order.payment_method || "-"} + ${screenshot} +
+
+ + + +
+
+ `; +} + +function renderPricingPage() { + if (!config || !$("#pricingGrid")) return; + $("#pricingGrid").innerHTML = Object.entries(config.menu) + .map(([game, products]) => { + const rows = Object.entries(products) + .map(([product, price]) => ` +
+ ${product} + ${money(price)} +
+ `) + .join(""); + return ` +
+

${game}

+
${rows}
+ Top Up ${game} +
+ `; + }) + .join(""); +} + +function setupSharedControls() { + $("#currencyButton")?.addEventListener("click", () => { + const menu = $("#currencyMenu"); + if (!menu) return; + menu.hidden = !menu.hidden; + $("#currencyButton").setAttribute("aria-expanded", String(!menu.hidden)); + }); + + $$("[data-currency]").forEach((button) => { + button.addEventListener("click", () => { + selectedCurrency = button.dataset.currency; + if ($("#currencyButton")) $("#currencyButton").children[1].textContent = selectedCurrency; + if ($("#currencyMenu")) $("#currencyMenu").hidden = true; + renderProductPreview(); + renderPricingPage(); + if (activeOrderId) refreshOrder(activeOrderId); + }); + }); + + $("#loginButton")?.addEventListener("click", () => { + showModal("Login / Register", "

Account login is not required for checkout yet. Customers can place an order directly and track it with the Order ID.

"); + }); + + $$("[data-modal-link]").forEach((link) => { + link.addEventListener("click", (event) => { + event.preventDefault(); + const content = { + terms: ["Terms of Service", "

Orders are processed after manual wallet verification. Wrong game IDs, wrong products, or mismatched payments may be rejected.

"], + privacy: ["Privacy Policy", "

Only order details, payment method, uploaded screenshot, and transaction notes are stored for verification and support.

"], + refund: ["Refund Policy", "

Rejected or unpaid orders are not processed. Refund requests must include the Order ID and wallet transaction proof.

"], + }[link.dataset.modalLink]; + if (content) showModal(content[0], content[1]); + }); + }); + + $$("[data-social]").forEach((button) => { + button.addEventListener("click", () => { + showModal(button.dataset.social, `

${button.dataset.social} link is not configured yet. Add your real page/link when ready.

`); + }); + }); + + $("#modalClose")?.addEventListener("click", closeModal); + $("#modalBackdrop")?.addEventListener("click", (event) => { + if (event.target.id === "modalBackdrop") closeModal(); + }); + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") closeModal(); + }); +} + +function setupHomePage() { + $$("[data-game-link]").forEach((card) => { + card.addEventListener("click", (event) => { + event.preventDefault(); + window.location.href = `/orders.html?game=${encodeURIComponent(card.dataset.gameLink)}`; + }); + }); +} + +function setupOrdersPage() { + if (!$("#orderForm")) return; + + $("#gameSelect").innerHTML = Object.keys(config.menu) + .map((game) => ``) + .join(""); + + const gameParam = new URLSearchParams(window.location.search).get("game"); + if (gameParam && config.menu[gameParam]) $("#gameSelect").value = gameParam; + renderProducts(); + + $("#buyButton").disabled = false; + $("#buyButton").textContent = "Buy"; + $("#gameSelect").addEventListener("change", renderProducts); + $("#productSelect").addEventListener("change", renderProductPreview); + + if ($("#kbzpay")) $("#kbzpay").textContent = config.kbzpay_number; + if ($("#ayapay")) $("#ayapay").textContent = config.ayapay_number; + if ($("#accountName")) $("#accountName").textContent = config.account_name; + if ($("#ayaQrImage") && config.ayapay_qr_path) { + $("#ayaQrImage").src = config.ayapay_qr_path; + $("#ayaQrPanel").hidden = false; + $("#ayaQrImage").addEventListener("error", () => { + $("#ayaQrPanel").hidden = true; + }); + } + + $("#orderForm").addEventListener("submit", async (event) => { + event.preventDefault(); + const payload = Object.fromEntries(new FormData(event.currentTarget).entries()); + try { + const order = await api("/api/orders", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + activeOrderId = order.order_id; + $("#paymentPanel").hidden = false; + $("#orderId").textContent = order.order_id; + $("#amount").textContent = money(order.amount); + setStatus("pending"); + startTimer(Math.floor(Date.now() / 1000) + config.expiry_seconds); + $("#paymentPanel").scrollIntoView({ behavior: "smooth", block: "start" }); + } catch (error) { + alert(error.message); + } + }); + + $("#paymentForm")?.addEventListener("submit", async (event) => { + event.preventDefault(); + if (!activeOrderId) return; + try { + const { order } = await api(`/api/orders/${encodeURIComponent(activeOrderId)}/payment`, { + method: "POST", + body: new FormData(event.currentTarget), + }); + setStatus(order.status); + showModal("Payment Uploaded", "

Screenshot uploaded. Please wait for manual wallet verification.

"); + } catch (error) { + alert(error.message); + } + }); +} + +function setupTrackPage() { + $("#trackForm")?.addEventListener("submit", async (event) => { + event.preventDefault(); + const orderId = new FormData(event.currentTarget).get("track_order_id"); + try { + const { order } = await api(`/api/orders/${encodeURIComponent(orderId)}`); + renderTrack(order, $("#trackResult")); + } catch (error) { + $("#trackResult").innerHTML = `
${error.message}
`; + } + }); +} + +function setupAdmin() { + $("#adminForm")?.addEventListener("submit", async (event) => { + event.preventDefault(); + const token = new FormData(event.currentTarget).get("token"); + try { + const { orders } = await api(`/api/admin/orders?token=${encodeURIComponent(token)}`); + $("#adminOrders").innerHTML = orders.length + ? orders.map((order) => renderAdminOrder(order, token)).join("") + : `
No orders yet.
`; + } catch (error) { + $("#adminOrders").innerHTML = `
${error.message}
`; + } + }); + + $("#adminOrders")?.addEventListener("click", async (event) => { + const button = event.target.closest("[data-admin-action]"); + if (!button) return; + const { adminAction, orderId, token } = button.dataset; + try { + await api(`/api/admin/orders/${orderId}/${adminAction}?token=${encodeURIComponent(token)}`, { + method: "POST", + }); + $("#adminForm").requestSubmit ? $("#adminForm").requestSubmit() : $("#adminForm").dispatchEvent(new Event("submit", { cancelable: true, bubbles: true })); + } catch (error) { + alert(error.message); + } + }); +} + +async function init() { + setupSharedControls(); + setupHomePage(); + try { + config = await api("/api/config"); + setupOrdersPage(); + setupTrackPage(); + setupAdmin(); + renderPricingPage(); + } catch (error) { + if ($("#buyButton")) { + $("#buyButton").disabled = true; + $("#buyButton").textContent = "Menu unavailable"; + } + showModal("Menu unavailable", `

${error.message}

`); + } +} + +init(); diff --git a/SmartCoin/frontend/assets/ml-card.png b/SmartCoin/frontend/assets/ml-card.png new file mode 100644 index 0000000..695fc91 Binary files /dev/null and b/SmartCoin/frontend/assets/ml-card.png differ diff --git a/SmartCoin/frontend/assets/ml-card.svg b/SmartCoin/frontend/assets/ml-card.svg new file mode 100644 index 0000000..c631732 --- /dev/null +++ b/SmartCoin/frontend/assets/ml-card.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + MOBILE + LEGENDS + diff --git a/SmartCoin/frontend/assets/pubg-card.png b/SmartCoin/frontend/assets/pubg-card.png new file mode 100644 index 0000000..544e86d Binary files /dev/null and b/SmartCoin/frontend/assets/pubg-card.png differ diff --git a/SmartCoin/frontend/assets/pubg-card.svg b/SmartCoin/frontend/assets/pubg-card.svg new file mode 100644 index 0000000..2252f90 --- /dev/null +++ b/SmartCoin/frontend/assets/pubg-card.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + PUBG + MOBILE + diff --git a/SmartCoin/frontend/aya-pay-qr.png b/SmartCoin/frontend/aya-pay-qr.png new file mode 100644 index 0000000..ef8e212 Binary files /dev/null and b/SmartCoin/frontend/aya-pay-qr.png differ diff --git a/SmartCoin/frontend/how-to-topup.html b/SmartCoin/frontend/how-to-topup.html new file mode 100644 index 0000000..431dea9 --- /dev/null +++ b/SmartCoin/frontend/how-to-topup.html @@ -0,0 +1,39 @@ + + + + + + How to Topup - Game Topup + + + + +
+

HOW TO TOPUP

Follow these steps to complete your order correctly.

+
+

Topup Steps

+
+ 1. Go to Orders and choose PUBG Mobile or Mobile Legends. + 2. Enter your user game ID exactly as shown in game. + 3. Select the product and click Buy. + 4. Pay the exact amount shown, including the unique digits. + 5. Upload a clear payment screenshot and optional transaction ID. + 6. Wait while payment is manually verified in KBZPay or AYA Pay. + 7. When completed, check your order status on the Track Order page. +
+
+
+ + + + diff --git a/SmartCoin/frontend/index.html b/SmartCoin/frontend/index.html new file mode 100644 index 0000000..bfce4b4 --- /dev/null +++ b/SmartCoin/frontend/index.html @@ -0,0 +1,111 @@ + + + + + + Game Topup + + + + + + + + +
+
+
+

CHOOSE YOUR GAME

+

Top up your favorite game instantly and securely

+
+ +
+ + PUBG Mobile top up + + + + Mobile Legends top up + +
+ +
+
+ + 100% SAFE +

Your payment and account are fully secured.

+
+
+ + INSTANT DELIVERY +

Top up instantly after payment confirmed.

+
+
+ + 24/7 SUPPORT +

Our support team is always ready to help you.

+
+
+ + TRUSTED SERVICE +

Thousands of players trust our service.

+
+
+
+
+ + + + + + + + diff --git a/SmartCoin/frontend/orders.html b/SmartCoin/frontend/orders.html new file mode 100644 index 0000000..dea9e8a --- /dev/null +++ b/SmartCoin/frontend/orders.html @@ -0,0 +1,107 @@ + + + + + + Orders - Game Topup + + + + + + + + +
+
+

PLACE ORDER

+

Select game, product, and upload payment proof after checkout.

+
+ +
+
+
+ Selected game +

Choose Product

+
+ +
+ + + +
Select a product to see price.
+ +
+
+ + +
+
+ + + + + diff --git a/SmartCoin/frontend/pricing.html b/SmartCoin/frontend/pricing.html new file mode 100644 index 0000000..0844cbe --- /dev/null +++ b/SmartCoin/frontend/pricing.html @@ -0,0 +1,28 @@ + + + + + + Pricing - Game Topup + + + + +
+

PRODUCT PRICING

Base prices before unique payment digits are added at checkout.

+
+
+ + + + diff --git a/SmartCoin/frontend/styles.css b/SmartCoin/frontend/styles.css new file mode 100644 index 0000000..d86f8dc --- /dev/null +++ b/SmartCoin/frontend/styles.css @@ -0,0 +1,1049 @@ +:root { + color-scheme: dark; + --page: #061027; + --panel: rgba(9, 20, 47, 0.88); + --panel-strong: rgba(13, 26, 61, 0.96); + --line: rgba(148, 163, 184, 0.24); + --ink: #f8fafc; + --muted: #c7d2fe; + --yellow: #ffc400; + --yellow-strong: #f4a900; + --cyan: #22d3ee; + --purple: #6d5dfc; + --danger: #ff5a66; + --ok: #34d399; + --warn: #fbbf24; +} + +* { + box-sizing: border-box; +} + +[hidden] { + display: none !important; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-height: 100vh; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: + radial-gradient(circle at 6% 48%, rgba(99, 46, 222, 0.34), transparent 28%), + radial-gradient(circle at 93% 42%, rgba(37, 99, 235, 0.24), transparent 30%), + linear-gradient(180deg, #081226 0%, #060d21 52%, #050914 100%); + color: var(--ink); +} + +body::before { + content: ""; + position: fixed; + inset: 92px 0 auto; + height: 610px; + pointer-events: none; + background: + linear-gradient(135deg, transparent 0 18%, rgba(88, 28, 135, 0.24) 18% 24%, transparent 24% 76%, rgba(30, 64, 175, 0.18) 76% 83%, transparent 83%), + radial-gradient(circle at 50% 0%, rgba(59, 130, 246, 0.1), transparent 62%); +} + +.site-header { + position: sticky; + top: 0; + z-index: 20; + height: 92px; + padding: 0 46px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + background: rgba(4, 10, 26, 0.92); + border-bottom: 1px solid var(--line); + backdrop-filter: blur(16px); +} + +.logo, +.main-nav a, +.site-footer a { + color: inherit; + text-decoration: none; +} + +.logo { + display: flex; + align-items: center; + gap: 12px; + min-width: 236px; +} + +.logo-mark { + width: 52px; + height: 52px; + display: grid; + place-items: center; + border-radius: 50%; + background: var(--yellow); + color: #061027; + font-size: 25px; +} + +.logo strong { + display: block; + font-size: 21px; + line-height: 1; + letter-spacing: 0; +} + +.logo small { + display: block; + margin-top: 7px; + color: #e5e7eb; + font-size: 12px; + font-weight: 800; +} + +.main-nav { + display: flex; + align-items: center; + justify-content: center; + gap: 32px; + flex: 1; +} + +.main-nav a { + position: relative; + min-height: 92px; + display: inline-flex; + align-items: center; + color: #d7dce8; + font-size: 16px; + font-weight: 650; +} + +.main-nav a.active, +.main-nav a:hover { + color: var(--yellow); +} + +.main-nav a.active::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 3px; + background: var(--yellow); +} + +.header-actions { + position: relative; + display: flex; + align-items: center; + gap: 24px; +} + +button { + border: 0; + min-height: 48px; + border-radius: 7px; + padding: 0 20px; + font: inherit; + font-weight: 850; + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.58; +} + +.currency-button { + display: inline-flex; + align-items: center; + gap: 10px; + min-width: 142px; + justify-content: center; + color: #eef2ff; + background: rgba(15, 23, 42, 0.72); + border: 1px solid var(--line); +} + +.currency-menu { + position: absolute; + top: calc(100% + 10px); + right: 190px; + min-width: 142px; + padding: 8px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(8, 18, 44, 0.98); + box-shadow: 0 18px 38px rgba(0, 0, 0, 0.34); +} + +.currency-menu button { + width: 100%; + min-height: 38px; + padding: 0 10px; + color: #f8fafc; + background: transparent; + box-shadow: none; + text-align: left; +} + +.currency-menu button:hover { + color: #111827; + background: var(--yellow); +} + +.login-button, +.game-card button, +.stack > button, +.inline-form button { + color: #111827; + background: linear-gradient(180deg, #ffd119 0%, #ffb800 100%); + box-shadow: 0 8px 18px rgba(255, 196, 0, 0.22); +} + +.login-button:hover, +.game-card button:hover, +.stack > button:hover, +.inline-form button:hover { + background: linear-gradient(180deg, #ffe054 0%, #f6b000 100%); +} + +.hero { + position: relative; + z-index: 1; + width: min(1428px, calc(100% - 88px)); + margin: 0 auto; + padding: 42px 0 34px; +} + +.page-main { + position: relative; + z-index: 1; + width: min(1120px, calc(100% - 40px)); + margin: 0 auto; + padding: 42px 0 44px; +} + +.narrow-main { + width: min(860px, calc(100% - 40px)); +} + +.page-title { + text-align: center; + margin-bottom: 28px; +} + +.page-title h1 { + margin: 0; + font-size: clamp(34px, 4vw, 52px); + line-height: 1; + font-weight: 950; +} + +.page-title h1 span { + color: var(--yellow); +} + +.page-title p { + margin: 12px 0 0; + color: #e2e8f0; + font-size: 18px; +} + +.pricing-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} + +.price-panel { + display: grid; + gap: 16px; +} + +.price-list { + display: grid; + gap: 10px; +} + +.price-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 13px 14px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(15, 23, 42, 0.62); +} + +.price-row strong { + color: var(--yellow); + white-space: nowrap; +} + +.price-action { + min-height: 46px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 7px; + color: #111827; + background: linear-gradient(180deg, #ffd119 0%, #ffb800 100%); + font-weight: 850; + text-decoration: none; +} + +.hero-copy { + text-align: center; + margin-bottom: 34px; +} + +.hero-copy h1 { + margin: 0; + font-size: clamp(36px, 4vw, 54px); + line-height: 1; + letter-spacing: 0; + font-weight: 950; + text-shadow: 0 3px 0 rgba(255, 255, 255, 0.14); +} + +.hero-copy h1 span { + color: var(--yellow); +} + +.hero-copy p { + margin: 14px 0 0; + color: #f1f5f9; + font-size: 22px; +} + +.game-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 28px; +} + +.game-card { + position: relative; + min-height: 0; + aspect-ratio: 1.5; + overflow: hidden; + border-radius: 18px; + border: 2px solid transparent; + background: var(--panel-strong); + isolation: isolate; + cursor: pointer; + box-shadow: 0 24px 58px rgba(0, 0, 0, 0.28); + color: inherit; + text-decoration: none; +} + +.game-card::before, +.game-card::after { + content: ""; + position: absolute; + inset: 0; + z-index: -2; + display: none; +} + +.game-card-image { + position: absolute; + inset: 0; + z-index: 0; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 1; +} + +.pubg-card { + border-color: rgba(255, 196, 0, 0.82); +} + +.pubg-card::before { + z-index: 0; + background: linear-gradient(90deg, rgba(6, 9, 18, 0.9) 0%, rgba(9, 16, 31, 0.62) 44%, rgba(17, 24, 39, 0.06) 100%); +} + +.ml-card { + border-color: rgba(76, 83, 255, 0.9); +} + +.ml-card::before { + z-index: 0; + background: linear-gradient(90deg, rgba(17, 24, 69, 0.88) 0%, rgba(31, 41, 105, 0.55) 48%, rgba(50, 37, 126, 0.04) 100%); +} + +.game-card-content { + position: relative; + z-index: 2; + width: min(430px, 68%); + height: 100%; + padding: 34px; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.game-logo { + margin-bottom: auto; + color: #fff; + font-weight: 950; + line-height: 0.92; + text-shadow: 0 5px 16px rgba(0, 0, 0, 0.38); +} + +.pubg-logo { + padding: 10px 14px; + border: 5px solid #fff; + font-size: 42px; +} + +.pubg-logo span, +.ml-logo span { + font-size: 24px; +} + +.ml-logo { + color: #ffe7a6; + font-size: 34px; +} + +.game-card h2 { + margin: 58px 0 8px; + font-size: 34px; + line-height: 1.05; + letter-spacing: 0; + font-weight: 950; +} + +.game-card p { + margin: 0 0 52px; + max-width: 330px; + color: #fff; + font-size: 17px; + line-height: 1.55; +} + +.game-card button { + display: inline-flex; + align-items: center; + gap: 16px; + min-width: 174px; + justify-content: center; + font-size: 17px; +} + +.game-card button span { + font-size: 30px; + line-height: 0; +} + +.game-art { + position: absolute; + inset: 0; + pointer-events: none; +} + +.helmet { + position: absolute; + right: 122px; + top: 92px; + width: 106px; + height: 92px; + border-radius: 48% 48% 42% 42%; + background: linear-gradient(145deg, #9ca3af, #1f2937 72%); + box-shadow: inset -16px -20px 0 rgba(0, 0, 0, 0.36), 0 16px 28px rgba(0, 0, 0, 0.3); +} + +.helmet::after { + content: ""; + position: absolute; + left: 12px; + right: 12px; + bottom: 22px; + height: 20px; + border-radius: 14px; + background: #111827; +} + +.blast { + position: absolute; + right: 4%; + bottom: -22%; + width: 390px; + height: 390px; + border-radius: 50%; + background: radial-gradient(circle, rgba(251, 191, 36, 0.95), rgba(249, 115, 22, 0.58) 42%, transparent 68%); + filter: blur(2px); +} + +.ml-art .crystal { + position: absolute; + width: 82px; + height: 138px; + clip-path: polygon(48% 0, 100% 34%, 68% 100%, 15% 76%, 0 28%); + background: linear-gradient(160deg, #dbeafe, #38bdf8 45%, #312e81); + opacity: 0.82; +} + +.ml-art .one { + right: 13%; + top: 18%; + transform: rotate(18deg); +} + +.ml-art .two { + right: 35%; + bottom: 8%; + transform: rotate(-28deg) scale(0.78); +} + +.blade { + position: absolute; + right: -5%; + bottom: 13%; + width: 420px; + height: 34px; + border-radius: 50%; + background: linear-gradient(90deg, transparent, #bae6fd 18%, #2563eb 58%, transparent); + transform: rotate(-24deg); + box-shadow: 0 0 28px rgba(34, 211, 238, 0.8); +} + +.trust-strip { + margin-top: 34px; + border: 1px solid var(--line); + border-radius: 14px; + background: rgba(9, 20, 47, 0.76); + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 20px 52px rgba(0, 0, 0, 0.18); +} + +.trust-strip div { + min-height: 148px; + padding: 30px 34px; + display: grid; + grid-template-columns: 56px minmax(0, 1fr); + column-gap: 20px; + align-content: center; +} + +.trust-strip div + div { + border-left: 1px solid var(--line); +} + +.trust-icon { + grid-row: span 2; + color: var(--yellow); + font-size: 48px; + line-height: 1; +} + +.trust-strip strong { + font-size: 17px; + align-self: end; +} + +.trust-strip p { + margin: 7px 0 0; + color: #f8fafc; + line-height: 1.45; +} + +.shop-layout { + position: relative; + z-index: 1; + width: min(1120px, calc(100% - 40px)); + margin: 0 auto 34px; + display: grid; + grid-template-columns: 1fr 1.15fr; + gap: 18px; + align-items: start; +} + +.order-layout { + width: min(720px, calc(100% - 40px)); + grid-template-columns: minmax(0, 1fr); + justify-content: center; +} + +.order-layout .order-panel, +.order-layout .payment-panel { + grid-column: 1; +} + +.panel { + background: rgba(8, 18, 44, 0.86); + border: 1px solid var(--line); + border-radius: 12px; + padding: 22px; + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.24); +} + +.payment-panel, +.tracking-panel, +.admin-panel, +.help-panel { + grid-column: 2; +} + +.single-panel { + grid-column: auto; +} + +.section-heading { + margin-bottom: 18px; +} + +.section-heading span, +.muted, +.payment-grid span, +.amount-box span, +.qr-panel span { + color: var(--muted); + font-size: 13px; +} + +h2 { + margin: 0; + font-size: 24px; +} + +.stack { + display: grid; + gap: 14px; +} + +label { + display: grid; + gap: 7px; + color: #e2e8f0; + font-size: 14px; + font-weight: 750; +} + +input, +select { + width: 100%; + min-height: 46px; + border-radius: 7px; + border: 1px solid var(--line); + padding: 0 12px; + color: var(--ink); + background: rgba(15, 23, 42, 0.9); + font: inherit; +} + +input[type="file"] { + padding: 11px 12px; +} + +.product-preview, +.notice { + padding: 12px; + border-radius: 8px; + background: rgba(15, 23, 42, 0.82); + border: 1px solid var(--line); + color: #dbeafe; +} + +.status-row, +.inline-form { + display: flex; + gap: 10px; + align-items: center; + justify-content: space-between; +} + +.inline-form input { + flex: 1; +} + +.badge { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 10px; + border-radius: 999px; + background: rgba(148, 163, 184, 0.16); + color: #dbeafe; + font-size: 13px; + font-weight: 850; +} + +.badge.waiting_verification, +.badge.pending { + color: var(--warn); + background: rgba(251, 191, 36, 0.12); +} + +.badge.paid, +.badge.completed { + color: var(--ok); + background: rgba(52, 211, 153, 0.12); +} + +.badge.rejected, +.badge.expired { + color: var(--danger); + background: rgba(255, 90, 102, 0.12); +} + +.amount-box { + margin: 18px 0; + padding: 18px; + border: 1px solid rgba(255, 196, 0, 0.55); + background: rgba(255, 196, 0, 0.08); + border-radius: 8px; + display: grid; + gap: 5px; +} + +.amount-box strong { + color: var(--yellow); + font-size: 34px; +} + +.payment-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.payment-grid div, +.qr-panel { + border: 1px solid var(--line); + border-radius: 8px; + padding: 13px; + background: rgba(15, 23, 42, 0.62); + display: grid; + gap: 6px; +} + +.qr-panel { + margin-top: 12px; + grid-template-columns: minmax(0, 1fr) 148px; + gap: 14px; + align-items: center; +} + +.qr-panel img { + width: 148px; + aspect-ratio: 1; + object-fit: contain; + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; +} + +.instructions { + margin: 18px 0; + padding-left: 18px; + color: #e2e8f0; +} + +.instructions li + li { + margin-top: 6px; +} + +.steps { + display: grid; + gap: 10px; + margin-top: 16px; +} + +.steps span { + padding: 12px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(15, 23, 42, 0.62); +} + +.track-result, +.admin-orders { + margin-top: 16px; + display: grid; + gap: 12px; +} + +.order-card { + border: 1px solid var(--line); + border-radius: 8px; + padding: 14px; + background: rgba(15, 23, 42, 0.62); + display: grid; + gap: 10px; +} + +.order-card header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.order-meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + color: #dbeafe; + font-size: 14px; +} + +.order-meta a { + color: var(--yellow); +} + +.actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +button.danger { + color: #fff; + background: var(--danger); +} + +button.secondary { + color: #fff; + background: #475569; +} + +.site-footer { + min-height: 92px; + padding: 0 60px; + border-top: 1px solid var(--line); + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + color: #e5e7eb; +} + +.site-footer div { + display: flex; + gap: 32px; +} + +.socials { + display: inline-flex; + align-items: center; + gap: 10px; + white-space: nowrap; +} + +.socials button { + width: 34px; + min-height: 34px; + padding: 0; + border-radius: 50%; + color: #111827; + background: var(--yellow); +} + +.modal-backdrop { + position: fixed; + inset: 0; + z-index: 60; + display: grid; + place-items: center; + padding: 24px; + background: rgba(2, 6, 23, 0.72); + backdrop-filter: blur(8px); +} + +.modal { + position: relative; + width: min(560px, 100%); + max-height: min(720px, calc(100vh - 48px)); + overflow: auto; + border: 1px solid var(--line); + border-radius: 12px; + padding: 24px; + background: rgba(8, 18, 44, 0.98); + box-shadow: 0 32px 80px rgba(0, 0, 0, 0.46); +} + +.modal h2 { + padding-right: 42px; + margin-bottom: 14px; +} + +.modal p { + margin: 0; + color: #dbeafe; + line-height: 1.55; +} + +.modal-close { + position: absolute; + top: 14px; + right: 14px; + width: 34px; + min-height: 34px; + padding: 0; + color: #f8fafc; + background: rgba(148, 163, 184, 0.16); + box-shadow: none; + font-size: 24px; +} + +.price-table { + width: 100%; + margin: 10px 0 18px; + border-collapse: collapse; + overflow: hidden; + border: 1px solid var(--line); + border-radius: 8px; +} + +.price-table td { + padding: 10px 12px; + border-bottom: 1px solid var(--line); + color: #e2e8f0; +} + +.price-table tr:last-child td { + border-bottom: 0; +} + +.price-table td:last-child { + text-align: right; + color: var(--yellow); + font-weight: 850; +} + +@media (max-width: 1120px) { + .site-header { + height: auto; + padding: 16px 24px; + flex-wrap: wrap; + } + + .main-nav { + order: 3; + width: 100%; + justify-content: flex-start; + overflow-x: auto; + } + + .main-nav a { + min-height: 44px; + } + + .hero { + width: min(100% - 32px, 1428px); + } + + .trust-strip { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .trust-strip div:nth-child(3) { + border-left: 0; + border-top: 1px solid var(--line); + } + + .trust-strip div:nth-child(4) { + border-top: 1px solid var(--line); + } +} + +@media (max-width: 880px) { + .game-grid, + .shop-layout, + .pricing-grid { + grid-template-columns: 1fr; + } + + .payment-panel, + .tracking-panel, + .admin-panel, + .help-panel { + grid-column: auto; + } + + .game-card { + min-height: 0; + } + + .site-footer { + padding: 24px; + flex-direction: column; + } +} + +@media (max-width: 620px) { + .logo { + min-width: 0; + } + + .header-actions { + width: 100%; + gap: 10px; + } + + .currency-button, + .login-button { + flex: 1; + min-width: 0; + } + + .currency-menu { + left: 0; + right: auto; + top: 58px; + } + + .hero-copy p { + font-size: 17px; + } + + .game-card-content { + width: 78%; + padding: 24px; + } + + .game-card h2 { + margin-top: 38px; + font-size: 28px; + } + + .trust-strip, + .payment-grid, + .qr-panel, + .order-meta { + grid-template-columns: 1fr; + } + + .trust-strip div, + .trust-strip div + div { + border-left: 0; + border-top: 1px solid var(--line); + } + + .trust-strip div:first-child { + border-top: 0; + } + + .inline-form, + .status-row { + align-items: stretch; + flex-direction: column; + } + + .qr-panel img { + width: 100%; + max-width: 220px; + } +} diff --git a/SmartCoin/frontend/track.html b/SmartCoin/frontend/track.html new file mode 100644 index 0000000..57a92d9 --- /dev/null +++ b/SmartCoin/frontend/track.html @@ -0,0 +1,35 @@ + + + + + + Track Order - Game Topup + + + + +
+

TRACK ORDER

Enter your order ID to see payment and delivery status.

+
+

Track Order

+
+ + +
+
+
+
+ + + + diff --git a/SmartCoin/live-checking/.env.example b/SmartCoin/live-checking/.env.example new file mode 100644 index 0000000..54d9705 --- /dev/null +++ b/SmartCoin/live-checking/.env.example @@ -0,0 +1,16 @@ +LIVE_CHECK_DRY_RUN=true +LIVE_CHECK_INTERVAL_SECONDS=60 + +SMARTCOIN_HEALTH_URL=http://127.0.0.1:8000/api/config +SMARTCOIN_HEALTH_TIMEOUT_MS=5000 + +SMILE_BALANCE_SOURCE=env +SMILE_BALANCE=100000 +SMILE_BALANCE_CURRENCY=MMK +SMILE_WARNING_BALANCE=50000 +SMILE_CRITICAL_BALANCE=10000 + +QUEUE_STATUS_FILE=./state/queue-status.json +BROWSER_HEARTBEAT_FILE=./state/browser-heartbeat.json +SESSION_STATUS_FILE=./state/session-status.json +BROWSER_HEARTBEAT_MAX_AGE_SECONDS=120 diff --git a/SmartCoin/live-checking/README.md b/SmartCoin/live-checking/README.md new file mode 100644 index 0000000..2444946 --- /dev/null +++ b/SmartCoin/live-checking/README.md @@ -0,0 +1,54 @@ +# Live Checking System + +Monitors the SmartCoin automation environment and triggers notification emails when important checks fail. + +## Checks + +- SmartCoin app health URL +- Smile.one balance threshold +- queue status file +- browser heartbeat file +- Smile.one session status file + +## Commands + +```bash +npm run check +npm start +``` + +`npm run check` runs one pass. `npm start` runs continuously. + +## Dry Run + +`LIVE_CHECK_DRY_RUN=true` logs alerts without sending email. Set it to `false` after `notification/.env` has real Gmail credentials. + +## State File Examples + +`state/queue-status.json` + +```json +{ + "ok": true, + "pending": 0, + "failed": 0 +} +``` + +`state/browser-heartbeat.json` + +```json +{ + "ok": true, + "timestamp": "2026-05-06T10:00:00.000Z" +} +``` + +`state/session-status.json` + +```json +{ + "valid": true, + "account": "Smile.one main account" +} +``` diff --git a/SmartCoin/live-checking/checks/balanceCheck.js b/SmartCoin/live-checking/checks/balanceCheck.js new file mode 100644 index 0000000..f97154e --- /dev/null +++ b/SmartCoin/live-checking/checks/balanceCheck.js @@ -0,0 +1,50 @@ +async function checkSmileBalance(config) { + const balance = config.smileBalance; + const currency = config.smileBalanceCurrency; + + if (balance <= config.smileCriticalBalance) { + return { + ok: false, + type: "lowBalance", + code: "smile_balance_critical", + message: `Smile.one balance is critical: ${balance.toLocaleString()} ${currency}.`, + details: { + balance, + currency, + level: "critical", + threshold: config.smileCriticalBalance, + source: config.smileBalanceSource, + }, + }; + } + + if (balance <= config.smileWarningBalance) { + return { + ok: false, + type: "lowBalance", + code: "smile_balance_warning", + message: `Smile.one balance is low: ${balance.toLocaleString()} ${currency}.`, + details: { + balance, + currency, + level: "warning", + threshold: config.smileWarningBalance, + source: config.smileBalanceSource, + }, + }; + } + + return { + ok: true, + code: "smile_balance_ok", + details: { + balance, + currency, + source: config.smileBalanceSource, + }, + }; +} + +module.exports = { + checkSmileBalance, +}; diff --git a/SmartCoin/live-checking/checks/httpHealthCheck.js b/SmartCoin/live-checking/checks/httpHealthCheck.js new file mode 100644 index 0000000..bade8a5 --- /dev/null +++ b/SmartCoin/live-checking/checks/httpHealthCheck.js @@ -0,0 +1,62 @@ +const http = require("http"); +const https = require("https"); + +function request(url, timeoutMs) { + return new Promise((resolve, reject) => { + const client = url.startsWith("https:") ? https : http; + const req = client.get(url, { timeout: timeoutMs }, (res) => { + res.resume(); + resolve({ + statusCode: res.statusCode, + ok: res.statusCode >= 200 && res.statusCode < 300, + }); + }); + + req.on("timeout", () => { + req.destroy(new Error(`Health check timed out after ${timeoutMs}ms`)); + }); + req.on("error", reject); + }); +} + +async function checkHttpHealth(config) { + try { + const result = await request(config.healthUrl, config.healthTimeoutMs); + if (!result.ok) { + return { + ok: false, + type: "critical", + code: "smartcoin_health_bad_status", + message: `SmartCoin health URL returned HTTP ${result.statusCode}.`, + details: { + url: config.healthUrl, + statusCode: result.statusCode, + }, + }; + } + + return { + ok: true, + code: "smartcoin_health_ok", + details: { + url: config.healthUrl, + statusCode: result.statusCode, + }, + }; + } catch (error) { + return { + ok: false, + type: "critical", + code: "smartcoin_health_failed", + message: `SmartCoin health check failed: ${error.message}`, + details: { + url: config.healthUrl, + error: error.message, + }, + }; + } +} + +module.exports = { + checkHttpHealth, +}; diff --git a/SmartCoin/live-checking/checks/stateFileCheck.js b/SmartCoin/live-checking/checks/stateFileCheck.js new file mode 100644 index 0000000..6533535 --- /dev/null +++ b/SmartCoin/live-checking/checks/stateFileCheck.js @@ -0,0 +1,131 @@ +const fs = require("fs"); + +function readJson(filePath) { + if (!fs.existsSync(filePath)) { + return null; + } + return JSON.parse(fs.readFileSync(filePath, "utf8")); +} + +async function checkQueueStatus(config) { + const status = readJson(config.queueStatusFile); + if (!status) { + return { + ok: true, + code: "queue_status_missing", + details: { + file: config.queueStatusFile, + note: "Queue status file does not exist yet.", + }, + }; + } + + if (status.ok === false || Number(status.failed || 0) > 0) { + return { + ok: false, + type: "critical", + code: "queue_error", + message: status.message || "Queue system reported an error.", + details: { + file: config.queueStatusFile, + ...status, + }, + }; + } + + return { + ok: true, + code: "queue_ok", + details: { + file: config.queueStatusFile, + ...status, + }, + }; +} + +async function checkBrowserHeartbeat(config) { + const heartbeat = readJson(config.browserHeartbeatFile); + if (!heartbeat) { + return { + ok: true, + code: "browser_heartbeat_missing", + details: { + file: config.browserHeartbeatFile, + note: "Browser heartbeat file does not exist yet.", + }, + }; + } + + const timestamp = Date.parse(heartbeat.timestamp || ""); + const ageSeconds = Number.isFinite(timestamp) ? Math.round((Date.now() - timestamp) / 1000) : Infinity; + const stale = ageSeconds > config.browserHeartbeatMaxAgeSeconds; + + if (heartbeat.ok === false || stale) { + return { + ok: false, + type: "critical", + code: stale ? "browser_heartbeat_stale" : "browser_crashed", + message: stale + ? `Browser heartbeat is stale: ${ageSeconds}s old.` + : heartbeat.message || "Browser reported a crash.", + details: { + file: config.browserHeartbeatFile, + ageSeconds, + maxAgeSeconds: config.browserHeartbeatMaxAgeSeconds, + ...heartbeat, + }, + }; + } + + return { + ok: true, + code: "browser_heartbeat_ok", + details: { + file: config.browserHeartbeatFile, + ageSeconds, + ...heartbeat, + }, + }; +} + +async function checkSessionStatus(config) { + const session = readJson(config.sessionStatusFile); + if (!session) { + return { + ok: true, + code: "session_status_missing", + details: { + file: config.sessionStatusFile, + note: "Session status file does not exist yet.", + }, + }; + } + + if (session.valid === false || session.expired === true) { + return { + ok: false, + type: "sessionExpired", + code: "session_expired", + message: session.message || "Smile.one account session expired.", + details: { + file: config.sessionStatusFile, + ...session, + }, + }; + } + + return { + ok: true, + code: "session_ok", + details: { + file: config.sessionStatusFile, + ...session, + }, + }; +} + +module.exports = { + checkQueueStatus, + checkBrowserHeartbeat, + checkSessionStatus, +}; diff --git a/SmartCoin/live-checking/config.js b/SmartCoin/live-checking/config.js new file mode 100644 index 0000000..bc144bb --- /dev/null +++ b/SmartCoin/live-checking/config.js @@ -0,0 +1,42 @@ +const path = require("path"); + +function boolFromEnv(name, fallback) { + const raw = process.env[name]; + if (raw === undefined) return fallback; + return ["1", "true", "yes", "on"].includes(raw.toLowerCase()); +} + +function numberFromEnv(name, fallback) { + const raw = process.env[name]; + if (!raw) return fallback; + const value = Number(raw); + return Number.isFinite(value) ? value : fallback; +} + +function pathFromEnv(name, fallback) { + const raw = process.env[name] || fallback; + if (path.isAbsolute(raw)) return raw; + return path.join(__dirname, raw); +} + +function getConfig() { + return { + dryRun: boolFromEnv("LIVE_CHECK_DRY_RUN", true), + intervalSeconds: numberFromEnv("LIVE_CHECK_INTERVAL_SECONDS", 60), + healthUrl: process.env.SMARTCOIN_HEALTH_URL || "http://127.0.0.1:8000/api/config", + healthTimeoutMs: numberFromEnv("SMARTCOIN_HEALTH_TIMEOUT_MS", 5000), + smileBalanceSource: process.env.SMILE_BALANCE_SOURCE || "env", + smileBalance: numberFromEnv("SMILE_BALANCE", 100000), + smileBalanceCurrency: process.env.SMILE_BALANCE_CURRENCY || "MMK", + smileWarningBalance: numberFromEnv("SMILE_WARNING_BALANCE", 50000), + smileCriticalBalance: numberFromEnv("SMILE_CRITICAL_BALANCE", 10000), + queueStatusFile: pathFromEnv("QUEUE_STATUS_FILE", "./state/queue-status.json"), + browserHeartbeatFile: pathFromEnv("BROWSER_HEARTBEAT_FILE", "./state/browser-heartbeat.json"), + sessionStatusFile: pathFromEnv("SESSION_STATUS_FILE", "./state/session-status.json"), + browserHeartbeatMaxAgeSeconds: numberFromEnv("BROWSER_HEARTBEAT_MAX_AGE_SECONDS", 120), + }; +} + +module.exports = { + getConfig, +}; diff --git a/SmartCoin/live-checking/package.json b/SmartCoin/live-checking/package.json new file mode 100644 index 0000000..8799ebf --- /dev/null +++ b/SmartCoin/live-checking/package.json @@ -0,0 +1,11 @@ +{ + "name": "smartcoin-live-checking", + "version": "1.0.0", + "private": true, + "description": "Live checking and monitoring system for SmartCoin automation health.", + "main": "server.js", + "scripts": { + "start": "node server.js monitor", + "check": "node server.js once" + } +} diff --git a/SmartCoin/live-checking/server.js b/SmartCoin/live-checking/server.js new file mode 100644 index 0000000..89adcd6 --- /dev/null +++ b/SmartCoin/live-checking/server.js @@ -0,0 +1,47 @@ +const { getConfig } = require("./config"); +const { runChecks } = require("./services/liveChecker"); +const { loadEnv } = require("./utils/env"); +const logger = require("./utils/logger"); + +loadEnv(); + +async function runOnce() { + const config = getConfig(); + const summary = await runChecks(config); + console.log(JSON.stringify(summary, null, 2)); + return summary; +} + +async function monitor() { + const config = getConfig(); + logger.info("Live checking monitor started", { + intervalSeconds: config.intervalSeconds, + dryRun: config.dryRun, + }); + + while (true) { + try { + await runChecks(getConfig()); + } catch (error) { + logger.error("Live checking cycle failed", { + error: error.message, + stack: error.stack, + }); + } + + await new Promise((resolve) => setTimeout(resolve, config.intervalSeconds * 1000)); + } +} + +if (require.main === module) { + const command = process.argv[2] || "once"; + const runner = command === "monitor" ? monitor : runOnce; + + runner().catch((error) => { + logger.error("Live checking failed", { + error: error.message, + stack: error.stack, + }); + process.exit(1); + }); +} diff --git a/SmartCoin/live-checking/services/liveChecker.js b/SmartCoin/live-checking/services/liveChecker.js new file mode 100644 index 0000000..4a6a1c5 --- /dev/null +++ b/SmartCoin/live-checking/services/liveChecker.js @@ -0,0 +1,80 @@ +const { + sendAutomationFailureAlert, + sendCriticalErrorAlert, + sendLowBalanceAlert, + sendSessionExpiredAlert, +} = require("../../notification/services/emailService"); +const { checkSmileBalance } = require("../checks/balanceCheck"); +const { checkHttpHealth } = require("../checks/httpHealthCheck"); +const { + checkBrowserHeartbeat, + checkQueueStatus, + checkSessionStatus, +} = require("../checks/stateFileCheck"); +const logger = require("../utils/logger"); + +async function notify(result, config) { + if (result.ok) return { sent: false, skipped: true, reason: "ok" }; + + if (config.dryRun) { + logger.warn("Dry-run alert", { + alertType: result.type, + code: result.code, + message: result.message, + details: result.details, + }); + return { sent: false, skipped: true, reason: "dry_run" }; + } + + if (result.type === "lowBalance") { + return sendLowBalanceAlert(result.details.balance, result.details.currency); + } + + if (result.type === "sessionExpired") { + return sendSessionExpiredAlert(result.details.account, result.details); + } + + if (result.type === "automationFailure") { + return sendAutomationFailureAlert(result.message, result.details); + } + + return sendCriticalErrorAlert(result.message, { + component: result.code, + ...result.details, + }); +} + +async function runChecks(config) { + const checks = [ + checkHttpHealth, + checkSmileBalance, + checkQueueStatus, + checkBrowserHeartbeat, + checkSessionStatus, + ]; + + const results = []; + for (const check of checks) { + const result = await check(config); + results.push(result); + logger.info("Live check completed", { + code: result.code, + ok: result.ok, + details: result.details, + }); + + if (!result.ok) { + await notify(result, config); + } + } + + return { + ok: results.every((result) => result.ok), + checkedAt: new Date().toISOString(), + results, + }; +} + +module.exports = { + runChecks, +}; diff --git a/SmartCoin/live-checking/utils/env.js b/SmartCoin/live-checking/utils/env.js new file mode 100644 index 0000000..89a667c --- /dev/null +++ b/SmartCoin/live-checking/utils/env.js @@ -0,0 +1,38 @@ +const fs = require("fs"); +const path = require("path"); + +function loadEnvFile(filePath) { + if (!fs.existsSync(filePath)) return; + + const content = fs.readFileSync(filePath, "utf8"); + for (const line of content.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + + const index = trimmed.indexOf("="); + if (index === -1) continue; + + const key = trimmed.slice(0, index).trim(); + let value = trimmed.slice(index + 1).trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + + if (!process.env[key]) { + process.env[key] = value; + } + } +} + +function loadEnv() { + const root = path.join(__dirname, "..", ".."); + loadEnvFile(path.join(root, "notification", ".env")); + loadEnvFile(path.join(root, "live-checking", ".env")); +} + +module.exports = { + loadEnv, +}; diff --git a/SmartCoin/live-checking/utils/logger.js b/SmartCoin/live-checking/utils/logger.js new file mode 100644 index 0000000..a2fa0a8 --- /dev/null +++ b/SmartCoin/live-checking/utils/logger.js @@ -0,0 +1,36 @@ +const fs = require("fs"); +const path = require("path"); + +const logDir = path.join(__dirname, "..", "logs"); +const logFile = path.join(logDir, "live-checking.log"); + +function ensureLogDir() { + fs.mkdirSync(logDir, { recursive: true }); +} + +function write(level, message, meta = {}) { + ensureLogDir(); + const entry = { + timestamp: new Date().toISOString(), + level, + message, + ...meta, + }; + const line = JSON.stringify(entry); + fs.appendFileSync(logFile, `${line}\n`); + + const printer = level === "error" ? console.error : console.log; + printer(line); +} + +module.exports = { + info(message, meta) { + write("info", message, meta); + }, + warn(message, meta) { + write("warn", message, meta); + }, + error(message, meta) { + write("error", message, meta); + }, +}; diff --git a/SmartCoin/logs/.gitkeep b/SmartCoin/logs/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/SmartCoin/logs/.gitkeep @@ -0,0 +1 @@ + diff --git a/SmartCoin/logs/automation-worker.log b/SmartCoin/logs/automation-worker.log new file mode 100644 index 0000000..e025688 --- /dev/null +++ b/SmartCoin/logs/automation-worker.log @@ -0,0 +1,14 @@ +{"timestamp":"2026-05-06T10:37:49.688Z","level":"error","message":"Automation worker failed","error":"fetch failed","stack":"TypeError: fetch failed"} +{"timestamp":"2026-05-06T10:38:40.816Z","level":"info","message":"No waiting automation jobs","failed":0} +{"timestamp":"2026-05-06T10:41:31.008Z","level":"error","message":"Automation worker failed","error":"fetch failed","stack":"TypeError: fetch failed"} +{"timestamp":"2026-05-10T14:03:57.475Z","level":"info","message":"Puppeteer browser launched","accountEmail":"smoke-check","headless":false,"userDataDir":"/Users/ven/Projects/SmartCoin/backend/sessions/smoke-check"} +{"timestamp":"2026-05-10T14:03:58.536Z","level":"info","message":"Puppeteer smoke check loaded Smile.one","url":"https://www.smile.one/","title":"Smile.One - (Brazil) | Créditos de Jogos e Serviços"} +{"timestamp":"2026-05-11T07:27:36.361Z","level":"error","message":"Automation worker failed","error":"Cannot reach SmartCoin API at http://127.0.0.1:8000: connect EPERM 127.0.0.1:8000 - Local (0.0.0.0:0)","stack":"Error: Cannot reach SmartCoin API at http://127.0.0.1:8000: connect EPERM 127.0.0.1:8000 - Local (0.0.0.0:0)\n at request (/Users/ven/SmartCoin/SmartCoin/backend/services/apiClient.js:23:11)\n at process.processTicksAndRejections (node:internal/process/task_queues:104:5)\n at async Promise.all (index 0)\n at async processNextOrder (/Users/ven/SmartCoin/SmartCoin/backend/workers/queueWorker.js:60:37)"} +{"timestamp":"2026-05-11T07:28:07.493Z","level":"info","message":"No waiting automation jobs","failed":0} +{"timestamp":"2026-05-11T07:28:15.176Z","level":"error","message":"Puppeteer smoke check failed","error":"Failed to launch the browser process: Code: null\n\nstderr:\nchrome_crashpad_handler: --database is required\nTry 'chrome_crashpad_handler --help' for more information.\n[0511/142815.143790:ERROR:third_party/crashpad/crashpad/util/file/file_io.cc:103] ReadExactly: expected 8, observed 0\n[0511/142815.148765:ERROR:third_party/crashpad/crashpad/client/crash_report_database_mac.mm:109] mkdir : No such file or directory (2)\n\nTROUBLESHOOTING: https://pptr.dev/troubleshooting\n","stack":"Error: Failed to launch the browser process: Code: null\n\nstderr:\nchrome_crashpad_handler: --database is required\nTry 'chrome_crashpad_handler --help' for more information.\n[0511/142815.143790:ERROR:third_party/crashpad/crashpad/util/file/file_io.cc:103] ReadExactly: expected 8, observed 0\n[0511/142815.148765:ERROR:third_party/crashpad/crashpad/client/crash_report_database_mac.mm:109] mkdir : No such file or directory (2)\n\nTROUBLESHOOTING: https://pptr.dev/troubleshooting\n\n at ChildProcess.onClose (/Users/ven/SmartCoin/SmartCoin/backend/node_modules/@puppeteer/browsers/lib/cjs/launch.js:350:24)\n at ChildProcess.emit (node:events:521:24)\n at ChildProcess._handle.onexit (node:internal/child_process:294:12)"} +{"timestamp":"2026-05-11T07:28:29.611Z","level":"error","message":"Puppeteer smoke check failed","error":"Failed to launch the browser process: Code: null\n\nstderr:\n\n\nTROUBLESHOOTING: https://pptr.dev/troubleshooting\n","stack":"Error: Failed to launch the browser process: Code: null\n\nstderr:\n\n\nTROUBLESHOOTING: https://pptr.dev/troubleshooting\n\n at ChildProcess.onClose (/Users/ven/SmartCoin/SmartCoin/backend/node_modules/@puppeteer/browsers/lib/cjs/launch.js:350:24)\n at ChildProcess.emit (node:events:521:24)\n at ChildProcess._handle.onexit (node:internal/child_process:294:12)"} +{"timestamp":"2026-05-11T07:28:42.816Z","level":"info","message":"Puppeteer browser launched","accountEmail":"smoke-check","headless":true,"userDataDir":"/Users/ven/SmartCoin/SmartCoin/backend/sessions/smoke-check"} +{"timestamp":"2026-05-11T07:28:44.547Z","level":"info","message":"Puppeteer smoke check loaded Smile.one","url":"https://www.smile.one/","title":"Smile.One - (Brazil) | Créditos de Jogos e Serviços"} +{"timestamp":"2026-05-11T07:29:22.661Z","level":"info","message":"Puppeteer browser launched","accountEmail":"smoke-check","headless":false,"userDataDir":"/Users/ven/SmartCoin/SmartCoin/backend/sessions/smoke-check"} +{"timestamp":"2026-05-11T07:29:24.429Z","level":"info","message":"Puppeteer smoke check loaded Smile.one","url":"https://www.smile.one/","title":"Smile.One - (Brazil) | Créditos de Jogos e Serviços"} +{"timestamp":"2026-05-11T07:55:54.136Z","level":"info","message":"No waiting automation jobs","failed":0} diff --git a/SmartCoin/notification/.env.example b/SmartCoin/notification/.env.example new file mode 100644 index 0000000..ff014dc --- /dev/null +++ b/SmartCoin/notification/.env.example @@ -0,0 +1,7 @@ +EMAIL_USER=yourgmail@gmail.com +EMAIL_PASS=your_app_password +ADMIN_EMAIL=yourgmail@gmail.com + +EMAIL_WARNING_BALANCE=50000 +EMAIL_CRITICAL_BALANCE=10000 +EMAIL_COOLDOWN_MINUTES=30 diff --git a/SmartCoin/notification/package-lock.json b/SmartCoin/notification/package-lock.json new file mode 100644 index 0000000..eb22ea1 --- /dev/null +++ b/SmartCoin/notification/package-lock.json @@ -0,0 +1,37 @@ +{ + "name": "smartcoin-email-notifications", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "smartcoin-email-notifications", + "version": "1.0.0", + "dependencies": { + "dotenv": "^16.4.7", + "nodemailer": "^8.0.7" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/nodemailer": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz", + "integrity": "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + } + } +} diff --git a/SmartCoin/notification/package.json b/SmartCoin/notification/package.json new file mode 100644 index 0000000..acace86 --- /dev/null +++ b/SmartCoin/notification/package.json @@ -0,0 +1,15 @@ +{ + "name": "smartcoin-email-notifications", + "version": "1.0.0", + "private": true, + "description": "Email notification module for SmartCoin game top-up automation alerts.", + "main": "server.js", + "scripts": { + "start": "node server.js", + "test:email": "node server.js test" + }, + "dependencies": { + "dotenv": "^16.4.7", + "nodemailer": "^8.0.7" + } +} diff --git a/SmartCoin/notification/server.js b/SmartCoin/notification/server.js new file mode 100644 index 0000000..7a36f6d --- /dev/null +++ b/SmartCoin/notification/server.js @@ -0,0 +1,49 @@ +require("dotenv").config(); + +const { + sendLowBalanceAlert, + sendAutomationFailureAlert, + sendCriticalErrorAlert, + sendSessionExpiredAlert, +} = require("./services/emailService"); + +async function runTest() { + const kind = process.argv[3] || "low-balance"; + + if (kind === "low-balance") { + return sendLowBalanceAlert(45000, "MMK"); + } + if (kind === "automation-failure") { + return sendAutomationFailureAlert("Purchase timeout", { + orderId: "ORD-TEST", + product: "86 Diamonds", + }); + } + if (kind === "critical-error") { + return sendCriticalErrorAlert(new Error("Browser crashed"), { + component: "browser", + }); + } + if (kind === "session-expired") { + return sendSessionExpiredAlert("Smile.one main account"); + } + + throw new Error(`Unknown test type: ${kind}`); +} + +if (require.main === module) { + if (process.argv[2] !== "test") { + console.log("Email notification module is ready."); + console.log("Run `npm run test:email` after creating notification/.env."); + process.exit(0); + } + + runTest() + .then((result) => { + console.log(JSON.stringify(result, null, 2)); + }) + .catch((error) => { + console.error(error.message); + process.exit(1); + }); +} diff --git a/SmartCoin/notification/services/emailService.js b/SmartCoin/notification/services/emailService.js new file mode 100644 index 0000000..ade8a34 --- /dev/null +++ b/SmartCoin/notification/services/emailService.js @@ -0,0 +1,210 @@ +const nodemailer = require("nodemailer"); +const logger = require("../utils/logger"); + +const DEFAULT_WARNING_BALANCE = 50000; +const DEFAULT_CRITICAL_BALANCE = 10000; +const DEFAULT_COOLDOWN_MINUTES = 30; + +const SUBJECTS = { + lowBalance: "Smile Balance Low", + automationFailure: "Automation Failed", + criticalError: "Critical System Error", + sessionExpired: "Smile Session Expired", +}; + +const lastSentAt = new Map(); + +function numberFromEnv(name, fallback) { + const raw = process.env[name]; + if (!raw) return fallback; + const value = Number(raw); + return Number.isFinite(value) ? value : fallback; +} + +function getConfig() { + return { + emailUser: process.env.EMAIL_USER, + emailPass: process.env.EMAIL_PASS, + adminEmail: process.env.ADMIN_EMAIL, + warningBalance: numberFromEnv("EMAIL_WARNING_BALANCE", DEFAULT_WARNING_BALANCE), + criticalBalance: numberFromEnv("EMAIL_CRITICAL_BALANCE", DEFAULT_CRITICAL_BALANCE), + cooldownMinutes: numberFromEnv("EMAIL_COOLDOWN_MINUTES", DEFAULT_COOLDOWN_MINUTES), + }; +} + +function validateConfig(config) { + const missing = []; + if (!config.emailUser) missing.push("EMAIL_USER"); + if (!config.emailPass) missing.push("EMAIL_PASS"); + if (!config.adminEmail) missing.push("ADMIN_EMAIL"); + + if (missing.length > 0) { + throw new Error(`Missing email environment variables: ${missing.join(", ")}`); + } +} + +function createTransporter(config = getConfig()) { + validateConfig(config); + return nodemailer.createTransport({ + service: "gmail", + port: 587, + secure: false, + auth: { + user: config.emailUser, + pass: config.emailPass, + }, + connectionTimeout: 10000, + greetingTimeout: 10000, + socketTimeout: 15000, + }); +} + +function cooldownKey(type, key = "default") { + return `${type}:${key}`; +} + +function canSend(type, key, cooldownMinutes) { + const now = Date.now(); + const previous = lastSentAt.get(cooldownKey(type, key)); + if (!previous) return true; + return now - previous >= cooldownMinutes * 60 * 1000; +} + +function markSent(type, key) { + lastSentAt.set(cooldownKey(type, key), Date.now()); +} + +function formatDetails(details = {}) { + return Object.entries(details) + .filter(([, value]) => value !== undefined && value !== null && value !== "") + .map(([key, value]) => `${escapeHtml(key)}${escapeHtml(String(value))}`) + .join(""); +} + +function escapeHtml(value) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function buildEmailHtml(title, message, details) { + const rows = formatDetails(details); + return ` +
+

${escapeHtml(title)}

+

${escapeHtml(message)}

+ ${ + rows + ? ` + ${rows} +
` + : "" + } +

+ Sent by SmartCoin notification system at ${new Date().toISOString()}. +

+
+ `; +} + +async function sendNotification(type, { subject, message, details = {}, cooldownKey: key = "default" }) { + const config = getConfig(); + const cooldownMinutes = config.cooldownMinutes; + + if (!canSend(type, key, cooldownMinutes)) { + logger.info("Email alert skipped due to cooldown", { alertType: type, cooldownKey: key }); + return { sent: false, skipped: true, reason: "cooldown" }; + } + + const transporter = createTransporter(config); + const title = subject || SUBJECTS[type] || "SmartCoin Alert"; + + try { + const result = await transporter.sendMail({ + from: `"SmartCoin Alerts" <${config.emailUser}>`, + to: config.adminEmail, + subject: title, + text: `${message}\n\n${JSON.stringify(details, null, 2)}`, + html: buildEmailHtml(title, message, details), + }); + + markSent(type, key); + logger.info("Email alert sent", { + alertType: type, + cooldownKey: key, + messageId: result.messageId, + accepted: result.accepted, + rejected: result.rejected, + }); + return { sent: true, messageId: result.messageId }; + } catch (error) { + logger.error("Email alert failed", { + alertType: type, + cooldownKey: key, + error: error.message, + }); + throw error; + } +} + +async function sendLowBalanceAlert(balance, currency = "MMK") { + const config = getConfig(); + const threshold = balance <= config.criticalBalance ? config.criticalBalance : config.warningBalance; + const level = balance <= config.criticalBalance ? "critical" : "warning"; + + if (balance > config.warningBalance) { + return { sent: false, skipped: true, reason: "balance_above_threshold" }; + } + + return sendNotification("lowBalance", { + message: `Smile.one balance is ${level}: ${balance.toLocaleString()} ${currency}.`, + details: { + balance, + currency, + level, + threshold, + }, + cooldownKey: level, + }); +} + +async function sendAutomationFailureAlert(reason, details = {}) { + return sendNotification("automationFailure", { + message: reason || "Purchase automation failed.", + details, + cooldownKey: details.orderId || details.jobId || reason || "automation", + }); +} + +async function sendCriticalErrorAlert(error, details = {}) { + const message = error instanceof Error ? error.message : String(error || "Critical system error"); + return sendNotification("criticalError", { + message, + details: { + ...details, + stack: error instanceof Error ? error.stack : undefined, + }, + cooldownKey: details.component || message, + }); +} + +async function sendSessionExpiredAlert(account, details = {}) { + return sendNotification("sessionExpired", { + message: `Smile.one session expired${account ? ` for ${account}` : ""}.`, + details: { + account, + ...details, + }, + cooldownKey: account || "smile-session", + }); +} + +module.exports = { + sendNotification, + sendLowBalanceAlert, + sendAutomationFailureAlert, + sendCriticalErrorAlert, + sendSessionExpiredAlert, +}; diff --git a/SmartCoin/notification/utils/logger.js b/SmartCoin/notification/utils/logger.js new file mode 100644 index 0000000..6f091e7 --- /dev/null +++ b/SmartCoin/notification/utils/logger.js @@ -0,0 +1,33 @@ +const fs = require("fs"); +const path = require("path"); + +const logDir = path.join(__dirname, "..", "logs"); +const logFile = path.join(logDir, "email-notifications.log"); + +function ensureLogDir() { + fs.mkdirSync(logDir, { recursive: true }); +} + +function write(level, message, meta = {}) { + ensureLogDir(); + const entry = { + timestamp: new Date().toISOString(), + level, + message, + ...meta, + }; + const line = JSON.stringify(entry); + fs.appendFileSync(logFile, `${line}\n`); + + const printer = level === "error" ? console.error : console.log; + printer(line); +} + +module.exports = { + info(message, meta) { + write("info", message, meta); + }, + error(message, meta) { + write("error", message, meta); + }, +}; diff --git a/SmartCoin/restart_server.sh b/SmartCoin/restart_server.sh new file mode 100755 index 0000000..8fbd899 --- /dev/null +++ b/SmartCoin/restart_server.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) + +"$SCRIPT_DIR/stop_server.sh" +"$SCRIPT_DIR/start_server.sh" diff --git a/SmartCoin/server.log b/SmartCoin/server.log new file mode 100644 index 0000000..9b562a9 --- /dev/null +++ b/SmartCoin/server.log @@ -0,0 +1,324 @@ +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 58808) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 58809) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 58810) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 58811) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 58812) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 58813) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 58814) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 58815) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 58816) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 58817) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 58818) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 58819) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 59371) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 59374) +Traceback (most recent call last): + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread + self.finish_request(request, client_address) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__ + self.handle() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request + method() + File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET + File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders + File "/Users/ven/Projects/game item resell/server.py", line 62, in db +sqlite3.OperationalError: unable to open database file +---------------------------------------- +Traceback (most recent call last): + File "/Users/ven/Projects/SmartCoin/server.py", line 542, in + server = ThreadingHTTPServer(("127.0.0.1", port), Handler) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 452, in __init__ + self.server_bind() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 138, in server_bind + socketserver.TCPServer.server_bind(self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 466, in server_bind + self.socket.bind(self.server_address) +OSError: [Errno 48] Address already in use +Traceback (most recent call last): + File "/Users/ven/Projects/SmartCoin/server.py", line 542, in + server = ThreadingHTTPServer(("127.0.0.1", port), Handler) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 452, in __init__ + self.server_bind() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 138, in server_bind + socketserver.TCPServer.server_bind(self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 466, in server_bind + self.socket.bind(self.server_address) +PermissionError: [Errno 1] Operation not permitted +Traceback (most recent call last): + File "/Users/ven/Projects/SmartCoin/server.py", line 685, in + server = ThreadingHTTPServer(("127.0.0.1", port), Handler) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 452, in __init__ + self.server_bind() + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 138, in server_bind + socketserver.TCPServer.server_bind(self) + File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 466, in server_bind + self.socket.bind(self.server_address) +PermissionError: [Errno 1] Operation not permitted +Server running at http://127.0.0.1:8000 +127.0.0.1 - "GET /api/config HTTP/1.1" 200 - +Server running at http://127.0.0.1:8000 +127.0.0.1 - "GET /api/config HTTP/1.1" 200 - +127.0.0.1 - "GET / HTTP/1.1" 200 - +127.0.0.1 - "GET /styles.css?v=20260428-13 HTTP/1.1" 200 - +127.0.0.1 - "GET /app.js?v=20260428-13 HTTP/1.1" 200 - +127.0.0.1 - "GET /assets/pubg-card.png HTTP/1.1" 200 - +127.0.0.1 - "GET /assets/ml-card.png HTTP/1.1" 200 - +127.0.0.1 - "GET /api/config HTTP/1.1" 200 - +127.0.0.1 - "GET /favicon.ico HTTP/1.1" 404 - +127.0.0.1 - "GET / HTTP/1.1" 200 - +127.0.0.1 - "GET /styles.css?v=20260428-13 HTTP/1.1" 200 - +127.0.0.1 - "GET /app.js?v=20260428-13 HTTP/1.1" 200 - +127.0.0.1 - "GET /assets/pubg-card.png HTTP/1.1" 200 - +127.0.0.1 - "GET /assets/ml-card.png HTTP/1.1" 200 - +127.0.0.1 - "GET /api/config HTTP/1.1" 200 - +127.0.0.1 - "GET /orders.html?game=PUBG%20Mobile HTTP/1.1" 200 - +127.0.0.1 - "GET /styles.css?v=20260428-11 HTTP/1.1" 200 - +127.0.0.1 - "GET /app.js?v=20260428-11 HTTP/1.1" 200 - +127.0.0.1 - "GET /aya-pay-qr.png HTTP/1.1" 404 - +127.0.0.1 - "GET /api/config HTTP/1.1" 200 - +Server running at http://127.0.0.1:8000 +127.0.0.1 - "GET /api/config HTTP/1.1" 200 - +127.0.0.1 - "GET / HTTP/1.1" 200 - +127.0.0.1 - "GET /app.js?v=20260428-13 HTTP/1.1" 200 - +127.0.0.1 - "GET /styles.css?v=20260428-13 HTTP/1.1" 200 - +127.0.0.1 - "GET /assets/pubg-card.png HTTP/1.1" 200 - +127.0.0.1 - "GET /assets/ml-card.png HTTP/1.1" 200 - +127.0.0.1 - "GET /api/config HTTP/1.1" 200 - +127.0.0.1 - "GET / HTTP/1.1" 200 - +127.0.0.1 - "GET /styles.css?v=20260428-13 HTTP/1.1" 200 - +127.0.0.1 - "GET /assets/pubg-card.png HTTP/1.1" 200 - +127.0.0.1 - "GET /app.js?v=20260428-13 HTTP/1.1" 200 - +127.0.0.1 - "GET /assets/ml-card.png HTTP/1.1" 200 - +127.0.0.1 - "GET /api/config HTTP/1.1" 200 - +127.0.0.1 - "GET /orders.html?game=PUBG%20Mobile HTTP/1.1" 200 - +127.0.0.1 - "GET /styles.css?v=20260428-11 HTTP/1.1" 200 - +127.0.0.1 - "GET /app.js?v=20260428-11 HTTP/1.1" 200 - +127.0.0.1 - "GET /api/config HTTP/1.1" 200 - +127.0.0.1 - "GET /aya-pay-qr.png HTTP/1.1" 404 - +127.0.0.1 - "POST /api/orders HTTP/1.1" 200 - diff --git a/SmartCoin/server.pid b/SmartCoin/server.pid new file mode 100644 index 0000000..059490b --- /dev/null +++ b/SmartCoin/server.pid @@ -0,0 +1 @@ +70547 diff --git a/SmartCoin/server.py b/SmartCoin/server.py new file mode 100644 index 0000000..1739d89 --- /dev/null +++ b/SmartCoin/server.py @@ -0,0 +1,687 @@ +#!/usr/bin/env python3 +import cgi +import json +import os +import random +import secrets +import sqlite3 +import time +import urllib.parse +import urllib.request +from datetime import datetime, timezone +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + + +BASE_DIR = Path(__file__).resolve().parent +BACKEND_DIR = BASE_DIR / "backend" +DB_DIR = BACKEND_DIR / "database" +DB_PATH = DB_DIR / "orders.db" +UPLOAD_DIR = BACKEND_DIR / "uploads" +PUBLIC_DIR = BASE_DIR / "frontend" + +ORDER_EXPIRY_SECONDS = int(os.getenv("ORDER_EXPIRY_SECONDS", "600")) +ACCOUNT_NAME = os.getenv("PAYMENT_ACCOUNT_NAME", "Your Account Name") +KBZPAY_NUMBER = os.getenv("KBZPAY_NUMBER", "09XXXXXXXXX") +AYAPAY_NUMBER = os.getenv("AYAPAY_NUMBER", "09YYYYYYYYY") +AYAPAY_QR_PATH = os.getenv("AYAPAY_QR_PATH", "/aya-pay-qr.png") +TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "") +TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "") +ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", "change-me") + +PRODUCT_MENU = { + "Mobile Legends": { + "86 Diamonds": 2500, + "172 Diamonds": 5000, + "257 Diamonds": 7500, + "344 Diamonds": 10000, + "429 Diamonds": 12500, + "Weekly Pass": 7000, + "Monthly Pass": 28000, + }, + "PUBG Mobile": { + "60 UC": 3200, + "325 UC": 15500, + "660 UC": 31000, + "1800 UC": 82000, + "3850 UC": 165000, + "8100 UC": 330000, + }, +} + +STATUSES = { + "pending", + "waiting_verification", + "paid", + "rejected", + "completed", + "expired", +} + + +def db(): + conn = sqlite3.connect(DB_PATH, timeout=10) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA busy_timeout = 10000") + return conn + + +def init_db(): + DB_DIR.mkdir(parents=True, exist_ok=True) + UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + PUBLIC_DIR.mkdir(exist_ok=True) + try: + with db() as conn: + try: + conn.execute("PRAGMA journal_mode = WAL") + except sqlite3.OperationalError as exc: + if "locked" not in str(exc).lower(): + raise + conn.execute( + """ + CREATE TABLE IF NOT EXISTS orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id TEXT NOT NULL UNIQUE, + game TEXT NOT NULL, + user_game_id TEXT NOT NULL, + product TEXT NOT NULL, + amount INTEGER NOT NULL, + payment_method TEXT, + transaction_id TEXT, + screenshot TEXT, + status TEXT NOT NULL CHECK(status IN ( + 'pending', + 'waiting_verification', + 'paid', + 'rejected', + 'completed', + 'expired' + )), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS transaction_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id TEXT NOT NULL, + action TEXT NOT NULL, + detail TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS order_queue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id TEXT NOT NULL UNIQUE, + status TEXT NOT NULL CHECK(status IN ( + 'waiting', + 'processing', + 'completed', + 'failed' + )), + attempts INTEGER NOT NULL DEFAULT 0, + last_error TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS smile_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + cookies TEXT, + balance INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL CHECK(status IN ( + 'active', + 'paused', + 'failed' + )), + last_used TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + except sqlite3.OperationalError as exc: + if "locked" not in str(exc).lower(): + raise + print("Warning: orders.db is locked by another app. Close DB Browser if order creation fails.") + + +def row_to_dict(row): + if row is None: + return None + data = dict(row) + data["expires_at"] = created_ts(data["created_at"]) + ORDER_EXPIRY_SECONDS + data["seconds_remaining"] = max(0, data["expires_at"] - int(time.time())) + return data + + +def created_ts(created_at): + created = datetime.strptime(created_at, "%Y-%m-%d %H:%M:%S") + return int(created.replace(tzinfo=timezone.utc).timestamp()) + + +def log_action(conn, order_id, action, detail=""): + conn.execute( + "INSERT INTO transaction_logs (order_id, action, detail) VALUES (?, ?, ?)", + (order_id, action, detail), + ) + + +def enqueue_paid_order(conn, order_id): + conn.execute( + """ + INSERT INTO order_queue (order_id, status) + VALUES (?, 'waiting') + ON CONFLICT(order_id) DO UPDATE SET + status = CASE + WHEN order_queue.status IN ('completed', 'processing') THEN order_queue.status + ELSE 'waiting' + END, + updated_at = CURRENT_TIMESTAMP + """, + (order_id,), + ) + log_action(conn, order_id, "queued", "Order is waiting for Smile.one automation") + + +def update_queue_status(order_id, status, detail=""): + if status not in {"waiting", "processing", "completed", "failed"}: + raise ValueError("Invalid queue status") + with db() as conn: + queue_row = conn.execute( + "SELECT * FROM order_queue WHERE order_id = ?", + (order_id,), + ).fetchone() + if queue_row is None: + return None + if status == "processing": + conn.execute( + """ + UPDATE order_queue + SET status = ?, attempts = attempts + 1, last_error = NULL, updated_at = CURRENT_TIMESTAMP + WHERE order_id = ? + """, + (status, order_id), + ) + else: + conn.execute( + """ + UPDATE order_queue + SET status = ?, last_error = ?, updated_at = CURRENT_TIMESTAMP + WHERE order_id = ? + """, + (status, detail, order_id), + ) + if status == "completed": + conn.execute("UPDATE orders SET status = 'completed' WHERE order_id = ?", (order_id,)) + log_action(conn, order_id, "completed", detail or "automation completed") + elif status == "failed": + log_action(conn, order_id, "automation_failed", detail) + elif status == "processing": + log_action(conn, order_id, "automation_processing", detail) + elif status == "waiting": + log_action(conn, order_id, "automation_retry", detail) + return dict( + conn.execute("SELECT * FROM order_queue WHERE order_id = ?", (order_id,)).fetchone() + ) + + +def expire_old_orders(): + cutoff = int(time.time()) - ORDER_EXPIRY_SECONDS + with db() as conn: + rows = conn.execute( + "SELECT order_id FROM orders WHERE status = 'pending' AND strftime('%s', created_at) < ?", + (cutoff,), + ).fetchall() + for row in rows: + conn.execute( + "UPDATE orders SET status = 'expired' WHERE order_id = ?", + (row["order_id"],), + ) + log_action(conn, row["order_id"], "expired", "Payment upload window elapsed") + + +def get_order(order_id): + expire_old_orders() + with db() as conn: + return row_to_dict( + conn.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone() + ) + + +def unique_order_id(): + return f"ORD{int(time.time())}{random.randint(100, 999)}" + + +def final_amount(product): + base = None + for products in PRODUCT_MENU.values(): + if product in products: + base = products[product] + break + if base is None: + raise ValueError("Unknown product") + return base + random.randint(11, 89) + + +def send_telegram(order): + if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID: + return + + caption = ( + f"Payment waiting verification\n" + f"Order: {order['order_id']}\n" + f"Amount: {order['amount']} MMK\n" + f"Method: {order['payment_method']}\n" + f"Game: {order['game']}\n" + f"User ID: {order['user_game_id']}\n" + f"Product: {order['product']}\n" + f"Txn ID: {order.get('transaction_id') or '-'}\n\n" + "Verify inside KBZPay/AYA Pay before confirming." + ) + keyboard = { + "inline_keyboard": [ + [ + {"text": "Confirm", "callback_data": f"confirm:{order['order_id']}"}, + {"text": "Reject", "callback_data": f"reject:{order['order_id']}"}, + ], + [{"text": "Completed", "callback_data": f"complete:{order['order_id']}"}], + ] + } + + screenshot_path = BASE_DIR / order["screenshot"] + url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendPhoto" + boundary = f"----codex{secrets.token_hex(8)}" + body = multipart_body( + boundary, + { + "chat_id": TELEGRAM_CHAT_ID, + "caption": caption, + "reply_markup": json.dumps(keyboard), + }, + "photo", + screenshot_path, + ) + request = urllib.request.Request( + url, + data=body, + headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, + ) + try: + urllib.request.urlopen(request, timeout=10).read() + except Exception as exc: + with db() as conn: + log_action(conn, order["order_id"], "telegram_failed", str(exc)) + + +def multipart_body(boundary, fields, file_field, file_path): + chunks = [] + for name, value in fields.items(): + chunks.append(f"--{boundary}\r\n".encode()) + chunks.append(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode()) + chunks.append(str(value).encode()) + chunks.append(b"\r\n") + chunks.append(f"--{boundary}\r\n".encode()) + chunks.append( + f'Content-Disposition: form-data; name="{file_field}"; filename="{file_path.name}"\r\n'.encode() + ) + chunks.append(b"Content-Type: image/jpeg\r\n\r\n") + chunks.append(file_path.read_bytes()) + chunks.append(b"\r\n") + chunks.append(f"--{boundary}--\r\n".encode()) + return b"".join(chunks) + + +def telegram_answer_callback(callback_id, text): + if not TELEGRAM_BOT_TOKEN or not callback_id: + return + url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/answerCallbackQuery" + data = urllib.parse.urlencode({"callback_query_id": callback_id, "text": text}).encode() + try: + urllib.request.urlopen(url, data=data, timeout=10).read() + except Exception: + pass + + +def send_telegram_text(text): + if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID: + return + url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage" + data = urllib.parse.urlencode({"chat_id": TELEGRAM_CHAT_ID, "text": text}).encode() + try: + urllib.request.urlopen(url, data=data, timeout=10).read() + except Exception: + pass + + +def update_order_status(order_id, status, detail=""): + if status not in STATUSES: + raise ValueError("Invalid status") + with db() as conn: + order = conn.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone() + if order is None: + return None + conn.execute("UPDATE orders SET status = ? WHERE order_id = ?", (status, order_id)) + if status == "paid": + enqueue_paid_order(conn, order_id) + log_action(conn, order_id, status, detail) + fresh = row_to_dict( + conn.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone() + ) + if status == "paid": + send_telegram_text(f"{order_id} is paid and ready to process on Smile.one.") + elif status == "rejected": + send_telegram_text(f"{order_id} was rejected. Do not process this order.") + elif status == "completed": + send_telegram_text(f"{order_id} has been marked completed.") + return fresh + + +class Handler(BaseHTTPRequestHandler): + def do_HEAD(self): + parsed = urllib.parse.urlparse(self.path) + if parsed.path == "/": + return self.head_file(PUBLIC_DIR / "index.html", "text/html") + if parsed.path.startswith("/uploads/"): + return self.head_file(UPLOAD_DIR / parsed.path.removeprefix("/uploads/"), guess_type(parsed.path)) + if parsed.path.startswith("/"): + return self.head_file(PUBLIC_DIR / parsed.path.lstrip("/"), guess_type(parsed.path)) + self.send_response(HTTPStatus.NOT_FOUND) + self.end_headers() + + def do_GET(self): + expire_old_orders() + parsed = urllib.parse.urlparse(self.path) + if parsed.path == "/": + return self.serve_file(PUBLIC_DIR / "index.html", "text/html") + if parsed.path == "/api/config": + return self.json( + { + "account_name": ACCOUNT_NAME, + "kbzpay_number": KBZPAY_NUMBER, + "ayapay_number": AYAPAY_NUMBER, + "ayapay_qr_path": AYAPAY_QR_PATH, + "menu": PRODUCT_MENU, + "expiry_seconds": ORDER_EXPIRY_SECONDS, + } + ) + if parsed.path.startswith("/api/orders/"): + order_id = parsed.path.rsplit("/", 1)[-1] + order = get_order(order_id) + if not order: + return self.error(HTTPStatus.NOT_FOUND, "Order not found") + return self.json({"order": order}) + if parsed.path == "/api/admin/orders": + if not self.is_admin(parsed): + return self.error(HTTPStatus.UNAUTHORIZED, "Invalid admin token") + with db() as conn: + rows = conn.execute( + "SELECT * FROM orders ORDER BY id DESC LIMIT 100" + ).fetchall() + return self.json({"orders": [row_to_dict(row) for row in rows]}) + if parsed.path == "/api/admin/queue": + if not self.is_admin(parsed): + return self.error(HTTPStatus.UNAUTHORIZED, "Invalid admin token") + with db() as conn: + rows = conn.execute( + """ + SELECT q.*, o.game, o.user_game_id, o.product, o.amount + FROM order_queue q + JOIN orders o ON o.order_id = q.order_id + ORDER BY q.id ASC + LIMIT 100 + """ + ).fetchall() + return self.json({"queue": [dict(row) for row in rows]}) + if parsed.path == "/api/admin/smile-accounts": + if not self.is_admin(parsed): + return self.error(HTTPStatus.UNAUTHORIZED, "Invalid admin token") + with db() as conn: + rows = conn.execute( + """ + SELECT id, email, balance, status, last_used, created_at, updated_at + FROM smile_accounts + ORDER BY status ASC, last_used ASC, id ASC + """ + ).fetchall() + return self.json({"accounts": [dict(row) for row in rows]}) + if parsed.path.startswith("/uploads/"): + return self.serve_file(UPLOAD_DIR / parsed.path.removeprefix("/uploads/"), guess_type(parsed.path)) + if parsed.path.startswith("/"): + return self.serve_file(PUBLIC_DIR / parsed.path.lstrip("/"), guess_type(parsed.path)) + return self.error(HTTPStatus.NOT_FOUND, "Not found") + + def do_POST(self): + expire_old_orders() + parsed = urllib.parse.urlparse(self.path) + if parsed.path == "/api/orders": + return self.create_order() + if parsed.path.startswith("/api/orders/") and parsed.path.endswith("/payment"): + order_id = parsed.path.split("/")[3] + return self.upload_payment(order_id) + if parsed.path == "/api/telegram/webhook": + return self.telegram_webhook() + if parsed.path.startswith("/api/admin/orders/"): + if not self.is_admin(parsed): + return self.error(HTTPStatus.UNAUTHORIZED, "Invalid admin token") + parts = parsed.path.split("/") + if len(parts) == 6 and parts[5] in {"confirm", "reject", "complete"}: + status = {"confirm": "paid", "reject": "rejected", "complete": "completed"}[parts[5]] + order = update_order_status(parts[4], status, "admin panel action") + if not order: + return self.error(HTTPStatus.NOT_FOUND, "Order not found") + return self.json({"order": order}) + if parsed.path.startswith("/api/admin/queue/"): + if not self.is_admin(parsed): + return self.error(HTTPStatus.UNAUTHORIZED, "Invalid admin token") + parts = parsed.path.split("/") + if len(parts) == 6 and parts[5] in {"process", "complete", "fail", "retry"}: + payload = self.read_json() + detail = (payload.get("detail") or "").strip() + status = { + "process": "processing", + "complete": "completed", + "fail": "failed", + "retry": "waiting", + }[parts[5]] + queue_item = update_queue_status(parts[4], status, detail) + if not queue_item: + return self.error(HTTPStatus.NOT_FOUND, "Queue item not found") + return self.json({"queue_item": queue_item}) + return self.error(HTTPStatus.NOT_FOUND, "Not found") + + def create_order(self): + payload = self.read_json() + game = payload.get("game", "").strip() + user_game_id = payload.get("user_game_id", "").strip() + product = payload.get("product", "").strip() + if not game or not user_game_id or not product: + return self.error(HTTPStatus.BAD_REQUEST, "game, user_game_id, and product are required") + if game not in PRODUCT_MENU or product not in PRODUCT_MENU[game]: + return self.error(HTTPStatus.BAD_REQUEST, "Selected product is not available for this game") + try: + amount = final_amount(product) + except ValueError as exc: + return self.error(HTTPStatus.BAD_REQUEST, str(exc)) + + order_id = unique_order_id() + try: + with db() as conn: + conn.execute( + """ + INSERT INTO orders (order_id, game, user_game_id, product, amount, status) + VALUES (?, ?, ?, ?, ?, 'pending') + """, + (order_id, game, user_game_id, product, amount), + ) + log_action(conn, order_id, "created", f"{product} for {user_game_id}") + except sqlite3.OperationalError as exc: + if "locked" in str(exc).lower(): + return self.error( + HTTPStatus.SERVICE_UNAVAILABLE, + "Database is locked. Close DB Browser or any app currently editing orders.db, then try again.", + ) + raise + return self.json({"order_id": order_id, "amount": amount}) + + def upload_payment(self, order_id): + order = get_order(order_id) + if not order: + return self.error(HTTPStatus.NOT_FOUND, "Order not found") + if order["status"] == "expired": + return self.error(HTTPStatus.GONE, "Order expired") + if order["status"] not in {"pending", "rejected"}: + return self.error(HTTPStatus.CONFLICT, "Payment upload is not available for this order") + + form = cgi.FieldStorage( + fp=self.rfile, + headers=self.headers, + environ={ + "REQUEST_METHOD": "POST", + "CONTENT_TYPE": self.headers.get("Content-Type"), + }, + ) + payment_method = (form.getfirst("payment_method") or "").strip() + transaction_id = (form.getfirst("transaction_id") or "").strip() + if payment_method not in {"KBZPay", "AYA Pay"}: + return self.error(HTTPStatus.BAD_REQUEST, "Invalid payment method") + file_item = form["screenshot"] if "screenshot" in form else None + if file_item is None or not file_item.filename: + return self.error(HTTPStatus.BAD_REQUEST, "Screenshot is required") + + extension = Path(file_item.filename).suffix.lower() + if extension not in {".jpg", ".jpeg", ".png", ".webp"}: + return self.error(HTTPStatus.BAD_REQUEST, "Screenshot must be jpg, png, or webp") + safe_name = f"{order_id}-{secrets.token_hex(6)}{extension}" + output_path = UPLOAD_DIR / safe_name + with output_path.open("wb") as fh: + fh.write(file_item.file.read()) + relative_path = f"uploads/{safe_name}" + + with db() as conn: + conn.execute( + """ + UPDATE orders + SET screenshot = ?, payment_method = ?, transaction_id = ?, status = 'waiting_verification' + WHERE order_id = ? + """, + (relative_path, payment_method, transaction_id, order_id), + ) + log_action(conn, order_id, "screenshot_uploaded", payment_method) + fresh = row_to_dict( + conn.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone() + ) + send_telegram(fresh) + return self.json({"order": fresh}) + + def telegram_webhook(self): + payload = self.read_json() + callback = payload.get("callback_query") or {} + data = callback.get("data", "") + callback_id = callback.get("id") + if ":" not in data: + return self.json({"ok": True}) + action, order_id = data.split(":", 1) + status_map = {"confirm": "paid", "reject": "rejected", "complete": "completed"} + if action not in status_map: + return self.json({"ok": True}) + status = status_map[action] + detail = "telegram callback" + order = update_order_status(order_id, status, detail) + telegram_answer_callback(callback_id, f"{order_id} marked {status}" if order else "Order not found") + return self.json({"ok": bool(order), "order": order}) + + def read_json(self): + length = int(self.headers.get("Content-Length", "0")) + if length == 0: + return {} + return json.loads(self.rfile.read(length).decode()) + + def is_admin(self, parsed): + params = urllib.parse.parse_qs(parsed.query) + token = params.get("token", [""])[0] + return secrets.compare_digest(token, ADMIN_TOKEN) + + def serve_file(self, path, content_type): + try: + resolved = path.resolve() + if not str(resolved).startswith(str(BASE_DIR)): + return self.error(HTTPStatus.FORBIDDEN, "Forbidden") + data = resolved.read_bytes() + except FileNotFoundError: + return self.error(HTTPStatus.NOT_FOUND, "Not found") + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", content_type) + if resolved.parent == PUBLIC_DIR: + self.send_header("Cache-Control", "no-store") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def head_file(self, path, content_type): + try: + resolved = path.resolve() + if not str(resolved).startswith(str(BASE_DIR)): + self.send_response(HTTPStatus.FORBIDDEN) + self.end_headers() + return + size = resolved.stat().st_size + except FileNotFoundError: + self.send_response(HTTPStatus.NOT_FOUND) + self.end_headers() + return + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", content_type) + if resolved.parent == PUBLIC_DIR: + self.send_header("Cache-Control", "no-store") + self.send_header("Content-Length", str(size)) + self.end_headers() + + def json(self, payload, status=HTTPStatus.OK): + data = json.dumps(payload).encode() + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def error(self, status, message): + return self.json({"error": message}, status) + + def log_message(self, fmt, *args): + print(f"{self.address_string()} - {fmt % args}") + + +def guess_type(path): + if path.endswith(".html"): + return "text/html" + if path.endswith(".css"): + return "text/css" + if path.endswith(".js"): + return "application/javascript" + if path.endswith(".png"): + return "image/png" + if path.endswith(".jpg") or path.endswith(".jpeg"): + return "image/jpeg" + if path.endswith(".webp"): + return "image/webp" + if path.endswith(".svg"): + return "image/svg+xml" + return "application/octet-stream" + + +if __name__ == "__main__": + init_db() + port = int(os.getenv("PORT", "8000")) + server = ThreadingHTTPServer(("127.0.0.1", port), Handler) + print(f"Server running at http://127.0.0.1:{port}") + server.serve_forever() diff --git a/SmartCoin/start_server.sh b/SmartCoin/start_server.sh new file mode 100755 index 0000000..f0cad6b --- /dev/null +++ b/SmartCoin/start_server.sh @@ -0,0 +1,49 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +PID_FILE="$SCRIPT_DIR/server.pid" +LOG_FILE="$SCRIPT_DIR/server.log" + +PORT="${PORT:-8000}" +ADMIN_TOKEN="${ADMIN_TOKEN:-test-token}" + +if [ -f "$PID_FILE" ]; then + OLD_PID=$(cat "$PID_FILE") + if kill -0 "$OLD_PID" 2>/dev/null; then + echo "Server already running on port $PORT with PID $OLD_PID" + echo "Open: http://127.0.0.1:$PORT/" + exit 0 + fi + rm -f "$PID_FILE" +fi + +cd "$SCRIPT_DIR" +nohup env PORT="$PORT" ADMIN_TOKEN="$ADMIN_TOKEN" PYTHONUNBUFFERED=1 python3 server.py >> "$LOG_FILE" 2>&1 & +PID=$! +echo "$PID" > "$PID_FILE" + +COUNT=0 +READY=0 +while kill -0 "$PID" 2>/dev/null; do + if curl -fsS "http://127.0.0.1:$PORT/api/config" >/dev/null 2>&1; then + READY=1 + break + fi + COUNT=$((COUNT + 1)) + if [ "$COUNT" -ge 20 ]; then + break + fi + sleep 0.25 +done + +if [ "$READY" -eq 1 ]; then + echo "Server started on port $PORT with PID $PID" + echo "Open: http://127.0.0.1:$PORT/" + echo "Log: $LOG_FILE" +else + rm -f "$PID_FILE" + echo "Server failed to start. Last log lines:" + tail -20 "$LOG_FILE" 2>/dev/null || true + exit 1 +fi diff --git a/SmartCoin/stop_server.sh b/SmartCoin/stop_server.sh new file mode 100755 index 0000000..b2df80e --- /dev/null +++ b/SmartCoin/stop_server.sh @@ -0,0 +1,36 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +PID_FILE="$SCRIPT_DIR/server.pid" +PORT="${PORT:-8000}" + +PID="" +if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") +fi + +if [ -z "$PID" ] || ! kill -0 "$PID" 2>/dev/null; then + PID=$(lsof -tiTCP:"$PORT" -sTCP:LISTEN 2>/dev/null || true) +fi + +if [ -z "$PID" ]; then + rm -f "$PID_FILE" + echo "No server is running on port $PORT" + exit 0 +fi + +kill "$PID" + +COUNT=0 +while kill -0 "$PID" 2>/dev/null; do + COUNT=$((COUNT + 1)) + if [ "$COUNT" -ge 20 ]; then + echo "Server did not stop after 10 seconds. Try: kill -9 $PID" + exit 1 + fi + sleep 0.5 +done + +rm -f "$PID_FILE" +echo "Server stopped. PID was $PID"