diff --git a/docker-compose.yml b/docker-compose.yml index 203f9e5..b9c0f75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,22 @@ services: timeout: 5s retries: 5 + redis: + image: redis:7-alpine + container_name: lightning-lotto-redis + ports: + - "6379:6379" + networks: + - lightning-lotto-network + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + backend: build: context: ./back_end @@ -66,10 +82,32 @@ services: - lightning-lotto-network restart: unless-stopped + telegram-bot: + build: + context: ./telegram_bot + dockerfile: Dockerfile + container_name: lightning-lotto-telegram-bot + depends_on: + - backend + - redis + environment: + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN} + API_BASE_URL: http://backend:3000 + FRONTEND_BASE_URL: ${FRONTEND_BASE_URL:-http://localhost:3001} + REDIS_URL: redis://redis:6379 + MAX_TICKETS_PER_PURCHASE: ${MAX_TICKETS_PER_PURCHASE:-100} + PAYMENT_POLL_INTERVAL_MS: ${PAYMENT_POLL_INTERVAL_MS:-5000} + PAYMENT_POLL_TIMEOUT_MS: ${PAYMENT_POLL_TIMEOUT_MS:-900000} + LOG_LEVEL: ${LOG_LEVEL:-info} + NODE_ENV: production + networks: + - lightning-lotto-network + restart: unless-stopped + volumes: postgres_data: + redis_data: networks: lightning-lotto-network: driver: bridge - diff --git a/telegram_bot/.gitignore b/telegram_bot/.gitignore new file mode 100644 index 0000000..072e016 --- /dev/null +++ b/telegram_bot/.gitignore @@ -0,0 +1,28 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment variables +.env +.env.local +.env.*.local + +# Logs +logs/ +*.log +npm-debug.log* + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Debug +*.swp +*.swo + diff --git a/telegram_bot/Dockerfile b/telegram_bot/Dockerfile new file mode 100644 index 0000000..441f4d1 --- /dev/null +++ b/telegram_bot/Dockerfile @@ -0,0 +1,44 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY tsconfig.json ./ +COPY src/ ./src/ + +# Build TypeScript +RUN npm run build + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install production dependencies only +RUN npm ci --only=production && npm cache clean --force + +# Copy built files from builder stage +COPY --from=builder /app/dist ./dist + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +USER nodejs + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD node -e "console.log('Bot running')" || exit 1 + +CMD ["node", "dist/index.js"] + diff --git a/telegram_bot/README.md b/telegram_bot/README.md new file mode 100644 index 0000000..4c84bc1 --- /dev/null +++ b/telegram_bot/README.md @@ -0,0 +1,146 @@ +# Lightning Jackpot Telegram Bot โšก๐ŸŽฐ + +A Telegram bot for the Lightning Jackpot lottery system. Users can buy tickets, check their tickets and wins, and receive instant notifications - all through Telegram! + +## Features + +- ๐ŸŽŸ **Buy Tickets** - Quick buttons for 1, 2, 5, 10 tickets or custom amounts +- ๐Ÿ’ธ **Lightning Payments** - Generate and pay Lightning invoices directly in chat +- ๐Ÿ“ฑ **QR Codes** - Visual QR codes for easy wallet scanning +- ๐Ÿ”” **Real-time Updates** - Automatic payment confirmation notifications +- ๐Ÿงพ **View Tickets** - See all your ticket purchases and their status +- ๐Ÿ† **Track Wins** - View your winning history and payouts +- โšก **Lightning Address** - Manage your payout address + +## Prerequisites + +- Node.js 18+ +- Telegram Bot Token (from [@BotFather](https://t.me/BotFather)) +- Lightning Jackpot Backend API running + +## Installation + +1. Clone the repository and navigate to the bot directory: + ```bash + cd telegram_bot + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Create environment file: + ```bash + cp env.example .env + ``` + +4. Configure your `.env` file: + ```env + TELEGRAM_BOT_TOKEN=your_bot_token_here + API_BASE_URL=http://localhost:3000 + FRONTEND_BASE_URL=http://localhost:3001 + ``` + +## Development + +Run the bot in development mode with auto-reload: +```bash +npm run dev:watch +``` + +Or run once: +```bash +npm run dev +``` + +## Production + +Build and run: +```bash +npm run build +npm start +``` + +## Docker + +Build the image: +```bash +docker build -t lightning-lotto-telegram-bot . +``` + +Run the container: +```bash +docker run -d \ + --name telegram-bot \ + -e TELEGRAM_BOT_TOKEN=your_token \ + -e API_BASE_URL=http://backend:3000 \ + -e FRONTEND_BASE_URL=http://frontend:3001 \ + lightning-lotto-telegram-bot +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `/start` | Begin using the bot | +| `/menu` | Show main menu | +| `/buy` | Buy lottery tickets | +| `/tickets` | View your tickets | +| `/wins` | View your past wins | +| `/address` | Update Lightning Address | +| `/help` | Help & information | + +## Architecture + +``` +src/ +โ”œโ”€โ”€ config/ # Configuration management +โ”œโ”€โ”€ handlers/ # Command and callback handlers +โ”‚ โ”œโ”€โ”€ start.ts # /start command +โ”‚ โ”œโ”€โ”€ buy.ts # Ticket purchase flow +โ”‚ โ”œโ”€โ”€ tickets.ts # View tickets +โ”‚ โ”œโ”€โ”€ wins.ts # View wins +โ”‚ โ”œโ”€โ”€ address.ts # Lightning address management +โ”‚ โ”œโ”€โ”€ help.ts # Help command +โ”‚ โ””โ”€โ”€ menu.ts # Menu handling +โ”œโ”€โ”€ services/ +โ”‚ โ”œโ”€โ”€ api.ts # Backend API client +โ”‚ โ”œโ”€โ”€ state.ts # User state management (Redis/in-memory) +โ”‚ โ”œโ”€โ”€ qr.ts # QR code generation +โ”‚ โ””โ”€โ”€ logger.ts # Logging service +โ”œโ”€โ”€ types/ # TypeScript type definitions +โ”œโ”€โ”€ utils/ +โ”‚ โ”œโ”€โ”€ format.ts # Formatting utilities +โ”‚ โ””โ”€โ”€ keyboards.ts # Telegram keyboard builders +โ””โ”€โ”€ index.ts # Main entry point +``` + +## State Management + +The bot uses Redis for persistent state management when available. If Redis is not configured, it falls back to in-memory storage (not recommended for production). + +User states: +- `idle` - Default state, browsing menu +- `awaiting_lightning_address` - Waiting for user to enter Lightning Address +- `awaiting_ticket_amount` - Waiting for custom ticket amount +- `awaiting_invoice_payment` - Polling for payment +- `updating_address` - Updating Lightning Address + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `TELEGRAM_BOT_TOKEN` | Telegram Bot API token | Required | +| `API_BASE_URL` | Backend API URL | `http://localhost:3000` | +| `FRONTEND_BASE_URL` | Frontend URL for ticket links | `http://localhost:3001` | +| `REDIS_URL` | Redis connection URL | Optional | +| `MAX_TICKETS_PER_PURCHASE` | Maximum tickets per purchase | `100` | +| `PAYMENT_POLL_INTERVAL_MS` | Payment polling interval | `5000` | +| `PAYMENT_POLL_TIMEOUT_MS` | Payment polling timeout | `900000` | +| `LOG_LEVEL` | Logging level | `info` | + +## License + +MIT + diff --git a/telegram_bot/env.example b/telegram_bot/env.example new file mode 100644 index 0000000..5f9df68 --- /dev/null +++ b/telegram_bot/env.example @@ -0,0 +1,22 @@ +# Telegram Bot Configuration +TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here + +# Backend API Configuration +API_BASE_URL=http://localhost:3000 + +# Frontend URL (for generating ticket links) +FRONTEND_BASE_URL=http://localhost:3001 + +# Redis Configuration (optional - falls back to in-memory if not set) +REDIS_URL=redis://localhost:6379 + +# Bot Configuration +MAX_TICKETS_PER_PURCHASE=100 +PAYMENT_POLL_INTERVAL_MS=5000 +PAYMENT_POLL_TIMEOUT_MS=900000 +INVOICE_EXPIRY_MINUTES=15 + +# Logging +LOG_LEVEL=info +NODE_ENV=development + diff --git a/telegram_bot/package-lock.json b/telegram_bot/package-lock.json new file mode 100644 index 0000000..d7dd3dd --- /dev/null +++ b/telegram_bot/package-lock.json @@ -0,0 +1,3753 @@ +{ + "name": "lightning-lotto-telegram-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lightning-lotto-telegram-bot", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "axios": "^1.6.2", + "dotenv": "^16.3.1", + "ioredis": "^5.3.2", + "node-telegram-bot-api": "^0.64.0", + "qrcode": "^1.5.3", + "winston": "^3.11.0" + }, + "devDependencies": { + "@types/node": "^20.10.4", + "@types/node-telegram-bot-api": "^0.64.2", + "@types/qrcode": "^1.5.5", + "nodemon": "^3.0.2", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.9.tgz", + "integrity": "sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==", + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.4", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.14.0", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request-promise": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@cypress/request-promise/-/request-promise-5.0.0.tgz", + "integrity": "sha512-eKdYVpa9cBEw2kTBlHeu1PP16Blwtum6QHg/u9s/MoHkZfuo1pRGka1VlUHXF5kdew82BvOJVVGk0x8X0nbp+w==", + "license": "ISC", + "dependencies": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^4.1.3" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "@cypress/request": "^3.0.0" + } + }, + "node_modules/@cypress/request-promise/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@cypress/request/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-telegram-bot-api": { + "version": "0.64.13", + "resolved": "https://registry.npmjs.org/@types/node-telegram-bot-api/-/node-telegram-bot-api-0.64.13.tgz", + "integrity": "sha512-/d3sYpZq4sDwKWAm9XsIsb8MclclJ1yPXZC7uAzZRrJIFY8yg/63oNrLf9CBTlaS/XGR1N66BwibfJQGiWVsqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/request": "*" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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/ansi-styles/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/ansi-styles/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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findindex": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.findindex/-/array.prototype.findindex-2.2.4.tgz", + "integrity": "sha512-LLm4mhxa9v8j0A/RPnpQAP4svXToJFh+Hp1pNYl5ZD5qpB4zdx/D4YjpVcETkhFbUKWO3iGMVLvrOnnmkAJT6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "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/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "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/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "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/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT", + "peer": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT", + "peer": true + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ioredis": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.4.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT", + "peer": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/node-telegram-bot-api": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.64.0.tgz", + "integrity": "sha512-/gxCuaEDUyWMBiHInP0ufopUkaaKprXiv3lyP9MMZdPy2KPfYKNYNKfd1Ph7o9KhfURDtOYowPZCi4UCr+2caw==", + "license": "MIT", + "dependencies": { + "@cypress/request": "^3.0.1", + "@cypress/request-promise": "^5.0.0", + "array.prototype.findindex": "^2.0.2", + "bl": "^1.2.3", + "debug": "^3.2.7", + "eventemitter3": "^3.0.0", + "file-type": "^3.9.0", + "mime": "^1.6.0", + "pump": "^2.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/node-telegram-bot-api/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "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/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "license": "ISC", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/request/node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "peer": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "peer": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "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/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "license": "ISC", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "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/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/winston": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", + "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/telegram_bot/package.json b/telegram_bot/package.json new file mode 100644 index 0000000..42d1c23 --- /dev/null +++ b/telegram_bot/package.json @@ -0,0 +1,37 @@ +{ + "name": "lightning-lotto-telegram-bot", + "version": "1.0.0", + "description": "Telegram bot for Lightning Lottery - Buy tickets, check wins, and receive payouts via Lightning", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node src/index.ts", + "dev:watch": "nodemon --exec ts-node src/index.ts" + }, + "keywords": [ + "telegram", + "bot", + "lightning", + "lottery", + "bitcoin" + ], + "license": "MIT", + "dependencies": { + "axios": "^1.6.2", + "dotenv": "^16.3.1", + "ioredis": "^5.3.2", + "node-telegram-bot-api": "^0.64.0", + "qrcode": "^1.5.3", + "winston": "^3.11.0" + }, + "devDependencies": { + "@types/node": "^20.10.4", + "@types/node-telegram-bot-api": "^0.64.2", + "@types/qrcode": "^1.5.5", + "nodemon": "^3.0.2", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} + diff --git a/telegram_bot/src/config/index.ts b/telegram_bot/src/config/index.ts new file mode 100644 index 0000000..363f490 --- /dev/null +++ b/telegram_bot/src/config/index.ts @@ -0,0 +1,50 @@ +import dotenv from 'dotenv'; + +dotenv.config(); + +function required(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + return value; +} + +function optional(name: string, defaultValue: string): string { + return process.env[name] || defaultValue; +} + +function optionalInt(name: string, defaultValue: number): number { + const value = process.env[name]; + if (!value) return defaultValue; + const parsed = parseInt(value, 10); + return isNaN(parsed) ? defaultValue : parsed; +} + +export const config = { + telegram: { + botToken: required('TELEGRAM_BOT_TOKEN'), + }, + api: { + baseUrl: optional('API_BASE_URL', 'http://localhost:3000'), + }, + frontend: { + baseUrl: optional('FRONTEND_BASE_URL', 'http://localhost:3001'), + }, + redis: { + url: process.env.REDIS_URL || null, + }, + bot: { + maxTicketsPerPurchase: optionalInt('MAX_TICKETS_PER_PURCHASE', 100), + paymentPollIntervalMs: optionalInt('PAYMENT_POLL_INTERVAL_MS', 5000), + paymentPollTimeoutMs: optionalInt('PAYMENT_POLL_TIMEOUT_MS', 900000), // 15 minutes + invoiceExpiryMinutes: optionalInt('INVOICE_EXPIRY_MINUTES', 15), + }, + logging: { + level: optional('LOG_LEVEL', 'info'), + }, + nodeEnv: optional('NODE_ENV', 'development'), +}; + +export default config; + diff --git a/telegram_bot/src/handlers/address.ts b/telegram_bot/src/handlers/address.ts new file mode 100644 index 0000000..0799fe1 --- /dev/null +++ b/telegram_bot/src/handlers/address.ts @@ -0,0 +1,106 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { stateManager } from '../services/state'; +import { logger, logUserAction } from '../services/logger'; +import { getMainMenuKeyboard, getCancelKeyboard } from '../utils/keyboards'; +import { isValidLightningAddress } from '../utils/format'; +import { messages } from '../messages'; + +/** + * Handle /address command or "Lightning Address" button + */ +export async function handleAddressCommand( + bot: TelegramBot, + msg: TelegramBot.Message +): Promise { + const chatId = msg.chat.id; + const userId = msg.from?.id; + + if (!userId) { + await bot.sendMessage(chatId, messages.errors.userNotIdentified); + return; + } + + logUserAction(userId, 'Requested address update'); + + try { + const user = await stateManager.getUser(userId); + + if (!user) { + await bot.sendMessage(chatId, messages.errors.startFirst); + return; + } + + const message = user.lightningAddress + ? messages.address.currentAddress(user.lightningAddress) + : messages.address.noAddressSet; + + await bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: getCancelKeyboard(), + }); + + await stateManager.updateUserState(userId, 'updating_address'); + } catch (error) { + logger.error('Error in handleAddressCommand', { error, userId }); + await bot.sendMessage(chatId, messages.errors.generic); + } +} + +/** + * Handle incoming Lightning Address from user + */ +export async function handleLightningAddressInput( + bot: TelegramBot, + msg: TelegramBot.Message +): Promise { + const chatId = msg.chat.id; + const userId = msg.from?.id; + const text = msg.text?.trim(); + + if (!userId || !text) return false; + + try { + const user = await stateManager.getUser(userId); + + if (!user) return false; + + // Check if user is in a state expecting lightning address + if (user.state !== 'awaiting_lightning_address' && user.state !== 'updating_address') { + return false; + } + + // Validate lightning address format + if (!isValidLightningAddress(text)) { + await bot.sendMessage(chatId, messages.address.invalidFormat, { + parse_mode: 'Markdown', + reply_markup: getCancelKeyboard(), + }); + return true; + } + + // Save the lightning address + await stateManager.updateLightningAddress(userId, text); + + logUserAction(userId, 'Lightning address updated'); + + const responseMessage = user.state === 'awaiting_lightning_address' + ? messages.address.firstTimeSuccess(text) + : messages.address.updateSuccess(text); + + await bot.sendMessage(chatId, responseMessage, { + parse_mode: 'Markdown', + reply_markup: getMainMenuKeyboard(), + }); + + return true; + } catch (error) { + logger.error('Error in handleLightningAddressInput', { error, userId }); + await bot.sendMessage(chatId, messages.address.saveFailed); + return true; + } +} + +export default { + handleAddressCommand, + handleLightningAddressInput, +}; diff --git a/telegram_bot/src/handlers/buy.ts b/telegram_bot/src/handlers/buy.ts new file mode 100644 index 0000000..73a38f8 --- /dev/null +++ b/telegram_bot/src/handlers/buy.ts @@ -0,0 +1,426 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { stateManager } from '../services/state'; +import { apiClient } from '../services/api'; +import { generateQRCode } from '../services/qr'; +import { logger, logUserAction, logPaymentEvent } from '../services/logger'; +import config from '../config'; +import { + getTicketAmountKeyboard, + getConfirmationKeyboard, + getViewTicketKeyboard, + getMainMenuKeyboard, + getCancelKeyboard, +} from '../utils/keyboards'; +import { formatSats, formatDate, formatTimeUntil } from '../utils/format'; +import { PendingPurchaseData, AwaitingPaymentData } from '../types'; +import { messages } from '../messages'; + +/** + * Handle /buy command or "Buy Tickets" button + */ +export async function handleBuyCommand( + bot: TelegramBot, + msg: TelegramBot.Message +): Promise { + const chatId = msg.chat.id; + const userId = msg.from?.id; + + if (!userId) { + await bot.sendMessage(chatId, messages.errors.userNotIdentified); + return; + } + + logUserAction(userId, 'Initiated ticket purchase'); + + try { + const user = await stateManager.getUser(userId); + + if (!user) { + await bot.sendMessage(chatId, messages.errors.startFirst); + return; + } + + // Check if lightning address is set + if (!user.lightningAddress) { + await bot.sendMessage(chatId, messages.address.needAddressFirst, { + parse_mode: 'Markdown', + reply_markup: getCancelKeyboard(), + }); + await stateManager.updateUserState(userId, 'awaiting_lightning_address'); + return; + } + + // Get next jackpot info + const jackpot = await apiClient.getNextJackpot(); + + if (!jackpot) { + await bot.sendMessage(chatId, messages.buy.noActiveJackpot, { parse_mode: 'Markdown' }); + return; + } + + // Show jackpot info and ticket selection + const drawTime = new Date(jackpot.cycle.scheduled_at); + const message = messages.buy.jackpotInfo( + formatSats(jackpot.cycle.pot_total_sats), + formatSats(jackpot.lottery.ticket_price_sats), + formatDate(drawTime), + formatTimeUntil(drawTime) + ); + + await bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: getTicketAmountKeyboard(), + }); + + // Store jackpot info in state for later use + await stateManager.updateUserState(userId, 'idle', { + cycleId: jackpot.cycle.id, + scheduledAt: jackpot.cycle.scheduled_at, + ticketPrice: jackpot.lottery.ticket_price_sats, + lotteryName: jackpot.lottery.name, + }); + } catch (error) { + logger.error('Error in handleBuyCommand', { error, userId }); + await bot.sendMessage(chatId, messages.errors.systemUnavailable); + } +} + +/** + * Handle ticket amount selection + */ +export async function handleTicketAmountSelection( + bot: TelegramBot, + query: TelegramBot.CallbackQuery, + amount: number | 'custom' +): Promise { + const chatId = query.message?.chat.id; + const userId = query.from.id; + const messageId = query.message?.message_id; + + if (!chatId || !messageId) return; + + await bot.answerCallbackQuery(query.id); + + try { + const user = await stateManager.getUser(userId); + if (!user) { + await bot.sendMessage(chatId, messages.errors.startFirst); + return; + } + + if (amount === 'custom') { + // Ask for custom amount + await bot.editMessageText( + messages.buy.customAmountPrompt(config.bot.maxTicketsPerPurchase), + { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + } + ); + + // Get fresh jackpot info + const jackpot = await apiClient.getNextJackpot(); + if (!jackpot) { + await bot.sendMessage(chatId, messages.buy.jackpotUnavailable); + return; + } + + await stateManager.updateUserState(userId, 'awaiting_ticket_amount', { + cycleId: jackpot.cycle.id, + scheduledAt: jackpot.cycle.scheduled_at, + ticketPrice: jackpot.lottery.ticket_price_sats, + lotteryName: jackpot.lottery.name, + }); + return; + } + + // Process selected amount + await processTicketSelection(bot, chatId, messageId, userId, amount); + } catch (error) { + logger.error('Error in handleTicketAmountSelection', { error, userId }); + await bot.sendMessage(chatId, messages.errors.generic); + } +} + +/** + * Handle custom ticket amount input + */ +export async function handleCustomTicketAmount( + bot: TelegramBot, + msg: TelegramBot.Message +): Promise { + const chatId = msg.chat.id; + const userId = msg.from?.id; + const text = msg.text?.trim(); + + if (!userId || !text) return false; + + try { + const user = await stateManager.getUser(userId); + if (!user || user.state !== 'awaiting_ticket_amount') { + return false; + } + + const amount = parseInt(text, 10); + + if (isNaN(amount) || amount < 1) { + await bot.sendMessage( + chatId, + messages.buy.invalidNumber(config.bot.maxTicketsPerPurchase), + { reply_markup: getCancelKeyboard() } + ); + return true; + } + + if (amount > config.bot.maxTicketsPerPurchase) { + await bot.sendMessage( + chatId, + messages.buy.tooManyTickets(config.bot.maxTicketsPerPurchase), + { reply_markup: getCancelKeyboard() } + ); + return true; + } + + await processTicketSelection(bot, chatId, undefined, userId, amount); + return true; + } catch (error) { + logger.error('Error in handleCustomTicketAmount', { error, userId }); + await bot.sendMessage(chatId, messages.errors.generic); + return true; + } +} + +/** + * Process ticket selection and show confirmation + */ +async function processTicketSelection( + bot: TelegramBot, + chatId: number, + messageId: number | undefined, + userId: number, + amount: number +): Promise { + // Get fresh jackpot info + const jackpot = await apiClient.getNextJackpot(); + if (!jackpot) { + await bot.sendMessage(chatId, messages.buy.jackpotUnavailable); + return; + } + + const totalAmount = amount * jackpot.lottery.ticket_price_sats; + + const confirmMessage = messages.buy.confirmPurchase( + amount, + formatSats(jackpot.lottery.ticket_price_sats), + formatSats(totalAmount), + formatDate(jackpot.cycle.scheduled_at) + ); + + const stateData: PendingPurchaseData = { + ticketCount: amount, + cycleId: jackpot.cycle.id, + scheduledAt: jackpot.cycle.scheduled_at, + ticketPrice: jackpot.lottery.ticket_price_sats, + totalAmount, + lotteryName: jackpot.lottery.name, + }; + + await stateManager.updateUserState(userId, 'idle', stateData); + + if (messageId) { + await bot.editMessageText(confirmMessage, { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + reply_markup: getConfirmationKeyboard(), + }); + } else { + await bot.sendMessage(chatId, confirmMessage, { + parse_mode: 'Markdown', + reply_markup: getConfirmationKeyboard(), + }); + } +} + +/** + * Handle purchase confirmation + */ +export async function handlePurchaseConfirmation( + bot: TelegramBot, + query: TelegramBot.CallbackQuery +): Promise { + const chatId = query.message?.chat.id; + const userId = query.from.id; + const messageId = query.message?.message_id; + + if (!chatId || !messageId) return; + + await bot.answerCallbackQuery(query.id, { text: messages.buy.creatingInvoice }); + + try { + const user = await stateManager.getUser(userId); + if (!user || !user.lightningAddress) { + await bot.sendMessage(chatId, messages.errors.setAddressFirst); + return; + } + + const pendingData = user.stateData as PendingPurchaseData | undefined; + if (!pendingData?.ticketCount) { + await bot.sendMessage(chatId, messages.errors.noPendingPurchase); + return; + } + + logUserAction(userId, 'Confirmed purchase', { tickets: pendingData.ticketCount }); + + // Create invoice + const purchaseResult = await apiClient.buyTickets( + pendingData.ticketCount, + user.lightningAddress, + userId + ); + + logPaymentEvent(userId, purchaseResult.ticket_purchase_id, 'created', { + tickets: pendingData.ticketCount, + amount: pendingData.totalAmount, + }); + + // Generate QR code + const qrBuffer = await generateQRCode(purchaseResult.invoice.payment_request); + + // Update message to show invoice + await bot.editMessageText(messages.buy.invoiceCreated, { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + }); + + // Send QR code + await bot.sendPhoto(chatId, qrBuffer, { + caption: messages.buy.invoiceCaption( + pendingData.ticketCount, + formatSats(pendingData.totalAmount), + purchaseResult.invoice.payment_request, + config.bot.invoiceExpiryMinutes + ), + parse_mode: 'Markdown', + reply_markup: getViewTicketKeyboard( + purchaseResult.ticket_purchase_id, + purchaseResult.public_url + ), + }); + + // Store purchase and start polling + const paymentData: AwaitingPaymentData = { + ...pendingData, + purchaseId: purchaseResult.ticket_purchase_id, + paymentRequest: purchaseResult.invoice.payment_request, + publicUrl: purchaseResult.public_url, + pollStartTime: Date.now(), + }; + + await stateManager.storePurchase(userId, purchaseResult.ticket_purchase_id, paymentData); + await stateManager.updateUserState(userId, 'awaiting_invoice_payment', paymentData); + + // Start payment polling + pollPaymentStatus(bot, chatId, userId, purchaseResult.ticket_purchase_id); + } catch (error) { + logger.error('Error in handlePurchaseConfirmation', { error, userId }); + await bot.sendMessage(chatId, messages.errors.invoiceCreationFailed, { + reply_markup: getMainMenuKeyboard(), + }); + await stateManager.clearUserStateData(userId); + } +} + +/** + * Poll payment status + */ +async function pollPaymentStatus( + bot: TelegramBot, + chatId: number, + userId: number, + purchaseId: string +): Promise { + const pollInterval = config.bot.paymentPollIntervalMs; + const timeout = config.bot.paymentPollTimeoutMs; + const startTime = Date.now(); + + logPaymentEvent(userId, purchaseId, 'polling'); + + const checkPayment = async (): Promise => { + try { + // Check if we've timed out + if (Date.now() - startTime > timeout) { + logPaymentEvent(userId, purchaseId, 'expired'); + await bot.sendMessage(chatId, messages.buy.invoiceExpired, { + parse_mode: 'Markdown', + reply_markup: getMainMenuKeyboard(), + }); + await stateManager.clearUserStateData(userId); + return; + } + + const status = await apiClient.getTicketStatus(purchaseId); + + if (!status) { + // Purchase not found, stop polling + logger.warn('Purchase not found during polling', { purchaseId }); + return; + } + + if (status.purchase.invoice_status === 'paid') { + logPaymentEvent(userId, purchaseId, 'confirmed', { + tickets: status.tickets.length, + }); + + // Payment received! + const ticketNumbers = status.tickets + .map((t) => `#${t.serial_number.toString().padStart(4, '0')}`) + .join('\n'); + + await bot.sendMessage( + chatId, + messages.buy.paymentReceived(ticketNumbers, formatDate(status.cycle.scheduled_at)), + { + parse_mode: 'Markdown', + reply_markup: getViewTicketKeyboard( + purchaseId, + config.frontend.baseUrl + '/tickets/' + purchaseId + ), + } + ); + + await stateManager.clearUserStateData(userId); + return; + } + + if (status.purchase.invoice_status === 'expired') { + logPaymentEvent(userId, purchaseId, 'expired'); + await bot.sendMessage(chatId, messages.buy.invoiceExpiredShort, { + parse_mode: 'Markdown', + reply_markup: getMainMenuKeyboard(), + }); + await stateManager.clearUserStateData(userId); + return; + } + + // Still pending, continue polling + setTimeout(checkPayment, pollInterval); + } catch (error) { + logger.error('Error polling payment status', { error, purchaseId }); + // Continue polling despite errors + setTimeout(checkPayment, pollInterval); + } + }; + + // Start polling after initial delay + setTimeout(checkPayment, pollInterval); +} + +export default { + handleBuyCommand, + handleTicketAmountSelection, + handleCustomTicketAmount, + handlePurchaseConfirmation, +}; diff --git a/telegram_bot/src/handlers/groups.ts b/telegram_bot/src/handlers/groups.ts new file mode 100644 index 0000000..05f2baa --- /dev/null +++ b/telegram_bot/src/handlers/groups.ts @@ -0,0 +1,341 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { groupStateManager } from '../services/groupState'; +import { logger, logUserAction } from '../services/logger'; +import { messages } from '../messages'; + +/** + * Check if a user is an admin in a group + */ +async function isGroupAdmin( + bot: TelegramBot, + chatId: number, + userId: number +): Promise { + try { + const member = await bot.getChatMember(chatId, userId); + return ['creator', 'administrator'].includes(member.status); + } catch (error) { + logger.error('Failed to check admin status', { error, chatId, userId }); + return false; + } +} + +/** + * Handle bot being added to a group + */ +export async function handleBotAddedToGroup( + bot: TelegramBot, + msg: TelegramBot.Message +): Promise { + const chatId = msg.chat.id; + const chatTitle = msg.chat.title || 'Unknown Group'; + const addedBy = msg.from?.id || 0; + + logger.info('Bot added to group', { chatId, chatTitle, addedBy }); + + try { + const settings = await groupStateManager.registerGroup(chatId, chatTitle, addedBy); + + await bot.sendMessage(chatId, messages.groups.welcome(chatTitle), { + parse_mode: 'Markdown', + }); + } catch (error) { + logger.error('Failed to register group', { error, chatId }); + } +} + +/** + * Handle bot being removed from a group + */ +export async function handleBotRemovedFromGroup( + bot: TelegramBot, + msg: TelegramBot.Message +): Promise { + const chatId = msg.chat.id; + logger.info('Bot removed from group', { chatId }); + + try { + await groupStateManager.removeGroup(chatId); + } catch (error) { + logger.error('Failed to remove group', { error, chatId }); + } +} + +/** + * Handle /settings command (group admin only) + */ +export async function handleGroupSettings( + bot: TelegramBot, + msg: TelegramBot.Message +): Promise { + const chatId = msg.chat.id; + const userId = msg.from?.id; + + // Only works in groups + if (msg.chat.type === 'private') { + await bot.sendMessage(chatId, messages.groups.privateChat); + return; + } + + if (!userId) return; + + // Check if user is admin + const isAdmin = await isGroupAdmin(bot, chatId, userId); + if (!isAdmin) { + await bot.sendMessage(chatId, messages.groups.adminOnly); + return; + } + + logUserAction(userId, 'Viewed group settings', { groupId: chatId }); + + try { + const settings = await groupStateManager.getGroup(chatId); + + if (!settings) { + // Register group if not found + await groupStateManager.registerGroup( + chatId, + msg.chat.title || 'Group', + userId + ); + } + + const currentSettings = settings || await groupStateManager.getGroup(chatId); + if (!currentSettings) { + await bot.sendMessage(chatId, messages.errors.generic); + return; + } + + await bot.sendMessage( + chatId, + messages.groups.settingsOverview(currentSettings), + { + parse_mode: 'Markdown', + reply_markup: getGroupSettingsKeyboard(currentSettings), + } + ); + } catch (error) { + logger.error('Error in handleGroupSettings', { error, chatId }); + await bot.sendMessage(chatId, messages.errors.generic); + } +} + +/** + * Handle group settings toggle callback + */ +export async function handleGroupSettingsCallback( + bot: TelegramBot, + query: TelegramBot.CallbackQuery, + action: string +): Promise { + const chatId = query.message?.chat.id; + const userId = query.from.id; + const messageId = query.message?.message_id; + + if (!chatId || !messageId) return; + + // Check if user is admin + const isAdmin = await isGroupAdmin(bot, chatId, userId); + if (!isAdmin) { + await bot.answerCallbackQuery(query.id, { + text: messages.groups.adminOnly, + show_alert: true, + }); + return; + } + + try { + let setting: 'enabled' | 'drawAnnouncements' | 'reminders' | 'ticketPurchaseAllowed'; + + switch (action) { + case 'toggle_enabled': + setting = 'enabled'; + break; + case 'toggle_announcements': + setting = 'drawAnnouncements'; + break; + case 'toggle_reminders': + setting = 'reminders'; + break; + case 'toggle_purchases': + setting = 'ticketPurchaseAllowed'; + break; + default: + await bot.answerCallbackQuery(query.id); + return; + } + + const currentSettings = await groupStateManager.getGroup(chatId); + if (!currentSettings) { + await bot.answerCallbackQuery(query.id, { text: 'Group not found' }); + return; + } + + const newValue = !currentSettings[setting]; + const updatedSettings = await groupStateManager.updateSetting(chatId, setting, newValue); + + if (!updatedSettings) { + await bot.answerCallbackQuery(query.id, { text: 'Failed to update' }); + return; + } + + logUserAction(userId, 'Updated group setting', { + groupId: chatId, + setting, + newValue, + }); + + await bot.answerCallbackQuery(query.id, { + text: `${setting} ${newValue ? 'enabled' : 'disabled'}`, + }); + + // Update the message with new settings + await bot.editMessageText( + messages.groups.settingsOverview(updatedSettings), + { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + reply_markup: getGroupSettingsKeyboard(updatedSettings), + } + ); + } catch (error) { + logger.error('Error in handleGroupSettingsCallback', { error, chatId, action }); + await bot.answerCallbackQuery(query.id, { text: 'Error updating settings' }); + } +} + +/** + * Generate inline keyboard for group settings + */ +function getGroupSettingsKeyboard(settings: { + enabled: boolean; + drawAnnouncements: boolean; + reminders: boolean; + ticketPurchaseAllowed: boolean; +}): TelegramBot.InlineKeyboardMarkup { + const onOff = (val: boolean) => val ? 'โœ…' : 'โŒ'; + + return { + inline_keyboard: [ + [{ + text: `${onOff(settings.enabled)} Bot Enabled`, + callback_data: 'group_toggle_enabled', + }], + [{ + text: `${onOff(settings.drawAnnouncements)} Draw Announcements`, + callback_data: 'group_toggle_announcements', + }], + [{ + text: `${onOff(settings.reminders)} Draw Reminders`, + callback_data: 'group_toggle_reminders', + }], + [{ + text: `${onOff(settings.ticketPurchaseAllowed)} Allow Ticket Purchases`, + callback_data: 'group_toggle_purchases', + }], + [{ + text: '๐Ÿ”„ Refresh', + callback_data: 'group_refresh', + }], + ], + }; +} + +/** + * Handle refresh callback + */ +export async function handleGroupRefresh( + bot: TelegramBot, + query: TelegramBot.CallbackQuery +): Promise { + const chatId = query.message?.chat.id; + const messageId = query.message?.message_id; + + if (!chatId || !messageId) return; + + await bot.answerCallbackQuery(query.id, { text: 'Refreshed!' }); + + const settings = await groupStateManager.getGroup(chatId); + if (!settings) return; + + await bot.editMessageText( + messages.groups.settingsOverview(settings), + { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + reply_markup: getGroupSettingsKeyboard(settings), + } + ); +} + +/** + * Send draw announcement to all enabled groups + */ +export async function broadcastDrawAnnouncement( + bot: TelegramBot, + announcement: string +): Promise { + const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements'); + let sent = 0; + + for (const group of groups) { + try { + await bot.sendMessage(group.groupId, announcement, { parse_mode: 'Markdown' }); + sent++; + } catch (error) { + logger.error('Failed to send announcement to group', { + groupId: group.groupId, + error, + }); + // If bot was removed from group, clean up + if ((error as any)?.response?.statusCode === 403) { + await groupStateManager.removeGroup(group.groupId); + } + } + } + + logger.info('Broadcast draw announcement', { sent, total: groups.length }); + return sent; +} + +/** + * Send draw reminder to all enabled groups + */ +export async function broadcastDrawReminder( + bot: TelegramBot, + reminder: string +): Promise { + const groups = await groupStateManager.getGroupsWithFeature('reminders'); + let sent = 0; + + for (const group of groups) { + try { + await bot.sendMessage(group.groupId, reminder, { parse_mode: 'Markdown' }); + sent++; + } catch (error) { + logger.error('Failed to send reminder to group', { + groupId: group.groupId, + error, + }); + if ((error as any)?.response?.statusCode === 403) { + await groupStateManager.removeGroup(group.groupId); + } + } + } + + logger.info('Broadcast draw reminder', { sent, total: groups.length }); + return sent; +} + +export default { + handleBotAddedToGroup, + handleBotRemovedFromGroup, + handleGroupSettings, + handleGroupSettingsCallback, + handleGroupRefresh, + broadcastDrawAnnouncement, + broadcastDrawReminder, +}; + diff --git a/telegram_bot/src/handlers/help.ts b/telegram_bot/src/handlers/help.ts new file mode 100644 index 0000000..26554a0 --- /dev/null +++ b/telegram_bot/src/handlers/help.ts @@ -0,0 +1,26 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { logUserAction } from '../services/logger'; +import { getMainMenuKeyboard } from '../utils/keyboards'; +import { messages } from '../messages'; + +/** + * Handle /help command + */ +export async function handleHelpCommand( + bot: TelegramBot, + msg: TelegramBot.Message +): Promise { + const chatId = msg.chat.id; + const userId = msg.from?.id; + + if (userId) { + logUserAction(userId, 'Viewed help'); + } + + await bot.sendMessage(chatId, messages.help.message, { + parse_mode: 'Markdown', + reply_markup: getMainMenuKeyboard(), + }); +} + +export default handleHelpCommand; diff --git a/telegram_bot/src/handlers/index.ts b/telegram_bot/src/handlers/index.ts new file mode 100644 index 0000000..8ca20cd --- /dev/null +++ b/telegram_bot/src/handlers/index.ts @@ -0,0 +1,33 @@ +export { handleStart } from './start'; +export { + handleAddressCommand, + handleLightningAddressInput, +} from './address'; +export { + handleBuyCommand, + handleTicketAmountSelection, + handleCustomTicketAmount, + handlePurchaseConfirmation, +} from './buy'; +export { + handleTicketsCommand, + handleViewTicket, + handleStatusCheck, +} from './tickets'; +export { handleWinsCommand } from './wins'; +export { handleHelpCommand } from './help'; +export { + handleMenuCommand, + handleCancel, + handleMenuCallback, +} from './menu'; +export { + handleBotAddedToGroup, + handleBotRemovedFromGroup, + handleGroupSettings, + handleGroupSettingsCallback, + handleGroupRefresh, + broadcastDrawAnnouncement, + broadcastDrawReminder, +} from './groups'; + diff --git a/telegram_bot/src/handlers/menu.ts b/telegram_bot/src/handlers/menu.ts new file mode 100644 index 0000000..34c6355 --- /dev/null +++ b/telegram_bot/src/handlers/menu.ts @@ -0,0 +1,92 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { stateManager } from '../services/state'; +import { logUserAction } from '../services/logger'; +import { getMainMenuKeyboard } from '../utils/keyboards'; +import { messages } from '../messages'; + +/** + * Handle /menu command + */ +export async function handleMenuCommand( + bot: TelegramBot, + msg: TelegramBot.Message +): Promise { + const chatId = msg.chat.id; + const userId = msg.from?.id; + + if (userId) { + logUserAction(userId, 'Opened menu'); + + // Reset user state to idle + const user = await stateManager.getUser(userId); + if (user && user.lightningAddress) { + await stateManager.updateUserState(userId, 'idle'); + } + } + + await bot.sendMessage(chatId, messages.menu.header, { + parse_mode: 'Markdown', + reply_markup: getMainMenuKeyboard(), + }); +} + +/** + * Handle cancel callback + */ +export async function handleCancel( + bot: TelegramBot, + query: TelegramBot.CallbackQuery +): Promise { + const chatId = query.message?.chat.id; + const userId = query.from.id; + const messageId = query.message?.message_id; + + if (!chatId) return; + + await bot.answerCallbackQuery(query.id, { text: 'Cancelled' }); + + // Clear user state + await stateManager.clearUserStateData(userId); + + // Update message + if (messageId) { + await bot.editMessageText(messages.menu.cancelled, { + chat_id: chatId, + message_id: messageId, + }); + } + + // Show menu + await bot.sendMessage(chatId, messages.menu.whatToDo, { + reply_markup: getMainMenuKeyboard(), + }); +} + +/** + * Handle menu callback (back to menu) + */ +export async function handleMenuCallback( + bot: TelegramBot, + query: TelegramBot.CallbackQuery +): Promise { + const chatId = query.message?.chat.id; + const userId = query.from.id; + + if (!chatId) return; + + await bot.answerCallbackQuery(query.id); + + // Clear user state + await stateManager.clearUserStateData(userId); + + await bot.sendMessage(chatId, messages.menu.header, { + parse_mode: 'Markdown', + reply_markup: getMainMenuKeyboard(), + }); +} + +export default { + handleMenuCommand, + handleCancel, + handleMenuCallback, +}; diff --git a/telegram_bot/src/handlers/start.ts b/telegram_bot/src/handlers/start.ts new file mode 100644 index 0000000..f17df25 --- /dev/null +++ b/telegram_bot/src/handlers/start.ts @@ -0,0 +1,68 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { stateManager } from '../services/state'; +import { logger, logUserAction } from '../services/logger'; +import { getMainMenuKeyboard, getCancelKeyboard } from '../utils/keyboards'; +import { messages } from '../messages'; + +/** + * Handle /start command + */ +export async function handleStart(bot: TelegramBot, msg: TelegramBot.Message): Promise { + const chatId = msg.chat.id; + const userId = msg.from?.id; + + if (!userId) { + await bot.sendMessage(chatId, messages.errors.userNotIdentified); + return; + } + + logUserAction(userId, 'Started bot', { + username: msg.from?.username, + firstName: msg.from?.first_name, + }); + + try { + // Check if user exists + let user = await stateManager.getUser(userId); + + if (!user) { + // Create new user + user = await stateManager.createUser( + userId, + msg.from?.username, + msg.from?.first_name, + msg.from?.last_name + ); + } + + // Welcome message + await bot.sendMessage(chatId, messages.start.welcome, { parse_mode: 'Markdown' }); + + // Check if lightning address is set + if (!user.lightningAddress) { + await bot.sendMessage(chatId, messages.start.needAddress, { + parse_mode: 'Markdown', + reply_markup: getCancelKeyboard(), + }); + + await stateManager.updateUserState(userId, 'awaiting_lightning_address'); + } else { + // Show main menu + await bot.sendMessage( + chatId, + messages.start.addressSet(user.lightningAddress), + { + parse_mode: 'Markdown', + reply_markup: getMainMenuKeyboard(), + } + ); + + await stateManager.updateUserState(userId, 'idle'); + } + } catch (error) { + logger.error('Error in handleStart', { error, userId }); + await bot.sendMessage(chatId, messages.errors.startAgain); + } +} + +export default handleStart; diff --git a/telegram_bot/src/handlers/tickets.ts b/telegram_bot/src/handlers/tickets.ts new file mode 100644 index 0000000..c7107ec --- /dev/null +++ b/telegram_bot/src/handlers/tickets.ts @@ -0,0 +1,259 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { stateManager } from '../services/state'; +import { apiClient } from '../services/api'; +import { logger, logUserAction } from '../services/logger'; +import config from '../config'; +import { getMainMenuKeyboard, getViewTicketKeyboard } from '../utils/keyboards'; +import { formatSats, formatDate } from '../utils/format'; +import { messages } from '../messages'; + +/** + * Handle /tickets command or "My Tickets" button + */ +export async function handleTicketsCommand( + bot: TelegramBot, + msg: TelegramBot.Message +): Promise { + const chatId = msg.chat.id; + const userId = msg.from?.id; + + if (!userId) { + await bot.sendMessage(chatId, messages.errors.userNotIdentified); + return; + } + + logUserAction(userId, 'Viewed tickets'); + + try { + const user = await stateManager.getUser(userId); + + if (!user) { + await bot.sendMessage(chatId, messages.errors.startFirst); + return; + } + + // Get user's purchase IDs from state + const purchaseIds = await stateManager.getUserPurchaseIds(userId, 10); + + if (purchaseIds.length === 0) { + await bot.sendMessage(chatId, messages.tickets.empty, { + parse_mode: 'Markdown', + reply_markup: getMainMenuKeyboard(), + }); + return; + } + + // Fetch status for each purchase + const purchases: Array<{ + id: string; + ticketCount: number; + scheduledAt: string; + invoiceStatus: string; + isWinner: boolean; + hasDrawn: boolean; + }> = []; + + for (const purchaseId of purchaseIds) { + try { + const status = await apiClient.getTicketStatus(purchaseId); + if (status) { + purchases.push({ + id: status.purchase.id, + ticketCount: status.purchase.number_of_tickets, + scheduledAt: status.cycle.scheduled_at, + invoiceStatus: status.purchase.invoice_status, + isWinner: status.result.is_winner, + hasDrawn: status.result.has_drawn, + }); + } + } catch (error) { + // Skip failed fetches + logger.debug('Failed to fetch purchase', { purchaseId }); + } + } + + if (purchases.length === 0) { + await bot.sendMessage(chatId, messages.tickets.notFound, { + parse_mode: 'Markdown', + reply_markup: getMainMenuKeyboard(), + }); + return; + } + + // Format purchases list + let message = messages.tickets.header; + + for (let i = 0; i < purchases.length; i++) { + const p = purchases[i]; + const drawDate = new Date(p.scheduledAt); + + let statusEmoji: string; + let statusText: string; + + if (p.invoiceStatus === 'pending') { + statusEmoji = 'โณ'; + statusText = messages.tickets.statusPending; + } else if (p.invoiceStatus === 'expired') { + statusEmoji = 'โŒ'; + statusText = messages.tickets.statusExpired; + } else if (!p.hasDrawn) { + statusEmoji = '๐ŸŽŸ'; + statusText = messages.tickets.statusActive; + } else if (p.isWinner) { + statusEmoji = '๐Ÿ†'; + statusText = messages.tickets.statusWon; + } else { + statusEmoji = '๐Ÿ˜”'; + statusText = messages.tickets.statusLost; + } + + message += `${i + 1}. ${statusEmoji} ${p.ticketCount} ticket${p.ticketCount > 1 ? 's' : ''} โ€“ ${formatDate(drawDate)} โ€“ ${statusText}\n`; + } + + message += messages.tickets.tapForDetails; + + // Create inline buttons for each purchase + const inlineKeyboard = purchases.map((p, i) => [{ + text: `${i + 1}. View Ticket #${p.id.substring(0, 8)}...`, + callback_data: `view_ticket_${p.id}`, + }]); + + await bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: inlineKeyboard }, + }); + } catch (error) { + logger.error('Error in handleTicketsCommand', { error, userId }); + await bot.sendMessage(chatId, messages.errors.fetchTicketsFailed, { + reply_markup: getMainMenuKeyboard(), + }); + } +} + +/** + * Handle viewing a specific ticket + */ +export async function handleViewTicket( + bot: TelegramBot, + query: TelegramBot.CallbackQuery, + purchaseId: string +): Promise { + const chatId = query.message?.chat.id; + + if (!chatId) return; + + await bot.answerCallbackQuery(query.id); + + try { + const status = await apiClient.getTicketStatus(purchaseId); + + if (!status) { + await bot.sendMessage(chatId, messages.errors.ticketNotFound); + return; + } + + const drawDate = new Date(status.cycle.scheduled_at); + const ticketNumbers = status.tickets + .map((t) => { + const winnerMark = t.is_winning_ticket ? ' ๐Ÿ†' : ''; + return `#${t.serial_number.toString().padStart(4, '0')}${winnerMark}`; + }) + .join(', '); + + let statusSection: string; + + if (status.purchase.invoice_status === 'pending') { + statusSection = messages.tickets.detailAwaitingPayment; + } else if (status.purchase.invoice_status === 'expired') { + statusSection = messages.tickets.detailExpired; + } else if (!status.result.has_drawn) { + statusSection = messages.tickets.detailActive; + } else if (status.result.is_winner) { + const payoutStatus = status.result.payout?.status === 'paid' ? 'Paid โœ…' : 'Pending'; + statusSection = messages.tickets.detailWinner( + formatSats(status.result.payout?.amount_sats || 0), + payoutStatus + ); + } else { + statusSection = messages.tickets.detailLost( + status.cycle.winning_ticket_id?.substring(0, 8) || 'N/A' + ); + } + + const message = messages.tickets.detailFormat( + status.purchase.id.substring(0, 8), + status.purchase.number_of_tickets, + ticketNumbers, + formatSats(status.purchase.amount_sats), + formatDate(drawDate), + statusSection + ); + + await bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: getViewTicketKeyboard( + purchaseId, + config.frontend.baseUrl + '/tickets/' + purchaseId + ), + }); + } catch (error) { + logger.error('Error in handleViewTicket', { error, purchaseId }); + await bot.sendMessage(chatId, messages.errors.fetchTicketDetailsFailed); + } +} + +/** + * Handle status check callback + */ +export async function handleStatusCheck( + bot: TelegramBot, + query: TelegramBot.CallbackQuery, + purchaseId: string +): Promise { + const chatId = query.message?.chat.id; + + if (!chatId) return; + + await bot.answerCallbackQuery(query.id, { text: 'Checking status...' }); + + try { + const status = await apiClient.getTicketStatus(purchaseId); + + if (!status) { + await bot.sendMessage(chatId, messages.errors.ticketNotFound); + return; + } + + let statusMessage: string; + + if (status.purchase.invoice_status === 'pending') { + statusMessage = messages.tickets.checkStatusPending; + } else if (status.purchase.invoice_status === 'paid' && status.purchase.ticket_issue_status === 'issued') { + if (!status.result.has_drawn) { + statusMessage = messages.tickets.checkStatusConfirmed; + } else if (status.result.is_winner) { + statusMessage = messages.tickets.checkStatusWon; + } else { + statusMessage = messages.tickets.checkStatusLost; + } + } else if (status.purchase.invoice_status === 'expired') { + statusMessage = messages.tickets.checkStatusExpired; + } else { + statusMessage = messages.tickets.checkStatusProcessing; + } + + await bot.answerCallbackQuery(query.id, { text: statusMessage, show_alert: true }); + } catch (error) { + logger.error('Error in handleStatusCheck', { error, purchaseId }); + await bot.answerCallbackQuery(query.id, { + text: messages.errors.checkStatusFailed, + show_alert: true, + }); + } +} + +export default { + handleTicketsCommand, + handleViewTicket, + handleStatusCheck, +}; diff --git a/telegram_bot/src/handlers/wins.ts b/telegram_bot/src/handlers/wins.ts new file mode 100644 index 0000000..75a28e9 --- /dev/null +++ b/telegram_bot/src/handlers/wins.ts @@ -0,0 +1,114 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { stateManager } from '../services/state'; +import { apiClient } from '../services/api'; +import { logger, logUserAction } from '../services/logger'; +import { getMainMenuKeyboard } from '../utils/keyboards'; +import { formatSats, formatDate } from '../utils/format'; +import { messages } from '../messages'; + +/** + * Handle /wins command or "My Wins" button + */ +export async function handleWinsCommand( + bot: TelegramBot, + msg: TelegramBot.Message +): Promise { + const chatId = msg.chat.id; + const userId = msg.from?.id; + + if (!userId) { + await bot.sendMessage(chatId, messages.errors.userNotIdentified); + return; + } + + logUserAction(userId, 'Viewed wins'); + + try { + const user = await stateManager.getUser(userId); + + if (!user) { + await bot.sendMessage(chatId, messages.errors.startFirst); + return; + } + + // Get user's purchase IDs and check for wins + const purchaseIds = await stateManager.getUserPurchaseIds(userId, 50); + + if (purchaseIds.length === 0) { + await bot.sendMessage(chatId, messages.wins.empty, { + parse_mode: 'Markdown', + reply_markup: getMainMenuKeyboard(), + }); + return; + } + + // Check each purchase for wins + const wins: Array<{ + purchaseId: string; + ticketId: string; + serialNumber: number; + amountSats: number; + status: string; + scheduledAt: string; + }> = []; + + for (const purchaseId of purchaseIds) { + try { + const status = await apiClient.getTicketStatus(purchaseId); + if (status && status.result.is_winner && status.result.payout) { + const winningTicket = status.tickets.find(t => t.is_winning_ticket); + wins.push({ + purchaseId: status.purchase.id, + ticketId: winningTicket?.id || '', + serialNumber: winningTicket?.serial_number || 0, + amountSats: status.result.payout.amount_sats, + status: status.result.payout.status, + scheduledAt: status.cycle.scheduled_at, + }); + } + } catch (error) { + // Skip failed fetches + logger.debug('Failed to fetch purchase for wins', { purchaseId }); + } + } + + if (wins.length === 0) { + await bot.sendMessage(chatId, messages.wins.noWinsYet, { + parse_mode: 'Markdown', + reply_markup: getMainMenuKeyboard(), + }); + return; + } + + // Calculate totals + const totalWinnings = wins.reduce((sum, w) => sum + w.amountSats, 0); + const paidWinnings = wins.filter(w => w.status === 'paid').reduce((sum, w) => sum + w.amountSats, 0); + + let message = messages.wins.header(formatSats(totalWinnings), formatSats(paidWinnings)); + + for (const win of wins) { + const statusEmoji = win.status === 'paid' ? 'โœ…' : 'โณ'; + message += `โ€ข ${formatSats(win.amountSats)} sats โ€” ${formatDate(win.scheduledAt)} โ€” ${statusEmoji} ${win.status}\n`; + } + + // Create buttons for viewing wins + const inlineKeyboard = wins.slice(0, 5).map((w) => [{ + text: `View Ticket #${w.serialNumber.toString().padStart(4, '0')}`, + callback_data: `view_ticket_${w.purchaseId}`, + }]); + + await bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: inlineKeyboard }, + }); + } catch (error) { + logger.error('Error in handleWinsCommand', { error, userId }); + await bot.sendMessage(chatId, messages.errors.fetchWinsFailed, { + reply_markup: getMainMenuKeyboard(), + }); + } +} + +export default { + handleWinsCommand, +}; diff --git a/telegram_bot/src/index.ts b/telegram_bot/src/index.ts new file mode 100644 index 0000000..83be041 --- /dev/null +++ b/telegram_bot/src/index.ts @@ -0,0 +1,448 @@ +import TelegramBot from 'node-telegram-bot-api'; +import config from './config'; +import { stateManager } from './services/state'; +import { groupStateManager } from './services/groupState'; +import { apiClient } from './services/api'; +import { logger, logUserAction } from './services/logger'; +import { + handleStart, + handleAddressCommand, + handleLightningAddressInput, + handleBuyCommand, + handleTicketAmountSelection, + handleCustomTicketAmount, + handlePurchaseConfirmation, + handleTicketsCommand, + handleViewTicket, + handleStatusCheck, + handleWinsCommand, + handleHelpCommand, + handleMenuCommand, + handleCancel, + handleMenuCallback, + handleBotAddedToGroup, + handleBotRemovedFromGroup, + handleGroupSettings, + handleGroupSettingsCallback, + handleGroupRefresh, +} from './handlers'; +import { getMainMenuKeyboard } from './utils/keyboards'; +import { messages } from './messages'; +import { formatSats, formatDate, formatTimeUntil } from './utils/format'; + +// Create bot instance +const bot = new TelegramBot(config.telegram.botToken, { polling: true }); + +// Track processed message IDs to prevent duplicate handling +const processedMessages = new Set(); +const MESSAGE_CACHE_TTL = 60000; // 1 minute + +function shouldProcessMessage(messageId: number): boolean { + if (processedMessages.has(messageId)) { + return false; + } + processedMessages.add(messageId); + // Clean up old entries + setTimeout(() => processedMessages.delete(messageId), MESSAGE_CACHE_TTL); + return true; +} + +/** + * Check if message is from a group + */ +function isGroupChat(msg: TelegramBot.Message): boolean { + return msg.chat.type === 'group' || msg.chat.type === 'supergroup'; +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// GROUP EVENTS +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +// Handle bot being added/removed from groups +bot.on('message', async (msg) => { + // Handle new chat members (bot added to group) + if (msg.new_chat_members) { + const botInfo = await bot.getMe(); + const botAdded = msg.new_chat_members.some(m => m.id === botInfo.id); + if (botAdded) { + await handleBotAddedToGroup(bot, msg); + } + } + + // Handle chat member left (bot removed from group) + if (msg.left_chat_member) { + const botInfo = await bot.getMe(); + if (msg.left_chat_member.id === botInfo.id) { + await handleBotRemovedFromGroup(bot, msg); + } + } +}); + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// COMMANDS +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +// Handle /start command +bot.onText(/\/start/, async (msg) => { + if (!shouldProcessMessage(msg.message_id)) return; + + // In groups, just show a welcome message + if (isGroupChat(msg)) { + await bot.sendMessage( + msg.chat.id, + `โšก *Lightning Jackpot Bot*\n\nTo buy tickets and manage your account, message me directly!\n\nUse /jackpot to see current jackpot info.\nAdmins: Use /settings to configure the bot.`, + { parse_mode: 'Markdown' } + ); + return; + } + + await handleStart(bot, msg); +}); + +// Handle /buy command +bot.onText(/\/buy/, async (msg) => { + if (!shouldProcessMessage(msg.message_id)) return; + + // Check if in group + if (isGroupChat(msg)) { + const settings = await groupStateManager.getGroup(msg.chat.id); + if (settings && !settings.ticketPurchaseAllowed) { + await bot.sendMessage(msg.chat.id, messages.groups.purchasesDisabled, { + parse_mode: 'Markdown', + }); + return; + } + } + + await handleBuyCommand(bot, msg); +}); + +// Handle /tickets command +bot.onText(/\/tickets/, async (msg) => { + if (!shouldProcessMessage(msg.message_id)) return; + + // Only in private chat + if (isGroupChat(msg)) { + await bot.sendMessage(msg.chat.id, '๐Ÿงพ To view your tickets, message me directly!'); + return; + } + + await handleTicketsCommand(bot, msg); +}); + +// Handle /wins command +bot.onText(/\/wins/, async (msg) => { + if (!shouldProcessMessage(msg.message_id)) return; + + // Only in private chat + if (isGroupChat(msg)) { + await bot.sendMessage(msg.chat.id, '๐Ÿ† To view your wins, message me directly!'); + return; + } + + await handleWinsCommand(bot, msg); +}); + +// Handle /address command +bot.onText(/\/address/, async (msg) => { + if (!shouldProcessMessage(msg.message_id)) return; + + // Only in private chat + if (isGroupChat(msg)) { + await bot.sendMessage(msg.chat.id, 'โšก To update your Lightning Address, message me directly!'); + return; + } + + await handleAddressCommand(bot, msg); +}); + +// Handle /menu command +bot.onText(/\/menu/, async (msg) => { + if (!shouldProcessMessage(msg.message_id)) return; + + // Only in private chat + if (isGroupChat(msg)) { + await bot.sendMessage(msg.chat.id, '๐Ÿ“ฑ To access the full menu, message me directly!'); + return; + } + + await handleMenuCommand(bot, msg); +}); + +// Handle /help command +bot.onText(/\/help/, async (msg) => { + if (!shouldProcessMessage(msg.message_id)) return; + await handleHelpCommand(bot, msg); +}); + +// Handle /jackpot command (works in groups and DMs) +bot.onText(/\/jackpot/, async (msg) => { + if (!shouldProcessMessage(msg.message_id)) return; + + try { + const jackpot = await apiClient.getNextJackpot(); + + if (!jackpot) { + await bot.sendMessage(msg.chat.id, messages.buy.noActiveJackpot, { + parse_mode: 'Markdown', + }); + return; + } + + const drawTime = new Date(jackpot.cycle.scheduled_at); + const message = `๐ŸŽฐ *Current Jackpot* + +๐Ÿ’ฐ *Prize Pool:* ${formatSats(jackpot.cycle.pot_total_sats)} sats +๐ŸŽŸ *Ticket Price:* ${formatSats(jackpot.lottery.ticket_price_sats)} sats +โฐ *Draw at:* ${formatDate(drawTime)} +โณ *Time left:* ${formatTimeUntil(drawTime)} + +Use /buy to get your tickets! ๐Ÿ€`; + + await bot.sendMessage(msg.chat.id, message, { parse_mode: 'Markdown' }); + } catch (error) { + logger.error('Error in /jackpot command', { error }); + await bot.sendMessage(msg.chat.id, messages.errors.systemUnavailable); + } +}); + +// Handle /settings command (groups only, admin only) +bot.onText(/\/settings/, async (msg) => { + if (!shouldProcessMessage(msg.message_id)) return; + await handleGroupSettings(bot, msg); +}); + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// TEXT MESSAGES +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +// Handle keyboard button presses (text messages) +bot.on('message', async (msg) => { + if (!msg.text || msg.text.startsWith('/')) return; + if (!shouldProcessMessage(msg.message_id)) return; + + // Ignore group messages for button handling + if (isGroupChat(msg)) return; + + const text = msg.text.trim(); + const userId = msg.from?.id; + + if (!userId) return; + + // Handle menu button presses + switch (text) { + case '๐ŸŽŸ Buy Tickets': + await handleBuyCommand(bot, msg); + return; + case '๐Ÿงพ My Tickets': + await handleTicketsCommand(bot, msg); + return; + case '๐Ÿ† My Wins': + await handleWinsCommand(bot, msg); + return; + case 'โšก Lightning Address': + await handleAddressCommand(bot, msg); + return; + case 'โ„น๏ธ Help': + await handleHelpCommand(bot, msg); + return; + } + + // Handle state-dependent text input + try { + const user = await stateManager.getUser(userId); + + if (!user) { + // Unknown user, prompt to start + await bot.sendMessage(msg.chat.id, messages.start.welcomeUnknown); + return; + } + + // Handle lightning address input + if (user.state === 'awaiting_lightning_address' || user.state === 'updating_address') { + const handled = await handleLightningAddressInput(bot, msg); + if (handled) return; + } + + // Handle custom ticket amount input + if (user.state === 'awaiting_ticket_amount') { + const handled = await handleCustomTicketAmount(bot, msg); + if (handled) return; + } + + // Unhandled message - show menu prompt + if (user.state === 'idle') { + await bot.sendMessage(msg.chat.id, messages.menu.didNotUnderstand, { + reply_markup: getMainMenuKeyboard(), + }); + } + } catch (error) { + logger.error('Error handling message', { error, userId, text }); + } +}); + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// CALLBACK QUERIES +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +// Handle callback queries (inline button presses) +bot.on('callback_query', async (query) => { + const data = query.data; + + if (!data) { + await bot.answerCallbackQuery(query.id); + return; + } + + logUserAction(query.from.id, 'Callback', { data }); + + try { + // Handle group settings toggles + if (data.startsWith('group_toggle_')) { + const action = data.replace('group_', ''); + await handleGroupSettingsCallback(bot, query, action); + return; + } + + // Handle group refresh + if (data === 'group_refresh') { + await handleGroupRefresh(bot, query); + return; + } + + // Handle buy amount selection + if (data.startsWith('buy_')) { + const amountStr = data.replace('buy_', ''); + if (amountStr === 'custom') { + await handleTicketAmountSelection(bot, query, 'custom'); + } else { + const amount = parseInt(amountStr, 10); + if (!isNaN(amount)) { + await handleTicketAmountSelection(bot, query, amount); + } + } + return; + } + + // Handle purchase confirmation + if (data === 'confirm_purchase') { + await handlePurchaseConfirmation(bot, query); + return; + } + + // Handle cancel + if (data === 'cancel') { + await handleCancel(bot, query); + return; + } + + // Handle menu + if (data === 'menu') { + await handleMenuCallback(bot, query); + return; + } + + // Handle view ticket + if (data.startsWith('view_ticket_')) { + const purchaseId = data.replace('view_ticket_', ''); + await handleViewTicket(bot, query, purchaseId); + return; + } + + // Handle status check + if (data.startsWith('status_')) { + const purchaseId = data.replace('status_', ''); + await handleStatusCheck(bot, query, purchaseId); + return; + } + + // Handle ticket pagination + if (data.startsWith('tickets_page_')) { + // TODO: Implement pagination + await bot.answerCallbackQuery(query.id, { text: 'Pagination coming soon!' }); + return; + } + + // Unknown callback + await bot.answerCallbackQuery(query.id); + } catch (error) { + logger.error('Error handling callback query', { error, data }); + await bot.answerCallbackQuery(query.id, { + text: messages.errors.generic, + show_alert: true, + }); + } +}); + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// ERROR HANDLING & LIFECYCLE +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +// Handle polling errors +bot.on('polling_error', (error) => { + logger.error('Polling error', { error: error.message }); +}); + +// Graceful shutdown +async function shutdown(): Promise { + logger.info('Shutting down...'); + bot.stopPolling(); + await stateManager.close(); + await groupStateManager.close(); + logger.info('Shutdown complete'); + process.exit(0); +} + +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); + +// Start bot +async function start(): Promise { + try { + // Initialize state managers + await stateManager.init(); + await groupStateManager.init(config.redis.url); + + // Set bot commands for private chats + await bot.setMyCommands([ + { command: 'start', description: 'Start the bot' }, + { command: 'menu', description: 'Show main menu' }, + { command: 'buy', description: 'Buy lottery tickets' }, + { command: 'tickets', description: 'View your tickets' }, + { command: 'wins', description: 'View your past wins' }, + { command: 'address', description: 'Update Lightning Address' }, + { command: 'jackpot', description: 'View current jackpot info' }, + { command: 'help', description: 'Help & information' }, + ]); + + // Set bot commands for groups (different scope) + await bot.setMyCommands( + [ + { command: 'jackpot', description: 'View current jackpot info' }, + { command: 'settings', description: 'Group settings (admin only)' }, + { command: 'help', description: 'Help & information' }, + ], + { scope: { type: 'all_group_chats' } } + ); + + const botInfo = await bot.getMe(); + logger.info(`๐Ÿค– Bot started: @${botInfo.username}`, { + id: botInfo.id, + username: botInfo.username, + }); + + logger.info('โšก Lightning Jackpot Telegram Bot is running!'); + logger.info(`๐Ÿ“ก API URL: ${config.api.baseUrl}`); + logger.info(`๐ŸŒ Frontend URL: ${config.frontend.baseUrl}`); + logger.info('๐Ÿ‘ฅ Group support enabled'); + } catch (error) { + logger.error('Failed to start bot', { error }); + process.exit(1); + } +} + +start(); + +// Export bot instance and broadcast functions for external use +export { bot }; +export { broadcastDrawAnnouncement, broadcastDrawReminder } from './handlers'; diff --git a/telegram_bot/src/messages/index.ts b/telegram_bot/src/messages/index.ts new file mode 100644 index 0000000..bc61c19 --- /dev/null +++ b/telegram_bot/src/messages/index.ts @@ -0,0 +1,444 @@ +/** + * All Telegram bot messages centralized for easy management and future i18n support + */ + +export const messages = { + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // ERRORS + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + errors: { + userNotIdentified: 'โŒ Could not identify user.', + startFirst: 'โŒ Please start the bot first with /start', + generic: 'โŒ An error occurred. Please try again.', + startAgain: 'โŒ An error occurred. Please try again with /start', + systemUnavailable: 'โŒ The lottery system is temporarily unavailable. Please try again soon.', + invoiceCreationFailed: 'โŒ Failed to create invoice. Please try again.', + fetchTicketsFailed: 'โŒ Failed to fetch tickets. Please try again.', + fetchWinsFailed: 'โŒ Failed to fetch wins. Please try again.', + ticketNotFound: 'โŒ Ticket not found.', + fetchTicketDetailsFailed: 'โŒ Failed to fetch ticket details.', + checkStatusFailed: 'โŒ Failed to check status', + noPendingPurchase: 'โŒ No pending purchase. Please start again with /buy', + setAddressFirst: 'โŒ Please set your Lightning Address first.', + }, + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // START / ONBOARDING + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + start: { + welcome: `โšก๐ŸŽ‰ *Welcome to Lightning Jackpot!* ๐ŸŽ‰โšก + +You can buy Bitcoin Lightning lottery tickets, and if you win, your prize is paid instantly to your Lightning Address! + +๐ŸŽฏ *How it works:* +1๏ธโƒฃ Set your Lightning Address (where you'll receive winnings) +2๏ธโƒฃ Buy tickets with Lightning +3๏ธโƒฃ Wait for the draw +4๏ธโƒฃ If you win, prize is sent instantly!`, + + needAddress: `Before you can play, I need your Lightning Address to send any winnings. + +*Example:* \`yourname@getalby.com\` + +Please send your Lightning Address now:`, + + addressSet: (address: string) => + `โœ… Your payout address is set to: \`${address}\` + +Use the menu below to get started! Good luck! ๐Ÿ€`, + + welcomeUnknown: '๐Ÿ‘‹ Welcome! Please use /start to begin.', + }, + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // LIGHTNING ADDRESS + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + address: { + currentAddress: (address: string) => + `โšก *Your Current Payout Address:* +\`${address}\` + +Send me your new Lightning Address to update it:`, + + noAddressSet: `โšก You don't have a Lightning Address set yet. + +Send me your Lightning Address now: + +*Example:* \`yourname@getalby.com\``, + + invalidFormat: `โŒ That doesn't look like a valid Lightning Address. + +*Format:* \`username@domain.com\` +*Example:* \`satoshi@getalby.com\` + +Please try again:`, + + firstTimeSuccess: (address: string) => + `โœ… *Perfect!* I'll use \`${address}\` to send any winnings. + +You're all set! Use the menu below to buy tickets and check your results. Good luck! ๐Ÿ€`, + + updateSuccess: (address: string) => + `โœ… *Lightning Address updated!* + +New address: \`${address}\` + +โš ๏ธ *Note:* Previous ticket purchases will still use their original addresses.`, + + saveFailed: 'โŒ An error occurred saving your address. Please try again.', + + needAddressFirst: `โŒ I don't have your Lightning Address yet! + +Please send your Lightning Address first: + +*Example:* \`yourname@getalby.com\``, + }, + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // BUY TICKETS + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + buy: { + noActiveJackpot: `๐Ÿ˜” *No Active Jackpot* + +There's no lottery cycle available right now. Please check back soon!`, + + jackpotInfo: ( + potSats: string, + ticketPrice: string, + drawTime: string, + timeLeft: string + ) => + `๐ŸŽฐ *Next Jackpot Info* + +๐Ÿ’ฐ *Prize Pool:* ${potSats} sats +๐ŸŽŸ *Ticket Price:* ${ticketPrice} sats each +โฐ *Draw at:* ${drawTime} +โณ *Time left:* ${timeLeft} + +How many tickets do you want to buy?`, + + customAmountPrompt: (maxTickets: number) => + `๐Ÿ”ข *Custom Amount* + +Enter the number of tickets you want to buy (1-${maxTickets}):`, + + invalidNumber: (maxTickets: number) => + `โŒ Please enter a valid number (1-${maxTickets}):`, + + tooManyTickets: (maxTickets: number) => + `โŒ Maximum ${maxTickets} tickets per purchase. + +Please enter a smaller number:`, + + confirmPurchase: ( + ticketCount: number, + ticketPrice: string, + totalAmount: string, + drawTime: string + ) => + `๐Ÿ“‹ *Confirm Purchase* + +๐ŸŽŸ *Tickets:* ${ticketCount} +๐Ÿ’ฐ *Price:* ${ticketPrice} sats each +๐Ÿ’ต *Total:* ${totalAmount} sats + +โฐ *Draw:* ${drawTime} + +Confirm this purchase?`, + + creatingInvoice: 'Creating invoice...', + + invoiceCreated: '๐Ÿ’ธ *Pay this Lightning invoice to complete your purchase:*', + + invoiceCaption: ( + ticketCount: number, + totalAmount: string, + paymentRequest: string, + expiryMinutes: number + ) => + `๐ŸŽŸ *${ticketCount} ticket${ticketCount > 1 ? 's' : ''}* +๐Ÿ’ฐ *Amount:* ${totalAmount} sats + +\`${paymentRequest}\` + +โณ This invoice expires in ${expiryMinutes} minutes. +I'll notify you when payment is received!`, + + paymentReceived: (ticketNumbers: string, drawTime: string) => + `๐ŸŽ‰ *Payment Received!* + +Your tickets have been issued! + +*Your Ticket Numbers:* +${ticketNumbers} + +*Draw Time:* ${drawTime} + +Good luck! ๐Ÿ€ I'll notify you after the draw!`, + + invoiceExpired: `โŒ *Invoice Expired* + +No payment was received in time. No tickets were issued. + +Use /buy to try again.`, + + invoiceExpiredShort: `โŒ *Invoice Expired* + +This invoice has expired. No tickets were issued. + +Use /buy to try again.`, + + jackpotUnavailable: 'โŒ Jackpot is no longer available.', + }, + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // TICKETS + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + tickets: { + header: `๐Ÿงพ *Your Recent Purchases*\n\n`, + + empty: `๐Ÿงพ *Your Tickets* + +You haven't purchased any tickets yet! + +Use /buy to get started! ๐ŸŽŸ`, + + notFound: `๐Ÿงพ *Your Tickets* + +No ticket purchases found. Purchase history may have expired. + +Use /buy to get new tickets! ๐ŸŽŸ`, + + tapForDetails: `\nTap a ticket below for details:`, + + statusPending: 'Pending', + statusExpired: 'Expired', + statusActive: 'Active', + statusWon: 'Won!', + statusLost: 'Lost', + + // Ticket detail view + detailHeader: '๐ŸŽซ *Ticket Details*', + + detailAwaitingPayment: 'โณ *Status:* Awaiting Payment', + detailExpired: 'โŒ *Status:* Invoice Expired (No tickets issued)', + detailActive: '๐ŸŽŸ *Status:* Active - Draw Pending', + detailWinner: (prizeSats: string, payoutStatus: string) => + `๐Ÿ† *Status:* WINNER! +๐Ÿ’ฐ *Prize:* ${prizeSats} sats +๐Ÿ“ค *Payout:* ${payoutStatus}`, + detailLost: (winningTicketId: string) => + `๐Ÿ˜” *Status:* Did not win this round +๐ŸŽฏ *Winning Ticket:* #${winningTicketId}`, + + detailFormat: ( + purchaseId: string, + ticketCount: number, + ticketNumbers: string, + amountSats: string, + drawDate: string, + statusSection: string + ) => + `๐ŸŽซ *Ticket Details* + +๐Ÿ“‹ *Purchase ID:* \`${purchaseId}...\` +๐ŸŽŸ *Tickets:* ${ticketCount} +๐Ÿ”ข *Numbers:* ${ticketNumbers} +๐Ÿ’ฐ *Amount Paid:* ${amountSats} sats +๐Ÿ“… *Draw:* ${drawDate} + +${statusSection}`, + + // Status check responses + checkStatusPending: 'โณ Still waiting for payment...', + checkStatusConfirmed: 'โœ… Payment confirmed! Tickets issued. Waiting for draw...', + checkStatusWon: '๐Ÿ† YOU WON! Check your Lightning wallet!', + checkStatusLost: '๐Ÿ˜” Draw completed. Better luck next time!', + checkStatusExpired: 'โŒ Invoice expired. No tickets were issued.', + checkStatusProcessing: 'โณ Processing...', + }, + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // WINS + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + wins: { + empty: `๐Ÿ† *Your Wins* + +You haven't purchased any tickets yet, so no wins to show! + +Use /buy to get started! ๐ŸŽŸ`, + + noWinsYet: `๐Ÿ† *Your Wins* + +You haven't won any jackpots yet. Keep playing! + +Use /buy to get more tickets! ๐ŸŽŸ๐Ÿ€`, + + header: (totalWinnings: string, paidWinnings: string) => + `๐Ÿ† *Your Wins* + +๐Ÿ’ฐ *Total Winnings:* ${totalWinnings} sats +โœ… *Paid:* ${paidWinnings} sats + +*Win History:* +`, + }, + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // HELP + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + help: { + message: `โšก๐ŸŽฐ *Lightning Jackpot Bot* ๐ŸŽฐโšก + +This is the Lightning Jackpot lottery bot! Buy tickets with Bitcoin Lightning, and if you win, your prize is paid instantly! + +*How It Works:* +1๏ธโƒฃ Set your Lightning Address (where winnings go) +2๏ธโƒฃ Buy tickets using the menu +3๏ธโƒฃ Pay the Lightning invoice +4๏ธโƒฃ Wait for the draw +5๏ธโƒฃ If you win, sats are sent to your address instantly! + +*Commands:* +โ€ข /buy โ€” Buy lottery tickets +โ€ข /tickets โ€” View your tickets +โ€ข /wins โ€” View your past wins +โ€ข /address โ€” Update Lightning Address +โ€ข /menu โ€” Show main menu +โ€ข /help โ€” Show this help + +*Tips:* +๐ŸŽŸ Each ticket is one chance to win +๐Ÿ’ฐ Prize pool grows with each ticket sold +โšก Winnings are paid instantly via Lightning +๐Ÿ”” You'll be notified after every draw + +Good luck! ๐Ÿ€`, + }, + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // MENU + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + menu: { + header: `๐ŸŽฐ *Lightning Jackpot Menu* + +What would you like to do?`, + + cancelled: 'โŒ Cancelled.', + whatToDo: 'What would you like to do?', + didNotUnderstand: + "I didn't understand that. Use the menu below or type /help for available commands.", + }, + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // DRAW NOTIFICATIONS + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + notifications: { + winner: ( + prizeSats: string, + winningTicket: string, + payoutStatus: string + ) => + `๐ŸŽ‰ *YOU WON THE LIGHTNING JACKPOT!* ๐ŸŽ‰ + +๐Ÿ’ฐ *Prize:* ${prizeSats} sats +๐ŸŽŸ *Winning Ticket:* #${winningTicket} +๐Ÿ“ค *Payout Status:* ${payoutStatus} + +Congratulations! ๐Ÿฅณ`, + + loser: (winningTicket: string, prizeSats: string) => + `The draw has finished! +Your tickets did not win this time. + +๐ŸŽŸ *Winning Ticket:* #${winningTicket} +๐Ÿ’ฐ *Prize:* ${prizeSats} sats + +Good luck next round! ๐Ÿ€`, + + drawAnnouncement: ( + winnerName: string, + winningTicket: string, + prizeSats: string, + totalTickets: number + ) => + `๐ŸŽฐ *JACKPOT DRAW COMPLETE!* ๐ŸŽฐ + +๐Ÿ† *Winner:* ${winnerName} +๐ŸŽŸ *Winning Ticket:* #${winningTicket} +๐Ÿ’ฐ *Prize:* ${prizeSats} sats +๐Ÿ“Š *Total Tickets:* ${totalTickets} + +Congratulations to the winner! โšก + +Use /buy to enter the next draw! ๐Ÿ€`, + + drawReminder: (potSats: string, drawTime: string, timeLeft: string) => + `โฐ *Draw Reminder!* + +๐ŸŽฐ The next Lightning Jackpot draw is coming up! + +๐Ÿ’ฐ *Current Prize Pool:* ${potSats} sats +๐Ÿ• *Draw Time:* ${drawTime} +โณ *Time Left:* ${timeLeft} + +Don't miss your chance to win! Use /buy to get your tickets! ๐ŸŽŸ`, + }, + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // GROUPS + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + groups: { + welcome: (groupName: string) => + `โšก๐ŸŽฐ *Lightning Jackpot Bot Added!* ๐ŸŽฐโšก + +Hello *${groupName}*! I'm the Lightning Jackpot lottery bot. + +I can announce lottery draws and remind you when jackpots are coming up! + +*Group Admin Commands:* +โ€ข /settings โ€” Configure bot settings for this group + +*User Commands:* +โ€ข /buy โ€” Buy lottery tickets (in DM) +โ€ข /jackpot โ€” View current jackpot info +โ€ข /help โ€” Get help + +To buy tickets, message me directly @LightningLottoBot! ๐ŸŽŸ`, + + privateChat: 'โŒ This command only works in groups. Use /menu to see available commands.', + + adminOnly: 'โš ๏ธ Only group administrators can change these settings.', + + settingsOverview: (settings: { + groupTitle: string; + enabled: boolean; + drawAnnouncements: boolean; + reminders: boolean; + ticketPurchaseAllowed: boolean; + }) => + `โš™๏ธ *Group Settings* + +๐Ÿ“ *Group:* ${settings.groupTitle} + +*Current Configuration:* +${settings.enabled ? 'โœ…' : 'โŒ'} Bot Enabled +${settings.drawAnnouncements ? 'โœ…' : 'โŒ'} Draw Announcements +${settings.reminders ? 'โœ…' : 'โŒ'} Draw Reminders +${settings.ticketPurchaseAllowed ? 'โœ…' : 'โŒ'} Ticket Purchases in Group + +Tap a button below to toggle settings:`, + + settingUpdated: (setting: string, enabled: boolean) => + `โœ… *${setting}* has been ${enabled ? 'enabled' : 'disabled'}.`, + + botDisabled: 'The Lightning Jackpot bot is currently disabled for this group.', + + purchasesDisabled: `๐ŸŽŸ Ticket purchases are disabled in this group for privacy. + +Please message me directly to buy tickets: @LightningLottoBot`, + }, +}; + +export default messages; + diff --git a/telegram_bot/src/services/api.ts b/telegram_bot/src/services/api.ts new file mode 100644 index 0000000..82d54ca --- /dev/null +++ b/telegram_bot/src/services/api.ts @@ -0,0 +1,145 @@ +import axios, { AxiosInstance, AxiosError } from 'axios'; +import config from '../config'; +import { logger, logApiCall } from './logger'; +import { + ApiResponse, + JackpotNextResponse, + BuyTicketsResponse, + TicketStatusResponse, +} from '../types'; + +class ApiClient { + private client: AxiosInstance; + + constructor() { + this.client = axios.create({ + baseURL: config.api.baseUrl, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Response interceptor for logging + this.client.interceptors.response.use( + (response) => { + logApiCall( + response.config.url || '', + response.config.method?.toUpperCase() || 'GET', + response.status + ); + return response; + }, + (error: AxiosError) => { + logApiCall( + error.config?.url || '', + error.config?.method?.toUpperCase() || 'GET', + error.response?.status, + error.message + ); + throw error; + } + ); + } + + /** + * Get next jackpot cycle information + */ + async getNextJackpot(): Promise { + try { + const response = await this.client.get>( + '/jackpot/next' + ); + return response.data.data; + } catch (error) { + if (axios.isAxiosError(error)) { + const status = error.response?.status; + if (status === 503) { + // No active lottery or cycle + return null; + } + } + logger.error('Failed to get next jackpot', { error }); + throw error; + } + } + + /** + * Buy lottery tickets + */ + async buyTickets( + tickets: number, + lightningAddress: string, + telegramUserId: number + ): Promise { + try { + const response = await this.client.post>( + '/jackpot/buy', + { + tickets, + lightning_address: lightningAddress, + buyer_name: `TG:${telegramUserId}`, + } + ); + return response.data.data; + } catch (error) { + if (axios.isAxiosError(error)) { + const errorData = error.response?.data as ApiResponse; + if (errorData?.error) { + throw new Error(errorData.message || errorData.error); + } + } + logger.error('Failed to buy tickets', { error }); + throw error; + } + } + + /** + * Get ticket purchase status + */ + async getTicketStatus(purchaseId: string): Promise { + try { + const response = await this.client.get>( + `/tickets/${purchaseId}` + ); + return response.data.data; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + return null; + } + logger.error('Failed to get ticket status', { error, purchaseId }); + throw error; + } + } + + /** + * Get user's ticket purchases by lightning address + * Note: This queries tickets by lightning address pattern matching + */ + async getUserTickets( + telegramUserId: number, + limit: number = 10, + offset: number = 0 + ): Promise { + // Since the backend doesn't have Telegram-specific endpoints, + // we'll need to track purchases locally in state + // This is a placeholder for future backend integration + return []; + } + + /** + * Health check + */ + async healthCheck(): Promise { + try { + await this.client.get('/jackpot/next'); + return true; + } catch (error) { + return false; + } + } +} + +export const apiClient = new ApiClient(); +export default apiClient; + diff --git a/telegram_bot/src/services/groupState.ts b/telegram_bot/src/services/groupState.ts new file mode 100644 index 0000000..c4f0d4f --- /dev/null +++ b/telegram_bot/src/services/groupState.ts @@ -0,0 +1,224 @@ +import Redis from 'ioredis'; +import config from '../config'; +import { logger } from './logger'; +import { GroupSettings, DEFAULT_GROUP_SETTINGS } from '../types/groups'; + +const GROUP_PREFIX = 'tg_group:'; +const GROUPS_LIST_KEY = 'tg_groups_list'; +const STATE_TTL = 60 * 60 * 24 * 365; // 1 year + +class GroupStateManager { + private redis: Redis | null = null; + private memoryStore: Map = new Map(); + private useRedis: boolean = false; + + async init(redisUrl: string | null): Promise { + if (redisUrl) { + try { + this.redis = new Redis(redisUrl); + await this.redis.ping(); + this.useRedis = true; + logger.info('Group state manager initialized with Redis'); + } catch (error) { + logger.warn('Failed to connect to Redis for groups, using in-memory store'); + this.redis = null; + this.useRedis = false; + } + } + } + + private async get(key: string): Promise { + if (this.useRedis && this.redis) { + return await this.redis.get(key); + } + return this.memoryStore.get(key) || null; + } + + private async set(key: string, value: string, ttl?: number): Promise { + if (this.useRedis && this.redis) { + if (ttl) { + await this.redis.setex(key, ttl, value); + } else { + await this.redis.set(key, value); + } + } else { + this.memoryStore.set(key, value); + } + } + + private async del(key: string): Promise { + if (this.useRedis && this.redis) { + await this.redis.del(key); + } else { + this.memoryStore.delete(key); + } + } + + private async sadd(key: string, value: string): Promise { + if (this.useRedis && this.redis) { + await this.redis.sadd(key, value); + } else { + const existing = this.memoryStore.get(key); + const set = existing ? new Set(JSON.parse(existing)) : new Set(); + set.add(value); + this.memoryStore.set(key, JSON.stringify([...set])); + } + } + + private async srem(key: string, value: string): Promise { + if (this.useRedis && this.redis) { + await this.redis.srem(key, value); + } else { + const existing = this.memoryStore.get(key); + if (existing) { + const set = new Set(JSON.parse(existing)); + set.delete(value); + this.memoryStore.set(key, JSON.stringify([...set])); + } + } + } + + private async smembers(key: string): Promise { + if (this.useRedis && this.redis) { + return await this.redis.smembers(key); + } + const existing = this.memoryStore.get(key); + return existing ? JSON.parse(existing) : []; + } + + /** + * Get group settings + */ + async getGroup(groupId: number): Promise { + const key = `${GROUP_PREFIX}${groupId}`; + const data = await this.get(key); + if (!data) return null; + + try { + const settings = JSON.parse(data); + return { + ...settings, + addedAt: new Date(settings.addedAt), + updatedAt: new Date(settings.updatedAt), + }; + } catch (error) { + logger.error('Failed to parse group settings', { groupId, error }); + return null; + } + } + + /** + * Create or update group settings + */ + async saveGroup(settings: GroupSettings): Promise { + const key = `${GROUP_PREFIX}${settings.groupId}`; + settings.updatedAt = new Date(); + await this.set(key, JSON.stringify(settings), STATE_TTL); + await this.sadd(GROUPS_LIST_KEY, settings.groupId.toString()); + logger.debug('Group settings saved', { groupId: settings.groupId }); + } + + /** + * Register a new group when bot is added + */ + async registerGroup( + groupId: number, + groupTitle: string, + addedBy: number + ): Promise { + const existing = await this.getGroup(groupId); + + if (existing) { + // Update title if changed + existing.groupTitle = groupTitle; + existing.updatedAt = new Date(); + await this.saveGroup(existing); + return existing; + } + + const settings: GroupSettings = { + groupId, + groupTitle, + ...DEFAULT_GROUP_SETTINGS, + addedBy, + addedAt: new Date(), + updatedAt: new Date(), + }; + + await this.saveGroup(settings); + logger.info('New group registered', { groupId, groupTitle, addedBy }); + return settings; + } + + /** + * Remove group when bot is removed + */ + async removeGroup(groupId: number): Promise { + const key = `${GROUP_PREFIX}${groupId}`; + await this.del(key); + await this.srem(GROUPS_LIST_KEY, groupId.toString()); + logger.info('Group removed', { groupId }); + } + + /** + * Update a specific setting + */ + async updateSetting( + groupId: number, + setting: keyof Pick, + value: boolean + ): Promise { + const settings = await this.getGroup(groupId); + if (!settings) return null; + + settings[setting] = value; + await this.saveGroup(settings); + return settings; + } + + /** + * Get all groups with a specific feature enabled + */ + async getGroupsWithFeature( + feature: 'enabled' | 'drawAnnouncements' | 'reminders' + ): Promise { + const groupIds = await this.smembers(GROUPS_LIST_KEY); + const groups: GroupSettings[] = []; + + for (const id of groupIds) { + const settings = await this.getGroup(parseInt(id, 10)); + if (settings && settings.enabled && settings[feature]) { + groups.push(settings); + } + } + + return groups; + } + + /** + * Get all registered groups + */ + async getAllGroups(): Promise { + const groupIds = await this.smembers(GROUPS_LIST_KEY); + const groups: GroupSettings[] = []; + + for (const id of groupIds) { + const settings = await this.getGroup(parseInt(id, 10)); + if (settings) { + groups.push(settings); + } + } + + return groups; + } + + async close(): Promise { + if (this.redis) { + await this.redis.quit(); + } + } +} + +export const groupStateManager = new GroupStateManager(); +export default groupStateManager; + diff --git a/telegram_bot/src/services/logger.ts b/telegram_bot/src/services/logger.ts new file mode 100644 index 0000000..478cf30 --- /dev/null +++ b/telegram_bot/src/services/logger.ts @@ -0,0 +1,82 @@ +import winston from 'winston'; +import config from '../config'; + +const { combine, timestamp, printf, colorize, errors } = winston.format; + +const logFormat = printf(({ level, message, timestamp, stack, ...meta }) => { + let log = `${timestamp} [${level}]: ${message}`; + + // Add metadata if present + const metaKeys = Object.keys(meta); + if (metaKeys.length > 0) { + // Filter out sensitive data + const safeMeta = { ...meta }; + if (safeMeta.bolt11) { + const bolt11 = safeMeta.bolt11 as string; + safeMeta.bolt11 = `${bolt11.substring(0, 10)}...${bolt11.substring(bolt11.length - 10)}`; + } + if (safeMeta.lightningAddress && config.nodeEnv === 'production') { + safeMeta.lightningAddress = '[REDACTED]'; + } + log += ` ${JSON.stringify(safeMeta)}`; + } + + // Add stack trace for errors + if (stack) { + log += `\n${stack}`; + } + + return log; +}); + +export const logger = winston.createLogger({ + level: config.logging.level, + format: combine( + errors({ stack: true }), + timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + logFormat + ), + transports: [ + new winston.transports.Console({ + format: combine( + colorize(), + timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + logFormat + ), + }), + ], +}); + +// Helper functions for common log scenarios +export const logUserAction = ( + userId: number, + action: string, + details?: Record +) => { + logger.info(action, { userId, ...details }); +}; + +export const logApiCall = ( + endpoint: string, + method: string, + statusCode?: number, + error?: string +) => { + if (error) { + logger.error(`API ${method} ${endpoint} failed`, { statusCode, error }); + } else { + logger.debug(`API ${method} ${endpoint}`, { statusCode }); + } +}; + +export const logPaymentEvent = ( + userId: number, + purchaseId: string, + event: 'created' | 'confirmed' | 'expired' | 'polling', + details?: Record +) => { + logger.info(`Payment ${event}`, { userId, purchaseId, ...details }); +}; + +export default logger; + diff --git a/telegram_bot/src/services/qr.ts b/telegram_bot/src/services/qr.ts new file mode 100644 index 0000000..fd6ab0b --- /dev/null +++ b/telegram_bot/src/services/qr.ts @@ -0,0 +1,27 @@ +import QRCode from 'qrcode'; +import { logger } from './logger'; + +/** + * Generate a QR code as a buffer from a Lightning invoice + */ +export async function generateQRCode(data: string): Promise { + try { + const buffer = await QRCode.toBuffer(data.toUpperCase(), { + errorCorrectionLevel: 'M', + type: 'png', + margin: 2, + width: 300, + color: { + dark: '#000000', + light: '#FFFFFF', + }, + }); + return buffer; + } catch (error) { + logger.error('Failed to generate QR code', { error }); + throw error; + } +} + +export default { generateQRCode }; + diff --git a/telegram_bot/src/services/state.ts b/telegram_bot/src/services/state.ts new file mode 100644 index 0000000..6d6838f --- /dev/null +++ b/telegram_bot/src/services/state.ts @@ -0,0 +1,262 @@ +import Redis from 'ioredis'; +import config from '../config'; +import { logger } from './logger'; +import { + TelegramUser, + UserState, + AwaitingPaymentData, +} from '../types'; + +const STATE_PREFIX = 'tg_user:'; +const PURCHASE_PREFIX = 'tg_purchase:'; +const USER_PURCHASES_PREFIX = 'tg_user_purchases:'; +const STATE_TTL = 60 * 60 * 24 * 30; // 30 days + +class StateManager { + private redis: Redis | null = null; + private memoryStore: Map = new Map(); + private useRedis: boolean = false; + + async init(): Promise { + if (config.redis.url) { + try { + this.redis = new Redis(config.redis.url); + + this.redis.on('error', (error) => { + logger.error('Redis connection error', { error: error.message }); + }); + + this.redis.on('connect', () => { + logger.info('Connected to Redis'); + }); + + // Test connection + await this.redis.ping(); + this.useRedis = true; + logger.info('State manager initialized with Redis'); + } catch (error) { + logger.warn('Failed to connect to Redis, falling back to in-memory store', { + error: (error as Error).message, + }); + this.redis = null; + this.useRedis = false; + } + } else { + logger.info('State manager initialized with in-memory store'); + this.useRedis = false; + } + } + + private async get(key: string): Promise { + if (this.useRedis && this.redis) { + return await this.redis.get(key); + } + return this.memoryStore.get(key) || null; + } + + private async set(key: string, value: string, ttl?: number): Promise { + if (this.useRedis && this.redis) { + if (ttl) { + await this.redis.setex(key, ttl, value); + } else { + await this.redis.set(key, value); + } + } else { + this.memoryStore.set(key, value); + } + } + + private async del(key: string): Promise { + if (this.useRedis && this.redis) { + await this.redis.del(key); + } else { + this.memoryStore.delete(key); + } + } + + private async lpush(key: string, value: string): Promise { + if (this.useRedis && this.redis) { + await this.redis.lpush(key, value); + await this.redis.ltrim(key, 0, 99); // Keep last 100 purchases + } else { + const existing = this.memoryStore.get(key); + const list = existing ? JSON.parse(existing) : []; + list.unshift(value); + if (list.length > 100) list.pop(); + this.memoryStore.set(key, JSON.stringify(list)); + } + } + + private async lrange(key: string, start: number, stop: number): Promise { + if (this.useRedis && this.redis) { + return await this.redis.lrange(key, start, stop); + } + const existing = this.memoryStore.get(key); + if (!existing) return []; + const list = JSON.parse(existing); + return list.slice(start, stop + 1); + } + + /** + * Get or create user + */ + async getUser(telegramId: number): Promise { + const key = `${STATE_PREFIX}${telegramId}`; + const data = await this.get(key); + if (!data) return null; + + try { + const user = JSON.parse(data); + return { + ...user, + createdAt: new Date(user.createdAt), + updatedAt: new Date(user.updatedAt), + }; + } catch (error) { + logger.error('Failed to parse user data', { telegramId, error }); + return null; + } + } + + /** + * Create or update user + */ + async saveUser(user: TelegramUser): Promise { + const key = `${STATE_PREFIX}${user.telegramId}`; + user.updatedAt = new Date(); + await this.set(key, JSON.stringify(user), STATE_TTL); + logger.debug('User saved', { telegramId: user.telegramId, state: user.state }); + } + + /** + * Create new user + */ + async createUser( + telegramId: number, + username?: string, + firstName?: string, + lastName?: string + ): Promise { + const user: TelegramUser = { + telegramId, + username, + firstName, + lastName, + state: 'awaiting_lightning_address', + createdAt: new Date(), + updatedAt: new Date(), + }; + await this.saveUser(user); + logger.info('New user created', { telegramId, username }); + return user; + } + + /** + * Update user state + */ + async updateUserState( + telegramId: number, + state: UserState, + stateData?: Record + ): Promise { + const user = await this.getUser(telegramId); + if (!user) { + logger.warn('Attempted to update state for non-existent user', { telegramId }); + return; + } + user.state = state; + user.stateData = stateData; + await this.saveUser(user); + } + + /** + * Update user's lightning address + */ + async updateLightningAddress( + telegramId: number, + lightningAddress: string + ): Promise { + const user = await this.getUser(telegramId); + if (!user) { + logger.warn('Attempted to update address for non-existent user', { telegramId }); + return; + } + user.lightningAddress = lightningAddress; + user.state = 'idle'; + user.stateData = undefined; + await this.saveUser(user); + } + + /** + * Store a ticket purchase for a user + */ + async storePurchase( + telegramId: number, + purchaseId: string, + data: AwaitingPaymentData + ): Promise { + // Store purchase data + const purchaseKey = `${PURCHASE_PREFIX}${purchaseId}`; + await this.set(purchaseKey, JSON.stringify({ + telegramId, + ...data, + createdAt: new Date().toISOString(), + }), STATE_TTL); + + // Add to user's purchase list + const userPurchasesKey = `${USER_PURCHASES_PREFIX}${telegramId}`; + await this.lpush(userPurchasesKey, purchaseId); + } + + /** + * Get purchase data + */ + async getPurchase(purchaseId: string): Promise<(AwaitingPaymentData & { telegramId: number }) | null> { + const key = `${PURCHASE_PREFIX}${purchaseId}`; + const data = await this.get(key); + if (!data) return null; + + try { + return JSON.parse(data); + } catch (error) { + logger.error('Failed to parse purchase data', { purchaseId, error }); + return null; + } + } + + /** + * Get user's recent purchase IDs + */ + async getUserPurchaseIds( + telegramId: number, + limit: number = 10 + ): Promise { + const key = `${USER_PURCHASES_PREFIX}${telegramId}`; + return await this.lrange(key, 0, limit - 1); + } + + /** + * Clear user state data (keeping lightning address) + */ + async clearUserStateData(telegramId: number): Promise { + const user = await this.getUser(telegramId); + if (!user) return; + user.state = 'idle'; + user.stateData = undefined; + await this.saveUser(user); + } + + /** + * Shutdown + */ + async close(): Promise { + if (this.redis) { + await this.redis.quit(); + logger.info('Redis connection closed'); + } + } +} + +export const stateManager = new StateManager(); +export default stateManager; + diff --git a/telegram_bot/src/types/groups.ts b/telegram_bot/src/types/groups.ts new file mode 100644 index 0000000..bd3faed --- /dev/null +++ b/telegram_bot/src/types/groups.ts @@ -0,0 +1,25 @@ +/** + * Group settings for lottery features + */ +export interface GroupSettings { + groupId: number; + groupTitle: string; + enabled: boolean; + drawAnnouncements: boolean; + reminders: boolean; + ticketPurchaseAllowed: boolean; + addedBy: number; + addedAt: Date; + updatedAt: Date; +} + +/** + * Default group settings + */ +export const DEFAULT_GROUP_SETTINGS: Omit = { + enabled: true, + drawAnnouncements: true, + reminders: true, + ticketPurchaseAllowed: false, // Disabled by default for privacy - users should buy in DM +}; + diff --git a/telegram_bot/src/types/index.ts b/telegram_bot/src/types/index.ts new file mode 100644 index 0000000..0b2c723 --- /dev/null +++ b/telegram_bot/src/types/index.ts @@ -0,0 +1,138 @@ +// User state for conversation flow +export type UserState = + | 'idle' + | 'awaiting_lightning_address' + | 'awaiting_ticket_amount' + | 'awaiting_invoice_payment' + | 'updating_address'; + +// Telegram user data stored in state +export interface TelegramUser { + telegramId: number; + username?: string; + firstName?: string; + lastName?: string; + lightningAddress?: string; + state: UserState; + stateData?: Record; + createdAt: Date; + updatedAt: Date; +} + +// API Response Types +export interface ApiResponse { + version: string; + data: T; + error?: string; + message?: string; +} + +export interface JackpotNextResponse { + lottery: { + id: string; + name: string; + ticket_price_sats: number; + }; + cycle: { + id: string; + cycle_type: string; + scheduled_at: string; + sales_open_at: string; + sales_close_at: string; + status: string; + pot_total_sats: number; + }; +} + +export interface BuyTicketsResponse { + ticket_purchase_id: string; + public_url: string; + invoice: { + payment_request: string; + amount_sats: number; + }; +} + +export interface TicketStatusResponse { + purchase: { + id: string; + lottery_id: string; + cycle_id: string; + lightning_address: string; + buyer_name: string; + number_of_tickets: number; + ticket_price_sats: number; + amount_sats: number; + invoice_status: 'pending' | 'paid' | 'expired' | 'cancelled'; + ticket_issue_status: 'not_issued' | 'issued'; + created_at: string; + }; + tickets: Array<{ + id: string; + serial_number: number; + is_winning_ticket: boolean; + }>; + cycle: { + id: string; + cycle_type: string; + scheduled_at: string; + status: string; + pot_total_sats: number; + pot_after_fee_sats: number | null; + winning_ticket_id: string | null; + }; + result: { + has_drawn: boolean; + is_winner: boolean; + payout: { + status: string; + amount_sats: number; + } | null; + }; +} + +// Ticket purchase for user's ticket list +export interface UserTicketPurchase { + id: string; + cycle_id: string; + scheduled_at: string; + cycle_status: string; + number_of_tickets: number; + amount_sats: number; + invoice_status: string; + ticket_issue_status: string; + created_at: string; +} + +// User win entry +export interface UserWin { + id: string; + cycle_id: string; + ticket_id: string; + amount_sats: number; + status: string; + scheduled_at: string; + created_at: string; +} + +// State data for pending purchase +export interface PendingPurchaseData { + ticketCount: number; + cycleId: string; + scheduledAt: string; + ticketPrice: number; + totalAmount: number; + lotteryName: string; +} + +// State data for awaiting payment +export interface AwaitingPaymentData extends PendingPurchaseData { + purchaseId: string; + paymentRequest: string; + publicUrl: string; + pollStartTime: number; +} + +// Re-export group types +export * from './groups'; + diff --git a/telegram_bot/src/utils/format.ts b/telegram_bot/src/utils/format.ts new file mode 100644 index 0000000..bd6b599 --- /dev/null +++ b/telegram_bot/src/utils/format.ts @@ -0,0 +1,71 @@ +/** + * Format sats amount with thousand separators + */ +export function formatSats(sats: number): string { + return new Intl.NumberFormat('en-US').format(sats); +} + +/** + * Format date for display + */ +export function formatDate(date: Date | string): string { + const d = typeof date === 'string' ? new Date(date) : date; + return d.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZone: 'UTC', + timeZoneName: 'short', + }); +} + +/** + * Format relative time until draw + */ +export function formatTimeUntil(date: Date | string): string { + const d = typeof date === 'string' ? new Date(date) : date; + const now = new Date(); + const diff = d.getTime() - now.getTime(); + + if (diff < 0) { + return 'now'; + } + + const minutes = Math.floor(diff / (1000 * 60)); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) { + return `${days}d ${hours % 24}h`; + } + if (hours > 0) { + return `${hours}h ${minutes % 60}m`; + } + return `${minutes}m`; +} + +/** + * Validate Lightning Address format + */ +export function isValidLightningAddress(address: string): boolean { + // Basic format: something@something.something + const regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + return regex.test(address); +} + +/** + * Escape markdown special characters for Telegram MarkdownV2 + */ +export function escapeMarkdown(text: string): string { + return text.replace(/[_*\[\]()~`>#+=|{}.!-]/g, '\\$&'); +} + +/** + * Truncate string with ellipsis + */ +export function truncate(str: string, maxLength: number): string { + if (str.length <= maxLength) return str; + return str.substring(0, maxLength - 3) + '...'; +} + diff --git a/telegram_bot/src/utils/keyboards.ts b/telegram_bot/src/utils/keyboards.ts new file mode 100644 index 0000000..b3ddbb8 --- /dev/null +++ b/telegram_bot/src/utils/keyboards.ts @@ -0,0 +1,145 @@ +import TelegramBot, { + InlineKeyboardMarkup, + ReplyKeyboardMarkup, +} from 'node-telegram-bot-api'; + +/** + * Main menu reply keyboard + */ +export function getMainMenuKeyboard(): ReplyKeyboardMarkup { + return { + keyboard: [ + [{ text: '๐ŸŽŸ Buy Tickets' }, { text: '๐Ÿงพ My Tickets' }], + [{ text: '๐Ÿ† My Wins' }, { text: 'โšก Lightning Address' }], + [{ text: 'โ„น๏ธ Help' }], + ], + resize_keyboard: true, + one_time_keyboard: false, + }; +} + +/** + * Quick ticket amount selection + */ +export function getTicketAmountKeyboard(): InlineKeyboardMarkup { + return { + inline_keyboard: [ + [ + { text: '1 ticket', callback_data: 'buy_1' }, + { text: '2 tickets', callback_data: 'buy_2' }, + { text: '5 tickets', callback_data: 'buy_5' }, + { text: '10 tickets', callback_data: 'buy_10' }, + ], + [{ text: '๐Ÿ”ข Custom Amount', callback_data: 'buy_custom' }], + [{ text: 'โŒ Cancel', callback_data: 'cancel' }], + ], + }; +} + +/** + * Confirmation keyboard + */ +export function getConfirmationKeyboard(): InlineKeyboardMarkup { + return { + inline_keyboard: [ + [ + { text: 'โœ… Confirm', callback_data: 'confirm_purchase' }, + { text: 'โŒ Cancel', callback_data: 'cancel' }, + ], + ], + }; +} + +/** + * Check if URL is valid for Telegram inline buttons (must be HTTPS, not localhost) + */ +function isValidTelegramUrl(url: string): boolean { + try { + const parsed = new URL(url); + // Telegram requires HTTPS and doesn't accept localhost/127.0.0.1 + return ( + parsed.protocol === 'https:' && + !parsed.hostname.includes('localhost') && + !parsed.hostname.includes('127.0.0.1') + ); + } catch { + return false; + } +} + +/** + * View ticket status button + */ +export function getViewTicketKeyboard( + purchaseId: string, + publicUrl?: string +): InlineKeyboardMarkup { + const buttons: TelegramBot.InlineKeyboardButton[][] = [ + [{ text: '๐Ÿ”„ Check Status', callback_data: `status_${purchaseId}` }], + ]; + + // Only add URL button if it's a valid Telegram URL (HTTPS, not localhost) + if (publicUrl && isValidTelegramUrl(publicUrl)) { + buttons.push([{ text: '๐ŸŒ View on Web', url: publicUrl }]); + } + + return { + inline_keyboard: buttons, + }; +} + +/** + * Ticket list navigation + */ +export function getTicketListKeyboard( + tickets: Array<{ id: string; label: string }>, + currentPage: number, + hasMore: boolean +): InlineKeyboardMarkup { + const buttons: TelegramBot.InlineKeyboardButton[][] = []; + + // Add ticket buttons + for (const ticket of tickets) { + buttons.push([{ + text: ticket.label, + callback_data: `view_ticket_${ticket.id}`, + }]); + } + + // Add pagination + const navButtons: TelegramBot.InlineKeyboardButton[] = []; + if (currentPage > 0) { + navButtons.push({ text: 'โฌ…๏ธ Previous', callback_data: `tickets_page_${currentPage - 1}` }); + } + if (hasMore) { + navButtons.push({ text: 'โžก๏ธ Next', callback_data: `tickets_page_${currentPage + 1}` }); + } + if (navButtons.length > 0) { + buttons.push(navButtons); + } + + return { inline_keyboard: buttons }; +} + +/** + * Back to menu button + */ +export function getBackToMenuKeyboard(): InlineKeyboardMarkup { + return { + inline_keyboard: [ + [{ text: '๐Ÿ  Back to Menu', callback_data: 'menu' }], + ], + }; +} + +/** + * Cancel operation button + */ +export function getCancelKeyboard(): InlineKeyboardMarkup { + return { + inline_keyboard: [ + [{ text: 'โŒ Cancel', callback_data: 'cancel' }], + ], + }; +} + diff --git a/telegram_bot/tsconfig.json b/telegram_bot/tsconfig.json new file mode 100644 index 0000000..2b11876 --- /dev/null +++ b/telegram_bot/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +