commit 23a455db95d92ee7bd61d8de47285852db2bd519 Author: debudebuye Date: Sat Jan 10 09:26:32 2026 +0300 feat: Initialize Yaltopia Telegram bot project with core architecture diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..35a9f33 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Telegram Bot Configuration +TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here + +# API Configuration +API_BASE_URL=http://localhost:3000/api \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e62130f --- /dev/null +++ b/.gitignore @@ -0,0 +1,101 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build output +dist/ +build/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +public + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Editor directories and files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Development Documentation + +docs/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b4e411f --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# Yaltopia Telegram Bot + +A feature-rich Telegram bot for property listings built with TypeScript and organized using a feature-first architecture. + +## Features + +- 📱 **Phone-based Authentication**: Users share phone number to login/register +- 🏠 **Property Management**: Browse, view, and add properties +- 👤 **User Registration**: Complete registration flow with validation +- 🔐 **Secure Login**: Password-based authentication for existing users +- 📋 **Property Listings**: View all available properties +- 🏘️ **My Properties**: Manage user's own property listings + +## Architecture + +This project follows a **feature-first approach** for better scalability: + +``` +src/ +├── shared/ # Shared utilities and services +│ ├── types/ # Common TypeScript interfaces +│ └── services/ # Shared services (API, Session) +├── features/ # Feature modules +│ ├── auth/ # Authentication feature +│ └── properties/ # Property management feature +└── bot/ # Bot orchestration +``` + +## Setup + +1. **Install dependencies:** + ```bash + npm install + ``` + +2. **Environment setup:** + ```bash + cp .env.example .env + ``` + + Edit `.env` and add your Telegram bot token: + ``` + TELEGRAM_BOT_TOKEN=your_bot_token_here + API_BASE_URL=http://localhost:3000/api + ``` + +3. **Build the project:** + ```bash + npm run build + ``` + +4. **Start the bot:** + ```bash + npm start + ``` + + For development: + ```bash + npm run dev + ``` + +## Bot Flow + +### 1. Authentication Flow +- User starts with `/start` +- Bot requests phone number +- If phone exists → Login with password +- If new user → Registration flow (name, email, password, confirm) + +### 2. Main Menu (After Login) +- 🏘️ **Browse Properties**: View all available properties +- 🏠 **My Properties**: View user's own listings +- ➕ **Add Property**: Create new property listing +- 👤 **Profile**: View user profile information + +### 3. Property Creation Flow +- Title → Description → Type (Rent/Sale) → Price → Area → Rooms → Toilets → Subcity → House Type + +## API Integration + +The bot integrates with your existing backend APIs: + +- `POST /telegram-auth/telegram-register` - User registration +- `POST /telegram-auth/telegram-login` - User login +- `GET /telegram-auth/phone/{phone}/check` - Check phone existence +- `GET /telegram-auth/validate-email/{email}` - Email validation +- `GET /listings` - Get all properties +- `POST /listings` - Create new property + +## Development + +### Scripts +- `npm run build` - Compile TypeScript +- `npm run dev` - Run in development mode with ts-node +- `npm run watch` - Watch mode for TypeScript compilation +- `npm start` - Run compiled JavaScript + +### Adding New Features + +1. Create feature directory in `src/features/` +2. Add service class for API interactions +3. Add handler class for bot interactions +4. Integrate in main bot class (`src/bot/bot.ts`) + +## Getting Your Bot Token + +1. Message [@BotFather](https://t.me/botfather) on Telegram +2. Use `/newbot` command +3. Follow the setup process +4. Copy the token to your `.env` file \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a6713f5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2733 @@ +{ + "name": "yaltopia-telegram-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "yaltopia-telegram-bot", + "version": "1.0.0", + "dependencies": { + "axios": "^1.6.0", + "dotenv": "^16.3.1", + "node-telegram-bot-api": "^0.66.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@types/node-telegram-bot-api": "^0.64.0", + "ts-node": "^10.9.0", + "typescript": "^5.3.0" + } + }, + "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/@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/@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.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "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/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/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/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-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/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/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/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/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/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/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": "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/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/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/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/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.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "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/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/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/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-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/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-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/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/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-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-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-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-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-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-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/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "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/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.66.0", + "resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.66.0.tgz", + "integrity": "sha512-s4Hrg5q+VPl4/tJVG++pImxF6eb8tNJNj4KnDqAOKL6zGU34lo9RXmyAN158njwGN+v8hdNf8s9fWIYW9hPb5A==", + "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/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/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/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/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/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/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/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/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/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/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/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/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.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/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/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/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/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-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/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/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/package.json b/package.json new file mode 100644 index 0000000..78aa6ee --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "yaltopia-telegram-bot", + "version": "1.0.0", + "description": "Yaltopia property listing Telegram bot", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node src/index.ts", + "watch": "tsc -w" + }, + "dependencies": { + "node-telegram-bot-api": "^0.66.0", + "axios": "^1.6.0", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@types/node-telegram-bot-api": "^0.64.0", + "typescript": "^5.3.0", + "ts-node": "^10.9.0" + } +} \ No newline at end of file diff --git a/src/bot/bot.ts b/src/bot/bot.ts new file mode 100644 index 0000000..f1cf407 --- /dev/null +++ b/src/bot/bot.ts @@ -0,0 +1,370 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { ApiService } from '../shared/services/api.service'; +import { SessionService } from '../shared/services/session.service'; +import { AuthService } from '../features/auth/auth.service'; +import { AuthHandler } from '../features/auth/auth.handler'; +import { PropertiesService } from '../features/properties/properties.service'; +import { PropertiesHandler } from '../features/properties/properties.handler'; +import { SystemMonitor } from '../shared/monitoring/system-monitor'; +import { Logger, LogLevel } from '../shared/monitoring/logger'; +import { AdminNotifier } from '../shared/monitoring/admin-notifier'; + +export class YaltopiaBot { + private bot: TelegramBot; + private apiService: ApiService; + private sessionService: SessionService; + private authService: AuthService; + private authHandler: AuthHandler; + private propertiesService: PropertiesService; + private propertiesHandler: PropertiesHandler; + private systemMonitor: SystemMonitor; + private logger: Logger; + private adminNotifier: AdminNotifier; + + constructor(token: string, apiBaseUrl: string, adminChatIds: number[] = []) { + this.bot = new TelegramBot(token, { polling: true }); + this.apiService = new ApiService(apiBaseUrl); + this.sessionService = new SessionService(); + + // Initialize monitoring + this.logger = new Logger(LogLevel.INFO); + this.systemMonitor = new SystemMonitor({ adminChatId: adminChatIds[0] }); + this.adminNotifier = new AdminNotifier(this.bot, { adminChatIds }); + + // Initialize services + this.authService = new AuthService(this.apiService); + this.propertiesService = new PropertiesService(this.apiService); + + // Initialize handlers + this.authHandler = new AuthHandler(this.bot, this.authService, this.sessionService, this.apiService); + this.propertiesHandler = new PropertiesHandler(this.bot, this.propertiesService, this.sessionService); + + this.setupHandlers(); + } + + private setupHandlers(): void { + // Start command + this.bot.onText(/\/start/, async (msg) => { + this.systemMonitor.trackUserActivity(msg.chat.id, 'start', true); + this.logger.userAction(msg.chat.id, 'start', true); + await this.authHandler.handleStart(msg.chat.id); + }); + + // Admin commands + this.bot.onText(/\/(status|metrics|errors|users|restart)(.*)/, async (msg, match) => { + if (match) { + const command = match[1]; + const args = match[2] ? match[2].trim().split(' ').filter(arg => arg) : []; + await this.adminNotifier.handleAdminCommand(msg.chat.id, `/${command}`, args); + } + }); + + // Contact handler (phone number) + this.bot.on('contact', async (msg) => { + if (msg.contact && msg.contact.phone_number) { + this.systemMonitor.trackUserActivity(msg.chat.id, 'phone_shared', true); + this.logger.userAction(msg.chat.id, 'phone_shared', true, { phone: msg.contact.phone_number }); + await this.authHandler.handlePhoneNumber(msg.chat.id, msg.contact.phone_number); + } + }); + + // Text message handler + this.bot.on('message', async (msg) => { + if (msg.text && !msg.text.startsWith('/') && !msg.contact) { + await this.handleTextMessage(msg.chat.id, msg.text); + } + }); + + // Callback query handler for inline buttons + this.bot.on('callback_query', async (callbackQuery) => { + await this.propertiesHandler.handlePropertyCallback(callbackQuery); + }); + + // Error handler + this.bot.on('polling_error', (error) => { + this.logger.error('Telegram polling error', undefined, { error: error.message }, error); + this.systemMonitor.trackError(`Polling error: ${error.message}`); + }); + } + + private async handleTextMessage(chatId: number, text: string): Promise { + const session = this.sessionService.getSession(chatId); + + // Restore access token if user is logged in + if (session.user?.access_token) { + this.apiService.setAccessToken(session.user.access_token); + } + + console.log(`💬 User ${chatId} sent message: "${text}" (step: ${session.step})`); + + try { + switch (session.step) { + case 'LOGIN_PASSWORD': + await this.authHandler.handleLoginPassword(chatId, text); + break; + + case 'REGISTER_NAME': + await this.authHandler.handleRegistrationName(chatId, text); + break; + + case 'REGISTER_EMAIL': + await this.authHandler.handleRegistrationEmail(chatId, text); + break; + + case 'REGISTER_PASSWORD': + await this.authHandler.handleRegistrationPassword(chatId, text); + break; + + case 'REGISTER_CONFIRM_PASSWORD': + await this.authHandler.handleRegistrationConfirmPassword(chatId, text); + break; + + case 'LOGGED_IN': + await this.handleLoggedInCommands(chatId, text); + break; + + // Property creation steps + case 'ADD_PROPERTY_TITLE': + if (text === '❌ Cancel') { + await this.propertiesHandler.handleCancelPropertyCreation(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handlePropertyTitle(chatId, text); + } + break; + + case 'ADD_PROPERTY_DESCRIPTION': + if (text === '❌ Cancel') { + await this.propertiesHandler.handleCancelPropertyCreation(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handlePropertyDescription(chatId, text); + } + break; + + case 'ADD_PROPERTY_TYPE': + if (text === '❌ Cancel') { + await this.propertiesHandler.handleCancelPropertyCreation(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handlePropertyType(chatId, text); + } + break; + + case 'ADD_PROPERTY_PRICE': + if (text === '❌ Cancel') { + await this.propertiesHandler.handleCancelPropertyCreation(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handlePropertyPrice(chatId, text); + } + break; + + case 'ADD_PROPERTY_AREA': + if (text === '❌ Cancel') { + await this.propertiesHandler.handleCancelPropertyCreation(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handlePropertyArea(chatId, text); + } + break; + + case 'ADD_PROPERTY_ROOMS': + if (text === '❌ Cancel') { + await this.propertiesHandler.handleCancelPropertyCreation(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handlePropertyRooms(chatId, text); + } + break; + + case 'ADD_PROPERTY_TOILETS': + if (text === '❌ Cancel') { + await this.propertiesHandler.handleCancelPropertyCreation(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handlePropertyToilets(chatId, text); + } + break; + + case 'ADD_PROPERTY_SUBCITY': + if (text === '❌ Cancel') { + await this.propertiesHandler.handleCancelPropertyCreation(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handlePropertySubcity(chatId, text); + } + break; + + case 'ADD_PROPERTY_HOUSE_TYPE': + if (text === '❌ Cancel') { + await this.propertiesHandler.handleCancelPropertyCreation(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handlePropertyHouseType(chatId, text); + } + break; + + // Edit property steps + case 'EDIT_PROPERTY_TITLE': + if (text === '❌ Cancel Edit') { + await this.propertiesHandler.handleCancelEdit(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handleEditPropertyValue(chatId, 'title', text); + } + break; + + case 'EDIT_PROPERTY_DESCRIPTION': + if (text === '❌ Cancel Edit') { + await this.propertiesHandler.handleCancelEdit(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handleEditPropertyValue(chatId, 'description', text); + } + break; + + case 'EDIT_PROPERTY_PRICE': + if (text === '❌ Cancel Edit') { + await this.propertiesHandler.handleCancelEdit(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handleEditPropertyValue(chatId, 'price', text); + } + break; + + case 'EDIT_PROPERTY_AREA': + if (text === '❌ Cancel Edit') { + await this.propertiesHandler.handleCancelEdit(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handleEditPropertyValue(chatId, 'area', text); + } + break; + + case 'EDIT_PROPERTY_ROOMS': + if (text === '❌ Cancel Edit') { + await this.propertiesHandler.handleCancelEdit(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handleEditPropertyValue(chatId, 'rooms', text); + } + break; + + case 'EDIT_PROPERTY_TOILETS': + if (text === '❌ Cancel Edit') { + await this.propertiesHandler.handleCancelEdit(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handleEditPropertyValue(chatId, 'toilets', text); + } + break; + + case 'EDIT_PROPERTY_LOCATION': + if (text === '❌ Cancel Edit') { + await this.propertiesHandler.handleCancelEdit(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handleEditPropertyValue(chatId, 'subcity', text); + } + break; + + case 'EDIT_PROPERTY_HOUSE_TYPE': + if (text === '❌ Cancel Edit') { + await this.propertiesHandler.handleCancelEdit(chatId); + } else if (text === '🚪 Logout') { + await this.authHandler.handleLogout(chatId); + } else { + await this.propertiesHandler.handleEditPropertyValue(chatId, 'houseType', text); + } + break; + + default: + console.log(`❓ User ${chatId} in unknown step: ${session.step}`); + await this.bot.sendMessage(chatId, + 'Please start by typing /start' + ); + } + } catch (error) { + console.error(`💥 Error handling message for user ${chatId}:`, error); + await this.bot.sendMessage(chatId, + '❌ Something went wrong. Please try again or type /start to restart.' + ); + } + } + + private async handleLoggedInCommands(chatId: number, text: string): Promise { + switch (text) { + case '🏘️ Browse Properties': + await this.propertiesHandler.handleBrowseProperties(chatId); + break; + + case '🏠 My Properties': + await this.propertiesHandler.handleMyProperties(chatId); + break; + + case '➕ Add Property': + await this.propertiesHandler.handleAddProperty(chatId); + break; + + case '👤 Profile': + await this.handleProfile(chatId); + break; + + case '🚪 Logout': + await this.authHandler.handleLogout(chatId); + break; + + default: + await this.bot.sendMessage(chatId, + 'Please select an option from the menu below:' + ); + } + } + + private async handleProfile(chatId: number): Promise { + const session = this.sessionService.getSession(chatId); + + if (!session.user) { + await this.bot.sendMessage(chatId, '❌ User not found.'); + return; + } + + const user = session.user; + console.log('🔍 [DEBUG] Profile - User object:', JSON.stringify(user, null, 2)); + + const profileMessage = + `👤 Your Profile\n\n` + + `📛 Name: ${user.name || 'Not available'}\n` + + `📧 Email: ${user.email || 'Not available'}\n` + + `📱 Phone: ${user.phone || 'Not available'}\n` + + `👔 Role: ${user.role || 'Not available'}`; + + await this.bot.sendMessage(chatId, profileMessage); + } + + start(): void { + console.log('🤖 Yaltopia Bot started successfully! [VERSION 2.0 - WITH VALIDATION]'); + } + + stop(): void { + this.bot.stopPolling(); + console.log('🤖 Yaltopia Bot stopped.'); + } +} \ No newline at end of file diff --git a/src/features/auth/auth.handler.ts b/src/features/auth/auth.handler.ts new file mode 100644 index 0000000..6f357d7 --- /dev/null +++ b/src/features/auth/auth.handler.ts @@ -0,0 +1,265 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { AuthService } from './auth.service'; +import { SessionService } from '../../shared/services/session.service'; +import { ApiService } from '../../shared/services/api.service'; + +export class AuthHandler { + constructor( + private bot: TelegramBot, + private authService: AuthService, + private sessionService: SessionService, + private apiService: ApiService + ) {} + + async handleStart(chatId: number): Promise { + console.log(`🚀 User ${chatId} started the bot`); + this.sessionService.setSessionStep(chatId, 'PHONE'); + await this.bot.sendMessage(chatId, + '🏠 Welcome to Yaltopia Property Bot!\n\n' + + 'Please share your phone number to get started.', + { + reply_markup: { + keyboard: [[{ text: 'Share Phone Number', request_contact: true }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handlePhoneNumber(chatId: number, phone: string): Promise { + console.log(`📱 User ${chatId} shared phone: ${phone}`); + + // Normalize phone number format - ensure it starts with + + let normalizedPhone = phone; + if (!phone.startsWith('+')) { + normalizedPhone = '+' + phone; + } + + console.log(`📱 Normalized phone: ${normalizedPhone}`); + + const phoneCheck = await this.authService.checkPhoneExists(normalizedPhone); + console.log(`📱 Phone check result for ${chatId}:`, phoneCheck); + + if (!phoneCheck.exists) { + // Scenario 3: Phone number doesn't exist - proceed with registration + console.log(`📝 User ${chatId} - phone not found, directing to registration`); + this.sessionService.setSessionStep(chatId, 'REGISTER_NAME', { phone: normalizedPhone }); + await this.bot.sendMessage(chatId, + '📝 New user! Let\'s create your AGENT account.\n\n' + + 'Please enter your full name:', + { reply_markup: { remove_keyboard: true } } + ); + } else if (phoneCheck.exists && phoneCheck.isAgent) { + // Scenario 1: Phone exists and user is AGENT - proceed with login + console.log(`🔐 User ${chatId} - AGENT account found, directing to login`); + this.sessionService.setSessionStep(chatId, 'LOGIN_PASSWORD', { phone: normalizedPhone }); + await this.bot.sendMessage(chatId, + '� AGENT aoccount found! Please enter your password:', + { reply_markup: { remove_keyboard: true } } + ); + } else if (phoneCheck.exists && !phoneCheck.isAgent) { + // Scenario 2: Phone exists but user has different role - contact admin (don't reveal role) + console.log(`❌ User ${chatId} - account exists with role ${phoneCheck.userRole}, not AGENT`); + await this.bot.sendMessage(chatId, + `❌ Account Access Restricted\n\n` + + '📞 Please contact the administrator for assistance or use a different phone number.', + { reply_markup: { remove_keyboard: true } } + ); + } else { + // Fallback case + console.log(`❓ User ${chatId} - unexpected phone check result`); + await this.bot.sendMessage(chatId, + '❌ Unable to verify phone number. Please try again or contact support.', + { reply_markup: { remove_keyboard: true } } + ); + } + } + + async handleLoginPassword(chatId: number, password: string): Promise { + console.log(`🔐 User ${chatId} attempting login`); + + const session = this.sessionService.getSession(chatId); + const phone = session.data.phone; + + console.log(`🔐 Login attempt - Phone: ${phone}, Password length: ${password.length}`); + + const result = await this.authService.login(phone, password); + + if (result.success && result.user) { + console.log(`✅ User ${chatId} login successful, role: ${result.user.role}`); + + // Additional role check after successful login + if (result.user.role !== 'AGENT') { + console.log(`❌ User ${chatId} denied access - role is ${result.user.role}, not AGENT`); + await this.bot.sendMessage(chatId, + '❌ Access denied. This bot is only available for AGENT accounts.\n\n' + + 'Please contact support if you believe this is an error.' + ); + return; + } + + this.sessionService.updateSession(chatId, { + step: 'LOGGED_IN', + user: result.user + }); + await this.showMainMenu(chatId, result.user.name); + } else { + console.log(`❌ User ${chatId} login failed: ${result.message}`); + + // Check if it might be an account status issue + if (result.message?.toLowerCase().includes('pending') || + result.message?.toLowerCase().includes('inactive') || + result.message?.toLowerCase().includes('disabled')) { + await this.bot.sendMessage(chatId, + '⏳ Your account is pending activation.\n\n' + + 'Please contact the administrator to activate your account before you can login.' + ); + } else { + await this.bot.sendMessage(chatId, + '❌ Invalid password. Please try again:\n\n' + + '💡 Tip: Make sure you\'re using the same password you used during registration.' + ); + } + } + } + + async handleRegistrationName(chatId: number, name: string): Promise { + console.log(`📝 User ${chatId} entered name: ${name}`); + this.sessionService.setSessionStep(chatId, 'REGISTER_EMAIL', { name }); + await this.bot.sendMessage(chatId, + '✅ Name saved!\n\n' + + 'Please enter your email address:' + ); + } + + async handleRegistrationEmail(chatId: number, email: string): Promise { + console.log(`📧 User ${chatId} entered email: ${email}`); + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + console.log(`❌ User ${chatId} entered invalid email format`); + await this.bot.sendMessage(chatId, + '❌ Invalid email format. Please enter a valid email:' + ); + return; + } + + const isValid = await this.authService.validateEmail(email); + if (!isValid) { + console.log(`❌ User ${chatId} email validation failed`); + await this.bot.sendMessage(chatId, + '❌ Email already exists or is invalid. Please try another:' + ); + return; + } + + console.log(`✅ User ${chatId} email validated successfully`); + this.sessionService.setSessionStep(chatId, 'REGISTER_PASSWORD', { email }); + await this.bot.sendMessage(chatId, + '✅ Email saved!\n\n' + + 'Please create a password (minimum 6 characters):' + ); + } + + async handleRegistrationPassword(chatId: number, password: string): Promise { + if (password.length < 6) { + await this.bot.sendMessage(chatId, + '❌ Password must be at least 6 characters. Please try again:' + ); + return; + } + + this.sessionService.setSessionStep(chatId, 'REGISTER_CONFIRM_PASSWORD', { password }); + await this.bot.sendMessage(chatId, + '✅ Password saved!\n\n' + + 'Please confirm your password:' + ); + } + + async handleRegistrationConfirmPassword(chatId: number, confirmPassword: string): Promise { + console.log(`🔐 User ${chatId} confirming password`); + + const session = this.sessionService.getSession(chatId); + const { phone, name, email, password } = session.data; + + if (password !== confirmPassword) { + console.log(`❌ User ${chatId} password confirmation failed`); + await this.bot.sendMessage(chatId, + '❌ Passwords don\'t match. Please confirm your password again:' + ); + return; + } + + const userData = { + name, + email, + phone, + password, + role: 'AGENT' as const // Backend now accepts role field + }; + + console.log(`📝 User ${chatId} starting registration with data:`, { ...userData, password: '[HIDDEN]' }); + + const result = await this.authService.register(userData); + + if (result.success && result.user) { + console.log(`✅ User ${chatId} registration successful, assigned role: ${result.user.role}`); + + // Verify the registered user has AGENT role + if (result.user.role !== 'AGENT') { + console.log(`❌ User ${chatId} registration failed - role is ${result.user.role}, not AGENT`); + await this.bot.sendMessage(chatId, + '❌ Registration failed: Invalid role assignment.\n\n' + + 'Please contact support for assistance.' + ); + return; + } + + // User has AGENT role - proceed with login + this.sessionService.updateSession(chatId, { + step: 'LOGGED_IN', + user: result.user + }); + await this.bot.sendMessage(chatId, + '🎉 AGENT registration successful! Welcome to Yaltopia!' + ); + await this.showMainMenu(chatId, result.user.name); + } else { + console.log(`❌ User ${chatId} registration failed: ${result.message}`); + await this.bot.sendMessage(chatId, + `❌ Registration failed: ${result.message || 'Unknown error'}\n\n` + + 'Please try again by typing /start' + ); + } + } + + private async showMainMenu(chatId: number, userName: string): Promise { + await this.bot.sendMessage(chatId, + `🏠 Welcome ${userName}!\n\n` + + 'What would you like to do?', + { + reply_markup: { + keyboard: [ + [{ text: '🏘️ Browse Properties' }, { text: '🏠 My Properties' }], + [{ text: '➕ Add Property' }, { text: '👤 Profile' }], + [{ text: '🚪 Logout' }] + ], + resize_keyboard: true + } + } + ); + } + + async handleLogout(chatId: number): Promise { + // Clear access token + this.apiService.clearAccessToken(); + + this.sessionService.clearSession(chatId); + await this.bot.sendMessage(chatId, + '👋 You have been logged out successfully!\n\n' + + 'Type /start to login again.', + { reply_markup: { remove_keyboard: true } } + ); + } +} \ No newline at end of file diff --git a/src/features/auth/auth.service.ts b/src/features/auth/auth.service.ts new file mode 100644 index 0000000..8a95c8b --- /dev/null +++ b/src/features/auth/auth.service.ts @@ -0,0 +1,181 @@ +import { ApiService } from '../../shared/services/api.service'; +import { User, UserRegistration } from '../../shared/types'; + +export class AuthService { + constructor(private apiService: ApiService) {} + + async checkPhoneExists(phone: string): Promise<{ exists: boolean; isAgent: boolean; userRole?: string; message?: string }> { + console.log(`🔍 Checking phone existence for: ${phone}`); + + const response = await this.apiService.get(`/telegram-auth/phone/${phone}/check`); + + console.log('📞 Phone check API response:', JSON.stringify(response, null, 2)); + + if (!response.success) { + console.log(`❌ Phone check failed: ${response.message}`); + return { exists: false, isAgent: false, message: response.message }; + } + + // Parse the actual API response structure + const apiData = response.data as any; + + if (!apiData) { + console.log('📞 No data returned from phone check API'); + return { exists: false, isAgent: false }; + } + + // Check if user exists based on the API response + // The API uses "flow" to indicate user status: "login" = existing user, "register" = new user + const flow = apiData.flow; + const hasAccount = flow === 'login'; + + console.log(`📞 Phone check result - flow: ${flow}, hasAccount: ${hasAccount}`); + + if (!hasAccount) { + console.log('👤 New user detected - directing to registration'); + return { exists: false, isAgent: false }; + } + + // User exists - we'll check their role after login since the phone check doesn't provide role info + // and the user details endpoint may not be available + console.log('👤 Existing user detected - will verify role after login'); + + return { + exists: true, + isAgent: true, // Assume true for now, will verify after login + userRole: undefined, // Will be checked after login + message: undefined + }; + } + + async getUserByPhone(phone: string): Promise<{ success: boolean; user?: User; message?: string }> { + const response = await this.apiService.get(`/telegram-auth/user/${phone}`); + return { + success: response.success, + user: response.data, + message: response.message + }; + } + + async validateEmail(email: string): Promise { + console.log(`📧 Validating email: ${email}`); + const response = await this.apiService.get(`/telegram-auth/validate-email/${email}`); + console.log(`📧 Email validation result: ${response.success}`); + return response.success; + } + + async register(userData: UserRegistration): Promise<{ success: boolean; user?: User; message?: string }> { + console.log('📝 Starting user registration:', { ...userData, password: '[HIDDEN]' }); + + const response = await this.apiService.post('/telegram-auth/telegram-register', userData); + + console.log('📝 Registration API full response:', JSON.stringify(response, null, 2)); + + // Extract user data and access token from the nested response structure + let extractedUser = response.data; + let accessToken: string | undefined; + + // The API returns nested structure: response.data.user and response.data.access_token + if (response.success && response.data) { + const responseAny = response as any; + + if (responseAny.data?.user) { + extractedUser = responseAny.data.user; + accessToken = responseAny.data.access_token; + console.log('✅ Found user data in response.data.user'); + } else if (responseAny.userData?.user) { + extractedUser = responseAny.userData.user; + accessToken = responseAny.userData.access_token; + console.log('✅ Found user data in response.userData.user'); + } else if (responseAny.user) { + extractedUser = responseAny.user; + accessToken = responseAny.access_token; + console.log('✅ Found user data in response.user'); + } else { + console.log('🔍 User data structure:', responseAny.data); + } + } + + // Set the access token for future API requests + if (accessToken) { + this.apiService.setAccessToken(accessToken); + // Add token to user object for session storage + if (extractedUser) { + extractedUser.access_token = accessToken; + } + } + + console.log('📝 Final extracted user:', extractedUser ? { ...extractedUser, password: undefined, access_token: '[HIDDEN]' } : 'No user data'); + + return { + success: response.success, + user: extractedUser, + message: response.message + }; + } + + async login(phone: string, password: string): Promise<{ success: boolean; user?: User; message?: string }> { + console.log(`🔐 Attempting login for phone: ${phone}`); + + const response = await this.apiService.post('/telegram-auth/telegram-login', { + phone, + password + }); + + console.log('🔐 Login API full response:', JSON.stringify(response, null, 2)); + + // Extract user data and access token from the nested response structure + let userData = response.data; + let accessToken: string | undefined; + + // Check if the response structure is nested + if (response.success && response.data) { + const responseAny = response as any; + + console.log('🔍 [DEBUG] Raw response.data:', JSON.stringify(responseAny.data, null, 2)); + + if (responseAny.data?.user) { + userData = responseAny.data.user; + accessToken = responseAny.data.access_token; + console.log('✅ Found user data in response.data.user'); + console.log('🔍 [DEBUG] Extracted user data:', JSON.stringify(userData, null, 2)); + } else if (responseAny.userData?.user) { + userData = responseAny.userData.user; + accessToken = responseAny.userData.access_token; + console.log('✅ Found user data in response.userData.user'); + console.log('🔍 [DEBUG] Extracted user data:', JSON.stringify(userData, null, 2)); + } else if (responseAny.user) { + userData = responseAny.user; + accessToken = responseAny.access_token; + console.log('✅ Found user data in response.user'); + console.log('🔍 [DEBUG] Extracted user data:', JSON.stringify(userData, null, 2)); + } else { + console.log('🔍 Login response structure:', responseAny.data); + // Try to use response.data directly if it has user fields + if (responseAny.data && (responseAny.data.name || responseAny.data.email || responseAny.data.phone)) { + userData = responseAny.data; + accessToken = responseAny.data.access_token || responseAny.access_token; + console.log('✅ Using response.data directly as user data'); + console.log('🔍 [DEBUG] Direct user data:', JSON.stringify(userData, null, 2)); + } + } + } + + // Set the access token for future API requests + if (accessToken) { + this.apiService.setAccessToken(accessToken); + // Add token to user object for session storage + if (userData) { + userData.access_token = accessToken; + } + } + + console.log('🔐 Final user data:', userData ? { ...userData, password: undefined, access_token: '[HIDDEN]' } : 'No user data'); + + return { + success: response.success, + user: userData, + message: response.message + }; + } +} \ No newline at end of file diff --git a/src/features/properties/properties.handler.ts b/src/features/properties/properties.handler.ts new file mode 100644 index 0000000..318110e --- /dev/null +++ b/src/features/properties/properties.handler.ts @@ -0,0 +1,811 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { PropertiesService } from './properties.service'; +import { SessionService } from '../../shared/services/session.service'; +import { Property } from '../../shared/types'; + +export class PropertiesHandler { + constructor( + private bot: TelegramBot, + private propertiesService: PropertiesService, + private sessionService: SessionService + ) {} + + async handleBrowseProperties(chatId: number): Promise { + const result = await this.propertiesService.getProperties(); + + if (!result.success || !result.properties) { + await this.bot.sendMessage(chatId, + '❌ Failed to load properties. Please try again later.' + ); + return; + } + + if (result.properties.length === 0) { + await this.bot.sendMessage(chatId, + '📭 No properties available at the moment.' + ); + return; + } + + await this.bot.sendMessage(chatId, + `🏘️ Found ${result.properties.length} properties:` + ); + + for (const property of result.properties.slice(0, 10)) { // Show first 10 + await this.sendPropertyCard(chatId, property); + } + + if (result.properties.length > 10) { + await this.bot.sendMessage(chatId, + `... and ${result.properties.length - 10} more properties.` + ); + } + } + + async handleMyProperties(chatId: number): Promise { + const session = this.sessionService.getSession(chatId); + + if (!session.user) { + await this.bot.sendMessage(chatId, + '❌ Please login first.' + ); + return; + } + + const result = await this.propertiesService.getUserProperties(session.user.id); + + if (!result.success || !result.properties) { + await this.bot.sendMessage(chatId, + '❌ Failed to load your properties. Please try again later.' + ); + return; + } + + if (result.properties.length === 0) { + await this.bot.sendMessage(chatId, + '📭 You haven\'t added any properties yet.\n\n' + + 'Use "➕ Add Property" to create your first listing!' + ); + return; + } + + await this.bot.sendMessage(chatId, + `🏠 Your Properties (${result.properties.length}):` + ); + + for (const property of result.properties) { + await this.sendPropertyCard(chatId, property, true); + } + } + + async handleAddProperty(chatId: number): Promise { + this.sessionService.setSessionStep(chatId, 'ADD_PROPERTY_TITLE'); + await this.bot.sendMessage(chatId, + '➕ Let\'s add a new property!\n\n' + + 'Please enter the property title:', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handlePropertyTitle(chatId: number, title: string): Promise { + this.sessionService.setSessionStep(chatId, 'ADD_PROPERTY_DESCRIPTION', { title }); + await this.bot.sendMessage(chatId, + '✅ Title saved!\n\n' + + 'Please enter a description for the property:', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handlePropertyDescription(chatId: number, description: string): Promise { + this.sessionService.setSessionStep(chatId, 'ADD_PROPERTY_TYPE', { description }); + await this.bot.sendMessage(chatId, + '✅ Description saved!\n\n' + + 'Is this property for rent or sale?', + { + reply_markup: { + keyboard: [ + [{ text: '🏠 For Rent' }, { text: '💰 For Sale' }], + [{ text: '❌ Cancel' }] + ], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handlePropertyType(chatId: number, typeText: string): Promise { + console.log(`🔍 [DEBUG] handlePropertyType called with: "${typeText}"`); + let type: string; + + if (typeText === '🏠 For Rent') { + console.log(`✅ [DEBUG] Valid input: For Rent`); + type = 'RENT'; + } else if (typeText === '💰 For Sale') { + console.log(`✅ [DEBUG] Valid input: For Sale`); + type = 'SELL'; + } else { + console.log(`❌ [DEBUG] Invalid input: "${typeText}" - showing error message`); + // Invalid input - ask user to select from the buttons + await this.bot.sendMessage(chatId, + '❌ Please select either "🏠 For Rent" or "💰 For Sale" using the buttons below:', + { + reply_markup: { + keyboard: [ + [{ text: '🏠 For Rent' }, { text: '💰 For Sale' }], + [{ text: '❌ Cancel' }] + ], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + return; + } + + console.log(`✅ [DEBUG] Setting type to: ${type}`); + this.sessionService.setSessionStep(chatId, 'ADD_PROPERTY_PRICE', { type }); + + const priceLabel = type === 'RENT' ? 'monthly rent' : 'sale price'; + await this.bot.sendMessage(chatId, + `✅ Type saved!\n\n` + + `Please enter the ${priceLabel} (in ETB):`, + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handlePropertyPrice(chatId: number, priceText: string): Promise { + const price = parseFloat(priceText); + if (isNaN(price) || price <= 0) { + await this.bot.sendMessage(chatId, + '❌ Invalid price. Please enter a valid number:' + ); + return; + } + + this.sessionService.setSessionStep(chatId, 'ADD_PROPERTY_AREA', { price }); + await this.bot.sendMessage(chatId, + '✅ Price saved!\n\n' + + 'Please enter the area in square meters:', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handlePropertyArea(chatId: number, areaText: string): Promise { + const area = parseFloat(areaText); + if (isNaN(area) || area <= 0) { + await this.bot.sendMessage(chatId, + '❌ Invalid area. Please enter a valid number:' + ); + return; + } + + this.sessionService.setSessionStep(chatId, 'ADD_PROPERTY_ROOMS', { area }); + await this.bot.sendMessage(chatId, + '✅ Area saved!\n\n' + + 'How many rooms does the property have?', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handlePropertyRooms(chatId: number, roomsText: string): Promise { + const rooms = parseInt(roomsText); + if (isNaN(rooms) || rooms <= 0) { + await this.bot.sendMessage(chatId, + '❌ Invalid number of rooms. Please enter a valid number:' + ); + return; + } + + this.sessionService.setSessionStep(chatId, 'ADD_PROPERTY_TOILETS', { rooms }); + await this.bot.sendMessage(chatId, + '✅ Rooms saved!\n\n' + + 'How many toilets/bathrooms does the property have?', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handlePropertyToilets(chatId: number, toiletsText: string): Promise { + const toilets = parseInt(toiletsText); + if (isNaN(toilets) || toilets <= 0) { + await this.bot.sendMessage(chatId, + '❌ Invalid number of toilets. Please enter a valid number:' + ); + return; + } + + this.sessionService.setSessionStep(chatId, 'ADD_PROPERTY_SUBCITY', { toilets }); + await this.bot.sendMessage(chatId, + '✅ Toilets saved!\n\n' + + 'Which subcity is the property located in? (e.g., Bole, Kirkos, etc.)', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handlePropertySubcity(chatId: number, subcity: string): Promise { + this.sessionService.setSessionStep(chatId, 'ADD_PROPERTY_HOUSE_TYPE', { subcity }); + await this.bot.sendMessage(chatId, + '✅ Subcity saved!\n\n' + + 'What type of house is it?', + { + reply_markup: { + keyboard: [ + [{ text: '🏢 Apartment' }, { text: '🏠 Villa' }], + [{ text: '🏘️ Condominium' }, { text: '🏪 Commercial' }], + [{ text: '❌ Cancel' }] + ], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handlePropertyHouseType(chatId: number, houseType: string): Promise { + let cleanHouseType: string; + + // Only accept valid house type options + if (houseType === '🏢 Apartment') { + cleanHouseType = 'Apartment'; + } else if (houseType === '🏠 Villa') { + cleanHouseType = 'Villa'; + } else if (houseType === '🏘️ Condominium') { + cleanHouseType = 'Condominium'; + } else if (houseType === '🏪 Commercial') { + cleanHouseType = 'Commercial'; + } else { + // Invalid input - ask user to select from the buttons + await this.bot.sendMessage(chatId, + '❌ Please select a house type using the buttons below:', + { + reply_markup: { + keyboard: [ + [{ text: '🏢 Apartment' }, { text: '🏠 Villa' }], + [{ text: '🏘️ Condominium' }, { text: '🏪 Commercial' }], + [{ text: '❌ Cancel' }] + ], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + return; + } + + const session = this.sessionService.getSession(chatId); + const propertyData = session.data; + + const property: Omit = { + title: propertyData.title, + description: propertyData.description, + type: propertyData.type, + price: propertyData.price, + area: propertyData.area, + rooms: propertyData.rooms, + toilets: propertyData.toilets, + subcity: propertyData.subcity, + houseType: cleanHouseType, + status: 'DRAFT', + taxRate: 15, + isTaxable: false + }; + + const result = await this.propertiesService.createProperty(property); + + if (result.success && result.property) { + this.sessionService.setSessionStep(chatId, 'LOGGED_IN'); + await this.bot.sendMessage(chatId, + '🎉 Property added successfully!', + { reply_markup: { remove_keyboard: true } } + ); + await this.sendPropertyCard(chatId, result.property, true); + await this.showMainMenu(chatId); + } else { + await this.bot.sendMessage(chatId, + `❌ Failed to add property: ${result.message || 'Unknown error'}`, + { reply_markup: { remove_keyboard: true } } + ); + await this.showMainMenu(chatId); + } + } + + async handleCancelPropertyCreation(chatId: number): Promise { + console.log(`❌ User ${chatId} cancelled property creation`); + + // Clear any property creation data and return to main menu + this.sessionService.setSessionStep(chatId, 'LOGGED_IN'); + + await this.bot.sendMessage(chatId, + '❌ Property creation cancelled.\n\n' + + 'Returning to main menu...', + { reply_markup: { remove_keyboard: true } } + ); + + await this.showMainMenu(chatId); + } + + async handlePropertyCallback(callbackQuery: any): Promise { + const chatId = callbackQuery.message.chat.id; + const data = callbackQuery.data; + const messageId = callbackQuery.message.message_id; + + console.log(`🔘 Callback received: ${data} from user ${chatId}`); + + // Answer the callback query to remove loading state + await this.bot.answerCallbackQuery(callbackQuery.id); + + if (data.startsWith('edit_property_')) { + const propertyId = data.replace('edit_property_', ''); + await this.handleEditProperty(chatId, propertyId, messageId); + } else if (data.startsWith('delete_property_')) { + const propertyId = data.replace('delete_property_', ''); + await this.handleDeleteProperty(chatId, propertyId, messageId); + } else if (data.startsWith('confirm_delete_')) { + const propertyId = data.replace('confirm_delete_', ''); + await this.handleConfirmDelete(chatId, propertyId, messageId); + } else if (data.startsWith('cancel_delete_')) { + const propertyId = data.replace('cancel_delete_', ''); + await this.handleCancelDelete(chatId, propertyId, messageId); + } else if (data.startsWith('back_to_property_')) { + const propertyId = data.replace('back_to_property_', ''); + await this.handleBackToProperty(chatId, propertyId, messageId); + } else if (data.startsWith('edit_title_')) { + const propertyId = data.replace('edit_title_', ''); + await this.handleEditTitle(chatId, propertyId, messageId); + } else if (data.startsWith('edit_desc_')) { + const propertyId = data.replace('edit_desc_', ''); + await this.handleEditDescription(chatId, propertyId, messageId); + } else if (data.startsWith('edit_price_')) { + const propertyId = data.replace('edit_price_', ''); + await this.handleEditPrice(chatId, propertyId, messageId); + } else if (data.startsWith('edit_area_')) { + const propertyId = data.replace('edit_area_', ''); + await this.handleEditArea(chatId, propertyId, messageId); + } else if (data.startsWith('edit_rooms_')) { + const propertyId = data.replace('edit_rooms_', ''); + await this.handleEditRooms(chatId, propertyId, messageId); + } else if (data.startsWith('edit_toilets_')) { + const propertyId = data.replace('edit_toilets_', ''); + await this.handleEditToilets(chatId, propertyId, messageId); + } else if (data.startsWith('edit_location_')) { + const propertyId = data.replace('edit_location_', ''); + await this.handleEditLocation(chatId, propertyId, messageId); + } else if (data.startsWith('edit_house_type_')) { + const propertyId = data.replace('edit_house_type_', ''); + await this.handleEditHouseType(chatId, propertyId, messageId); + } + } + + async handleEditProperty(chatId: number, propertyId: string, messageId: number): Promise { + await this.bot.editMessageReplyMarkup({ + inline_keyboard: [ + [ + { text: '📝 Edit Title', callback_data: `edit_title_${propertyId}` }, + { text: '📄 Edit Description', callback_data: `edit_desc_${propertyId}` } + ], + [ + { text: '💰 Edit Price', callback_data: `edit_price_${propertyId}` }, + { text: '📐 Edit Area', callback_data: `edit_area_${propertyId}` } + ], + [ + { text: '🛏️ Edit Rooms', callback_data: `edit_rooms_${propertyId}` }, + { text: '🚿 Edit Toilets', callback_data: `edit_toilets_${propertyId}` } + ], + [ + { text: '📍 Edit Location', callback_data: `edit_location_${propertyId}` }, + { text: '🏠 Edit House Type', callback_data: `edit_house_type_${propertyId}` } + ], + [ + { text: '⬅️ Back', callback_data: `back_to_property_${propertyId}` } + ] + ] + }, { chat_id: chatId, message_id: messageId }); + + await this.bot.sendMessage(chatId, + '✏️ Select what you want to edit:\n\n' + + '💡 Tip: Click on the option you want to modify.' + ); + } + + async handleDeleteProperty(chatId: number, propertyId: string, messageId: number): Promise { + await this.bot.editMessageReplyMarkup({ + inline_keyboard: [ + [ + { text: '⚠️ Yes, Delete', callback_data: `confirm_delete_${propertyId}` }, + { text: '❌ Cancel', callback_data: `cancel_delete_${propertyId}` } + ] + ] + }, { chat_id: chatId, message_id: messageId }); + + await this.bot.sendMessage(chatId, + '⚠️ Are you sure you want to delete this property?\n\n' + + '🚨 This action cannot be undone!' + ); + } + + async handleConfirmDelete(chatId: number, propertyId: string, messageId: number): Promise { + try { + const result = await this.propertiesService.deleteProperty(propertyId); + + if (result.success) { + await this.bot.sendMessage(chatId, + '✅ Property deleted successfully!' + ); + + // Delete the property message + await this.bot.deleteMessage(chatId, messageId); + } else { + await this.bot.sendMessage(chatId, + `❌ Failed to delete property: ${result.message || 'Unknown error'}` + ); + } + } catch (error) { + console.error('Error deleting property:', error); + await this.bot.sendMessage(chatId, + '❌ An error occurred while deleting the property.' + ); + } + } + + async handleCancelDelete(chatId: number, propertyId: string, messageId: number): Promise { + // Get the property details to restore the original buttons + try { + const result = await this.propertiesService.getPropertyById(propertyId); + + if (result.success && result.property) { + await this.bot.editMessageReplyMarkup({ + inline_keyboard: [ + [ + { text: '✏️ Edit', callback_data: `edit_property_${propertyId}` }, + { text: '🗑️ Delete', callback_data: `delete_property_${propertyId}` } + ] + ] + }, { chat_id: chatId, message_id: messageId }); + + await this.bot.sendMessage(chatId, '❌ Delete cancelled.'); + } + } catch (error) { + console.error('Error cancelling delete:', error); + await this.bot.sendMessage(chatId, '❌ An error occurred.'); + } + } + + async handleBackToProperty(chatId: number, propertyId: string, messageId: number): Promise { + try { + const result = await this.propertiesService.getPropertyById(propertyId); + + if (result.success && result.property) { + await this.bot.editMessageReplyMarkup({ + inline_keyboard: [ + [ + { text: '✏️ Edit', callback_data: `edit_property_${propertyId}` }, + { text: '🗑️ Delete', callback_data: `delete_property_${propertyId}` } + ] + ] + }, { chat_id: chatId, message_id: messageId }); + + await this.bot.sendMessage(chatId, '⬅️ Back to property options.'); + } + } catch (error) { + console.error('Error going back to property:', error); + await this.bot.sendMessage(chatId, '❌ An error occurred.'); + } + } + + async handleEditTitle(chatId: number, propertyId: string, messageId: number): Promise { + this.sessionService.setSessionStep(chatId, 'EDIT_PROPERTY_TITLE', { propertyId, messageId }); + await this.bot.sendMessage(chatId, + '📝 Enter the new title for the property:', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel Edit' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handleEditDescription(chatId: number, propertyId: string, messageId: number): Promise { + this.sessionService.setSessionStep(chatId, 'EDIT_PROPERTY_DESCRIPTION', { propertyId, messageId }); + await this.bot.sendMessage(chatId, + '📄 Enter the new description for the property:', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel Edit' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handleEditPrice(chatId: number, propertyId: string, messageId: number): Promise { + this.sessionService.setSessionStep(chatId, 'EDIT_PROPERTY_PRICE', { propertyId, messageId }); + await this.bot.sendMessage(chatId, + '💰 Enter the new price (in ETB):', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel Edit' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handleEditArea(chatId: number, propertyId: string, messageId: number): Promise { + this.sessionService.setSessionStep(chatId, 'EDIT_PROPERTY_AREA', { propertyId, messageId }); + await this.bot.sendMessage(chatId, + '📐 Enter the new area (in square meters):', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel Edit' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handleEditRooms(chatId: number, propertyId: string, messageId: number): Promise { + this.sessionService.setSessionStep(chatId, 'EDIT_PROPERTY_ROOMS', { propertyId, messageId }); + await this.bot.sendMessage(chatId, + '🛏️ Enter the new number of rooms:', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel Edit' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handleEditToilets(chatId: number, propertyId: string, messageId: number): Promise { + this.sessionService.setSessionStep(chatId, 'EDIT_PROPERTY_TOILETS', { propertyId, messageId }); + await this.bot.sendMessage(chatId, + '🚿 Enter the new number of toilets:', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel Edit' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handleEditLocation(chatId: number, propertyId: string, messageId: number): Promise { + this.sessionService.setSessionStep(chatId, 'EDIT_PROPERTY_LOCATION', { propertyId, messageId }); + await this.bot.sendMessage(chatId, + '📍 Enter the new location (subcity):', + { + reply_markup: { + keyboard: [[{ text: '❌ Cancel Edit' }]], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handleEditHouseType(chatId: number, propertyId: string, messageId: number): Promise { + this.sessionService.setSessionStep(chatId, 'EDIT_PROPERTY_HOUSE_TYPE', { propertyId, messageId }); + await this.bot.sendMessage(chatId, + '🏠 Select the new house type:', + { + reply_markup: { + keyboard: [ + [{ text: '🏢 Apartment' }, { text: '🏠 Villa' }], + [{ text: '🏘️ Condominium' }, { text: '🏪 Commercial' }], + [{ text: '❌ Cancel Edit' }] + ], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + } + + async handleEditPropertyValue(chatId: number, field: string, value: string): Promise { + const session = this.sessionService.getSession(chatId); + const { propertyId, messageId } = session.data; + + try { + let updateData: any = {}; + let processedValue: any = value; + + // Process the value based on the field type + switch (field) { + case 'title': + case 'description': + case 'subcity': + updateData[field] = value; + break; + case 'price': + case 'area': + processedValue = parseFloat(value); + if (isNaN(processedValue) || processedValue <= 0) { + await this.bot.sendMessage(chatId, + '❌ Invalid number. Please enter a valid positive number:' + ); + return; + } + updateData[field] = processedValue; + break; + case 'rooms': + case 'toilets': + processedValue = parseInt(value); + if (isNaN(processedValue) || processedValue <= 0) { + await this.bot.sendMessage(chatId, + '❌ Invalid number. Please enter a valid positive integer:' + ); + return; + } + updateData[field] = processedValue; + break; + case 'houseType': + // Validate house type + if (value === '🏢 Apartment') { + updateData.houseType = 'Apartment'; + } else if (value === '🏠 Villa') { + updateData.houseType = 'Villa'; + } else if (value === '🏘️ Condominium') { + updateData.houseType = 'Condominium'; + } else if (value === '🏪 Commercial') { + updateData.houseType = 'Commercial'; + } else { + await this.bot.sendMessage(chatId, + '❌ Please select a house type using the buttons below:', + { + reply_markup: { + keyboard: [ + [{ text: '🏢 Apartment' }, { text: '🏠 Villa' }], + [{ text: '🏘️ Condominium' }, { text: '🏪 Commercial' }], + [{ text: '❌ Cancel Edit' }] + ], + resize_keyboard: true, + one_time_keyboard: true + } + } + ); + return; + } + break; + } + + // Update the property + const result = await this.propertiesService.updateProperty(propertyId, updateData); + + if (result.success && result.property) { + this.sessionService.setSessionStep(chatId, 'LOGGED_IN'); + await this.bot.sendMessage(chatId, + `✅ Property ${field} updated successfully!`, + { reply_markup: { remove_keyboard: true } } + ); + + // Show the updated property + await this.sendPropertyCard(chatId, result.property, true); + await this.showMainMenu(chatId); + } else { + await this.bot.sendMessage(chatId, + `❌ Failed to update property: ${result.message || 'Unknown error'}`, + { reply_markup: { remove_keyboard: true } } + ); + await this.showMainMenu(chatId); + } + } catch (error) { + console.error('Error updating property:', error); + await this.bot.sendMessage(chatId, + '❌ An error occurred while updating the property.', + { reply_markup: { remove_keyboard: true } } + ); + await this.showMainMenu(chatId); + } + } + + async handleCancelEdit(chatId: number): Promise { + this.sessionService.setSessionStep(chatId, 'LOGGED_IN'); + await this.bot.sendMessage(chatId, + '❌ Edit cancelled.', + { reply_markup: { remove_keyboard: true } } + ); + await this.showMainMenu(chatId); + } + + private async sendPropertyCard(chatId: number, property: Property, isOwner = false): Promise { + console.log(`🔍 [DEBUG] sendPropertyCard - isOwner: ${isOwner}, property.id: ${property.id}`); + console.log(`🔍 [DEBUG] Property object:`, JSON.stringify(property, null, 2)); + + const typeEmoji = property.type === 'RENT' ? '🏠' : '💰'; + const statusEmoji = property.status === 'PUBLISHED' ? '✅' : '📝'; + + const message = + `${typeEmoji} ${property.title}\n` + + `${statusEmoji} Status: ${property.status}\n\n` + + `📝 ${property.description}\n\n` + + `💵 Price: ${property.price.toLocaleString()} ETB ${property.type === 'RENT' ? '/month' : ''}\n` + + `📐 Area: ${property.area} m²\n` + + `🛏️ Rooms: ${property.rooms}\n` + + `🚿 Toilets: ${property.toilets}\n` + + `📍 Location: ${property.subcity}\n` + + `🏠 Type: ${property.houseType}`; + + if (isOwner && property.id) { + console.log(`✅ [DEBUG] Adding inline buttons for property ${property.id}`); + // Add action buttons for property owner (removed publish button) + await this.bot.sendMessage(chatId, message, { + reply_markup: { + inline_keyboard: [ + [ + { text: '✏️ Edit', callback_data: `edit_property_${property.id}` }, + { text: '🗑️ Delete', callback_data: `delete_property_${property.id}` } + ] + ] + } + }); + } else { + console.log(`❌ [DEBUG] No buttons - isOwner: ${isOwner}, property.id: ${property.id}`); + await this.bot.sendMessage(chatId, message); + } + } + + private async showMainMenu(chatId: number): Promise { + await this.bot.sendMessage(chatId, + 'What would you like to do next?', + { + reply_markup: { + keyboard: [ + [{ text: '🏘️ Browse Properties' }, { text: '🏠 My Properties' }], + [{ text: '➕ Add Property' }, { text: '👤 Profile' }], + [{ text: '🚪 Logout' }] + ], + resize_keyboard: true + } + } + ); + } +} \ No newline at end of file diff --git a/src/features/properties/properties.service.ts b/src/features/properties/properties.service.ts new file mode 100644 index 0000000..0bd81b9 --- /dev/null +++ b/src/features/properties/properties.service.ts @@ -0,0 +1,78 @@ +import { ApiService } from '../../shared/services/api.service'; +import { Property } from '../../shared/types'; + +export class PropertiesService { + constructor(private apiService: ApiService) {} + + async getProperties(): Promise<{ success: boolean; properties?: Property[]; message?: string }> { + const response = await this.apiService.get('/listings'); + return { + success: response.success, + properties: response.data, + message: response.message + }; + } + + async getUserProperties(userId: string): Promise<{ success: boolean; properties?: Property[]; message?: string }> { + // Use the correct API endpoint for user's properties + const response = await this.apiService.get('/listings/my-listings'); + return { + success: response.success, + properties: response.data, + message: response.message + }; + } + + async createProperty(property: Omit): Promise<{ success: boolean; property?: Property; message?: string }> { + const response = await this.apiService.post('/listings', property); + return { + success: response.success, + property: response.data, + message: response.message + }; + } + + async getPropertyById(id: string): Promise<{ success: boolean; property?: Property; message?: string }> { + const response = await this.apiService.get(`/listings/${id}`); + return { + success: response.success, + property: response.data, + message: response.message + }; + } + + async updateProperty(id: string, property: Partial): Promise<{ success: boolean; property?: Property; message?: string }> { + const response = await this.apiService.patch(`/listings/${id}`, property); + return { + success: response.success, + property: response.data, + message: response.message + }; + } + + async deleteProperty(id: string): Promise<{ success: boolean; message?: string }> { + const response = await this.apiService.delete(`/listings/${id}`); + return { + success: response.success, + message: response.message + }; + } + + async togglePropertyStatus(id: string): Promise<{ success: boolean; property?: Property; message?: string }> { + // First get the current property to know its current status + const currentProperty = await this.getPropertyById(id); + + if (!currentProperty.success || !currentProperty.property) { + return { + success: false, + message: 'Property not found' + }; + } + + // Toggle the status + const newStatus = currentProperty.property.status === 'PUBLISHED' ? 'DRAFT' : 'PUBLISHED'; + + // Update the property with new status + return await this.updateProperty(id, { status: newStatus }); + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1bf3b18 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,30 @@ +import dotenv from 'dotenv'; +import { YaltopiaBot } from './bot/bot'; + +// Load environment variables +dotenv.config(); + +const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; +const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000/api'; + +if (!TELEGRAM_BOT_TOKEN) { + console.error('❌ TELEGRAM_BOT_TOKEN is required in environment variables'); + process.exit(1); +} + +// Create and start the bot +const bot = new YaltopiaBot(TELEGRAM_BOT_TOKEN, API_BASE_URL); +bot.start(); + +// Graceful shutdown +process.on('SIGINT', () => { + console.log('\n🛑 Shutting down bot...'); + bot.stop(); + process.exit(0); +}); + +process.on('SIGTERM', () => { + console.log('\n🛑 Shutting down bot...'); + bot.stop(); + process.exit(0); +}); \ No newline at end of file diff --git a/src/shared/monitoring/admin-notifier.ts b/src/shared/monitoring/admin-notifier.ts new file mode 100644 index 0000000..cf664a4 --- /dev/null +++ b/src/shared/monitoring/admin-notifier.ts @@ -0,0 +1,287 @@ +import TelegramBot from 'node-telegram-bot-api'; +import axios from 'axios'; + +export interface AdminConfig { + adminChatIds: number[]; + webhookUrl?: string; + emailConfig?: { + host: string; + port: number; + user: string; + pass: string; + to: string[]; + }; +} + +export class AdminNotifier { + private bot: TelegramBot; + private config: AdminConfig; + + constructor(bot: TelegramBot, config: AdminConfig) { + this.bot = bot; + this.config = config; + } + + async sendAlert(level: 'critical' | 'warning' | 'info', title: string, message: string, data?: any): Promise { + const emoji = this.getEmojiForLevel(level); + const formattedMessage = this.formatMessage(level, title, message, data); + + // Send to admin chat(s) + for (const adminChatId of this.config.adminChatIds) { + try { + await this.bot.sendMessage(adminChatId, formattedMessage, { + parse_mode: 'Markdown', + disable_web_page_preview: true + }); + } catch (error) { + console.error(`Failed to send alert to admin ${adminChatId}:`, error); + } + } + + // Send to webhook if configured + if (this.config.webhookUrl) { + await this.sendWebhookAlert(level, title, message, data); + } + + // Send email if configured (would need email library) + if (this.config.emailConfig) { + await this.sendEmailAlert(level, title, message, data); + } + } + + async sendSystemReport(report: string): Promise { + const chunks = this.splitMessage(report, 4000); // Telegram message limit + + for (const adminChatId of this.config.adminChatIds) { + try { + for (const chunk of chunks) { + await this.bot.sendMessage(adminChatId, chunk, { + parse_mode: 'Markdown', + disable_web_page_preview: true + }); + } + } catch (error) { + console.error(`Failed to send report to admin ${adminChatId}:`, error); + } + } + } + + async sendErrorSummary(errors: Array<{ timestamp: Date; error: string; userId?: number; context?: string }>): Promise { + if (errors.length === 0) return; + + const summary = this.formatErrorSummary(errors); + await this.sendAlert('warning', 'Error Summary', summary); + } + + async sendUserActivityAlert(userId: number, activity: string, suspicious: boolean = false): Promise { + const level = suspicious ? 'warning' : 'info'; + const title = suspicious ? 'Suspicious User Activity' : 'User Activity Alert'; + const message = `User ${userId} performed: ${activity}`; + + await this.sendAlert(level, title, message, { userId, activity, suspicious }); + } + + async sendSystemHealthAlert(status: 'healthy' | 'warning' | 'critical', issues: string[]): Promise { + if (status === 'healthy') return; + + const level = status === 'critical' ? 'critical' : 'warning'; + const title = `System Health: ${status.toUpperCase()}`; + const message = issues.length > 0 ? issues.join('\n') : 'System health check completed'; + + await this.sendAlert(level, title, message, { status, issues }); + } + + private getEmojiForLevel(level: string): string { + switch (level) { + case 'critical': return '🚨'; + case 'warning': return '⚠️'; + case 'info': return 'ℹ️'; + default: return '📢'; + } + } + + private formatMessage(level: string, title: string, message: string, data?: any): string { + const emoji = this.getEmojiForLevel(level); + const timestamp = new Date().toISOString(); + + let formatted = `${emoji} **${title}**\n\n`; + formatted += `📅 ${timestamp}\n`; + formatted += `🔍 Level: ${level.toUpperCase()}\n\n`; + formatted += `📝 ${message}\n`; + + if (data) { + formatted += `\n📊 **Details:**\n`; + formatted += '```json\n'; + formatted += JSON.stringify(data, null, 2); + formatted += '\n```'; + } + + return formatted; + } + + private formatErrorSummary(errors: Array<{ timestamp: Date; error: string; userId?: number; context?: string }>): string { + const recentErrors = errors.slice(0, 10); // Show last 10 errors + + let summary = `📊 **Error Summary** (${errors.length} total errors)\n\n`; + + for (const error of recentErrors) { + summary += `🕐 ${error.timestamp.toISOString()}\n`; + summary += `👤 User: ${error.userId || 'System'}\n`; + summary += `❌ ${error.error}\n`; + if (error.context) { + summary += `📝 Context: ${error.context}\n`; + } + summary += '\n'; + } + + if (errors.length > 10) { + summary += `... and ${errors.length - 10} more errors\n`; + } + + return summary; + } + + private splitMessage(message: string, maxLength: number): string[] { + if (message.length <= maxLength) { + return [message]; + } + + const chunks: string[] = []; + let currentChunk = ''; + + const lines = message.split('\n'); + + for (const line of lines) { + if (currentChunk.length + line.length + 1 > maxLength) { + if (currentChunk) { + chunks.push(currentChunk); + currentChunk = ''; + } + + // If single line is too long, split it + if (line.length > maxLength) { + const words = line.split(' '); + let currentLine = ''; + + for (const word of words) { + if (currentLine.length + word.length + 1 > maxLength) { + if (currentLine) { + chunks.push(currentLine); + currentLine = ''; + } + currentLine = word; + } else { + currentLine += (currentLine ? ' ' : '') + word; + } + } + + if (currentLine) { + currentChunk = currentLine; + } + } else { + currentChunk = line; + } + } else { + currentChunk += (currentChunk ? '\n' : '') + line; + } + } + + if (currentChunk) { + chunks.push(currentChunk); + } + + return chunks; + } + + private async sendWebhookAlert(level: string, title: string, message: string, data?: any): Promise { + if (!this.config.webhookUrl) return; + + try { + const payload = { + level, + title, + message, + data, + timestamp: new Date().toISOString(), + bot: 'Yaltopia Bot' + }; + + await axios.post(this.config.webhookUrl, payload, { + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('Failed to send webhook alert:', error); + } + } + + private async sendEmailAlert(level: string, title: string, message: string, data?: any): Promise { + // Email implementation would go here + // You'd need to install nodemailer: npm install nodemailer @types/nodemailer + console.log('Email alert would be sent here:', { level, title, message, data }); + } + + // Admin Commands Handler + async handleAdminCommand(chatId: number, command: string, args: string[]): Promise { + if (!this.config.adminChatIds.includes(chatId)) { + await this.bot.sendMessage(chatId, '❌ Unauthorized access'); + return; + } + + switch (command) { + case '/status': + await this.handleStatusCommand(chatId); + break; + case '/metrics': + await this.handleMetricsCommand(chatId); + break; + case '/errors': + await this.handleErrorsCommand(chatId, args); + break; + case '/users': + await this.handleUsersCommand(chatId, args); + break; + case '/restart': + await this.handleRestartCommand(chatId); + break; + default: + await this.bot.sendMessage(chatId, + '🤖 **Admin Commands:**\n\n' + + '/status - System health status\n' + + '/metrics - Current metrics\n' + + '/errors [count] - Recent errors\n' + + '/users [count] - Recent user activity\n' + + '/restart - Restart bot (with confirmation)' + ); + } + } + + private async handleStatusCommand(chatId: number): Promise { + // This would integrate with SystemMonitor + await this.bot.sendMessage(chatId, '✅ System status check would be displayed here'); + } + + private async handleMetricsCommand(chatId: number): Promise { + // This would integrate with SystemMonitor + await this.bot.sendMessage(chatId, '📊 System metrics would be displayed here'); + } + + private async handleErrorsCommand(chatId: number, args: string[]): Promise { + const count = args[0] ? parseInt(args[0]) : 10; + await this.bot.sendMessage(chatId, `📋 Last ${count} errors would be displayed here`); + } + + private async handleUsersCommand(chatId: number, args: string[]): Promise { + const count = args[0] ? parseInt(args[0]) : 20; + await this.bot.sendMessage(chatId, `👥 Last ${count} user activities would be displayed here`); + } + + private async handleRestartCommand(chatId: number): Promise { + await this.bot.sendMessage(chatId, + '⚠️ **Restart Confirmation Required**\n\n' + + 'Are you sure you want to restart the bot?\n' + + 'Reply with "CONFIRM RESTART" to proceed.' + ); + } +} \ No newline at end of file diff --git a/src/shared/monitoring/logger.ts b/src/shared/monitoring/logger.ts new file mode 100644 index 0000000..cf9040b --- /dev/null +++ b/src/shared/monitoring/logger.ts @@ -0,0 +1,250 @@ +import fs from 'fs'; +import path from 'path'; + +export enum LogLevel { + ERROR = 0, + WARN = 1, + INFO = 2, + DEBUG = 3 +} + +export interface LogEntry { + timestamp: Date; + level: LogLevel; + message: string; + userId?: number; + context?: any; + stack?: string; +} + +export class Logger { + private logLevel: LogLevel; + private logDir: string; + private maxFileSize: number; + private maxFiles: number; + + constructor( + logLevel: LogLevel = LogLevel.INFO, + logDir: string = './logs', + maxFileSize: number = 10 * 1024 * 1024, // 10MB + maxFiles: number = 5 + ) { + this.logLevel = logLevel; + this.logDir = logDir; + this.maxFileSize = maxFileSize; + this.maxFiles = maxFiles; + + // Create logs directory if it doesn't exist + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + } + + private shouldLog(level: LogLevel): boolean { + return level <= this.logLevel; + } + + private formatLogEntry(entry: LogEntry): string { + const timestamp = entry.timestamp.toISOString(); + const level = LogLevel[entry.level]; + const userId = entry.userId ? `[User:${entry.userId}]` : ''; + const context = entry.context ? `[${JSON.stringify(entry.context)}]` : ''; + + let logLine = `${timestamp} [${level}] ${userId} ${entry.message} ${context}`; + + if (entry.stack) { + logLine += `\nStack: ${entry.stack}`; + } + + return logLine; + } + + private writeToFile(entry: LogEntry): void { + const filename = `bot-${new Date().toISOString().split('T')[0]}.log`; + const filepath = path.join(this.logDir, filename); + + try { + const logLine = this.formatLogEntry(entry) + '\n'; + + // Check file size and rotate if necessary + if (fs.existsSync(filepath)) { + const stats = fs.statSync(filepath); + if (stats.size > this.maxFileSize) { + this.rotateLogFile(filepath); + } + } + + fs.appendFileSync(filepath, logLine); + } catch (error) { + console.error('Failed to write to log file:', error); + } + } + + private rotateLogFile(filepath: string): void { + const dir = path.dirname(filepath); + const basename = path.basename(filepath, '.log'); + + // Rotate existing files + for (let i = this.maxFiles - 1; i > 0; i--) { + const oldFile = path.join(dir, `${basename}.${i}.log`); + const newFile = path.join(dir, `${basename}.${i + 1}.log`); + + if (fs.existsSync(oldFile)) { + if (i === this.maxFiles - 1) { + fs.unlinkSync(oldFile); // Delete oldest file + } else { + fs.renameSync(oldFile, newFile); + } + } + } + + // Move current file to .1 + const rotatedFile = path.join(dir, `${basename}.1.log`); + fs.renameSync(filepath, rotatedFile); + } + + error(message: string, userId?: number, context?: any, error?: Error): void { + if (!this.shouldLog(LogLevel.ERROR)) return; + + const entry: LogEntry = { + timestamp: new Date(), + level: LogLevel.ERROR, + message, + userId, + context, + stack: error?.stack + }; + + console.error(`🚨 ${this.formatLogEntry(entry)}`); + this.writeToFile(entry); + } + + warn(message: string, userId?: number, context?: any): void { + if (!this.shouldLog(LogLevel.WARN)) return; + + const entry: LogEntry = { + timestamp: new Date(), + level: LogLevel.WARN, + message, + userId, + context + }; + + console.warn(`⚠️ ${this.formatLogEntry(entry)}`); + this.writeToFile(entry); + } + + info(message: string, userId?: number, context?: any): void { + if (!this.shouldLog(LogLevel.INFO)) return; + + const entry: LogEntry = { + timestamp: new Date(), + level: LogLevel.INFO, + message, + userId, + context + }; + + console.log(`ℹ️ ${this.formatLogEntry(entry)}`); + this.writeToFile(entry); + } + + debug(message: string, userId?: number, context?: any): void { + if (!this.shouldLog(LogLevel.DEBUG)) return; + + const entry: LogEntry = { + timestamp: new Date(), + level: LogLevel.DEBUG, + message, + userId, + context + }; + + console.log(`🐛 ${this.formatLogEntry(entry)}`); + this.writeToFile(entry); + } + + // Structured logging methods + userAction(userId: number, action: string, success: boolean, details?: any): void { + this.info(`User action: ${action}`, userId, { success, ...details }); + } + + apiCall(method: string, endpoint: string, status: number, duration: number, userId?: number): void { + this.info(`API call: ${method} ${endpoint}`, userId, { status, duration }); + } + + securityEvent(event: string, userId?: number, details?: any): void { + this.warn(`Security event: ${event}`, userId, details); + } + + systemEvent(event: string, details?: any): void { + this.info(`System event: ${event}`, undefined, details); + } + + // Get recent logs + getRecentLogs(hours: number = 24, level?: LogLevel): LogEntry[] { + const logs: LogEntry[] = []; + const cutoffTime = new Date(Date.now() - hours * 60 * 60 * 1000); + + try { + const files = fs.readdirSync(this.logDir) + .filter(f => f.endsWith('.log')) + .sort() + .reverse(); // Most recent first + + for (const file of files) { + const filepath = path.join(this.logDir, file); + const content = fs.readFileSync(filepath, 'utf8'); + const lines = content.split('\n').filter(line => line.trim()); + + for (const line of lines) { + try { + const match = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z) \[(\w+)\]/); + if (match) { + const timestamp = new Date(match[1]); + if (timestamp < cutoffTime) continue; + + const logLevel = LogLevel[match[2] as keyof typeof LogLevel]; + if (level !== undefined && logLevel !== level) continue; + + // Parse the rest of the log entry (simplified) + logs.push({ + timestamp, + level: logLevel, + message: line.substring(match[0].length).trim() + }); + } + } catch (parseError) { + // Skip malformed log lines + } + } + } + } catch (error) { + this.error('Failed to read log files', undefined, { error: error instanceof Error ? error.message : String(error) }); + } + + return logs.slice(0, 1000); // Limit to 1000 entries + } + + // Clean old log files + cleanOldLogs(daysToKeep: number = 30): void { + try { + const files = fs.readdirSync(this.logDir); + const cutoffTime = Date.now() - daysToKeep * 24 * 60 * 60 * 1000; + + for (const file of files) { + if (!file.endsWith('.log')) continue; + + const filepath = path.join(this.logDir, file); + const stats = fs.statSync(filepath); + + if (stats.mtime.getTime() < cutoffTime) { + fs.unlinkSync(filepath); + this.info(`Cleaned old log file: ${file}`); + } + } + } catch (error) { + this.error('Failed to clean old logs', undefined, { error: error instanceof Error ? error.message : String(error) }); + } + } +} \ No newline at end of file diff --git a/src/shared/monitoring/system-monitor.ts b/src/shared/monitoring/system-monitor.ts new file mode 100644 index 0000000..29d5cb9 --- /dev/null +++ b/src/shared/monitoring/system-monitor.ts @@ -0,0 +1,312 @@ +import { EventEmitter } from 'events'; +import axios from 'axios'; + +export interface SystemMetrics { + totalUsers: number; + activeUsers: number; + totalMessages: number; + errorCount: number; + apiCalls: number; + successfulLogins: number; + failedLogins: number; + registrations: number; + propertiesCreated: number; + uptime: number; + memoryUsage: NodeJS.MemoryUsage; + lastError?: { + timestamp: Date; + error: string; + userId?: number; + context?: string; + }; +} + +export interface AlertConfig { + adminChatId?: number; + webhookUrl?: string; + emailConfig?: { + host: string; + port: number; + user: string; + pass: string; + to: string[]; + }; +} + +export class SystemMonitor extends EventEmitter { + private metrics: SystemMetrics; + private startTime: Date; + private alertConfig: AlertConfig; + private activeUsers = new Set(); + private recentErrors: Array<{ timestamp: Date; error: string; userId?: number; context?: string }> = []; + + constructor(alertConfig: AlertConfig = {}) { + super(); + this.startTime = new Date(); + this.alertConfig = alertConfig; + this.metrics = { + totalUsers: 0, + activeUsers: 0, + totalMessages: 0, + errorCount: 0, + apiCalls: 0, + successfulLogins: 0, + failedLogins: 0, + registrations: 0, + propertiesCreated: 0, + uptime: 0, + memoryUsage: process.memoryUsage() + }; + + // Update metrics every minute + setInterval(() => { + this.updateMetrics(); + }, 60000); + + // Clean old errors every hour + setInterval(() => { + this.cleanOldErrors(); + }, 3600000); + } + + // User Activity Tracking + trackUserActivity(userId: number, action: string, success: boolean = true, context?: any): void { + this.activeUsers.add(userId); + this.metrics.totalMessages++; + + const logEntry = { + timestamp: new Date(), + userId, + action, + success, + context: context ? JSON.stringify(context) : undefined + }; + + console.log(`📊 [MONITOR] User:${userId} Action:${action} Success:${success}`, context || ''); + + // Track specific actions + switch (action) { + case 'login': + if (success) { + this.metrics.successfulLogins++; + } else { + this.metrics.failedLogins++; + this.trackError(`Login failed for user ${userId}`, userId, 'authentication'); + } + break; + case 'registration': + if (success) { + this.metrics.registrations++; + this.metrics.totalUsers++; + } + break; + case 'property_created': + if (success) { + this.metrics.propertiesCreated++; + } + break; + } + + this.emit('userActivity', logEntry); + } + + // API Call Tracking + trackApiCall(endpoint: string, method: string, status: number, duration: number): void { + this.metrics.apiCalls++; + + const logEntry = { + timestamp: new Date(), + endpoint, + method, + status, + duration + }; + + console.log(`🌐 [API] ${method} ${endpoint} - ${status} (${duration}ms)`); + + if (status >= 400) { + this.trackError(`API Error: ${method} ${endpoint} returned ${status}`, undefined, 'api'); + } + + this.emit('apiCall', logEntry); + } + + // Error Tracking + trackError(error: string, userId?: number, context?: string): void { + this.metrics.errorCount++; + + const errorEntry = { + timestamp: new Date(), + error, + userId, + context + }; + + this.recentErrors.push(errorEntry); + this.metrics.lastError = errorEntry; + + console.error(`🚨 [ERROR] ${error}`, { userId, context }); + + // Send alert for critical errors + this.sendAlert('error', `🚨 Bot Error: ${error}`, { + userId, + context, + timestamp: new Date().toISOString() + }); + + this.emit('error', errorEntry); + } + + // System Health Check + getHealthStatus(): { status: 'healthy' | 'warning' | 'critical'; issues: string[] } { + const issues: string[] = []; + let status: 'healthy' | 'warning' | 'critical' = 'healthy'; + + // Check error rate (last hour) + const recentErrorCount = this.recentErrors.filter( + e => Date.now() - e.timestamp.getTime() < 3600000 + ).length; + + if (recentErrorCount > 50) { + status = 'critical'; + issues.push(`High error rate: ${recentErrorCount} errors in last hour`); + } else if (recentErrorCount > 10) { + status = 'warning'; + issues.push(`Elevated error rate: ${recentErrorCount} errors in last hour`); + } + + // Check memory usage + const memUsage = process.memoryUsage(); + const memUsageMB = memUsage.heapUsed / 1024 / 1024; + + if (memUsageMB > 500) { + status = status === 'critical' ? 'critical' : 'warning'; + issues.push(`High memory usage: ${memUsageMB.toFixed(2)}MB`); + } + + // Check failed login rate + const failedLoginRate = this.metrics.failedLogins / Math.max(this.metrics.successfulLogins, 1); + if (failedLoginRate > 0.5) { + status = status === 'critical' ? 'critical' : 'warning'; + issues.push(`High failed login rate: ${(failedLoginRate * 100).toFixed(1)}%`); + } + + return { status, issues }; + } + + // Get Current Metrics + getMetrics(): SystemMetrics { + this.updateMetrics(); + return { ...this.metrics }; + } + + // Generate Report + generateReport(): string { + const health = this.getHealthStatus(); + const metrics = this.getMetrics(); + + return ` +🤖 **Yaltopia Bot System Report** +📅 Generated: ${new Date().toISOString()} + +**System Health: ${health.status.toUpperCase()}** +${health.issues.length > 0 ? '⚠️ Issues:\n' + health.issues.map(i => `- ${i}`).join('\n') : '✅ All systems operational'} + +**Usage Statistics:** +👥 Total Users: ${metrics.totalUsers} +🟢 Active Users (24h): ${metrics.activeUsers} +💬 Total Messages: ${metrics.totalMessages} +🌐 API Calls: ${metrics.apiCalls} + +**Authentication:** +✅ Successful Logins: ${metrics.successfulLogins} +❌ Failed Logins: ${metrics.failedLogins} +📝 Registrations: ${metrics.registrations} + +**Business Metrics:** +🏠 Properties Created: ${metrics.propertiesCreated} + +**System Performance:** +⏱️ Uptime: ${Math.floor(metrics.uptime / 3600)}h ${Math.floor((metrics.uptime % 3600) / 60)}m +💾 Memory Usage: ${(metrics.memoryUsage.heapUsed / 1024 / 1024).toFixed(2)}MB +🚨 Total Errors: ${metrics.errorCount} + +${metrics.lastError ? `**Last Error:** +🕐 ${metrics.lastError.timestamp.toISOString()} +👤 User: ${metrics.lastError.userId || 'System'} +📝 ${metrics.lastError.error}` : '✅ No recent errors'} + `.trim(); + } + + // Send Alert + private async sendAlert(type: 'error' | 'warning' | 'info', message: string, data?: any): Promise { + const alert = { + type, + message, + data, + timestamp: new Date().toISOString(), + botName: 'Yaltopia Bot' + }; + + console.log(`🚨 [ALERT] ${type.toUpperCase()}: ${message}`); + + // Send to admin via Telegram (if configured) + if (this.alertConfig.adminChatId) { + try { + // This would need the bot instance to send messages + this.emit('adminAlert', { chatId: this.alertConfig.adminChatId, message: `🚨 ${message}` }); + } catch (error) { + console.error('Failed to send Telegram alert:', error); + } + } + + // Send to webhook (if configured) + if (this.alertConfig.webhookUrl) { + try { + await axios.post(this.alertConfig.webhookUrl, alert, { + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + console.error('Failed to send webhook alert:', error); + } + } + + this.emit('alert', alert); + } + + private updateMetrics(): void { + this.metrics.uptime = Math.floor((Date.now() - this.startTime.getTime()) / 1000); + this.metrics.activeUsers = this.activeUsers.size; + this.metrics.memoryUsage = process.memoryUsage(); + + // Clear active users older than 24 hours (simplified) + // In production, you'd want more sophisticated tracking + if (this.metrics.uptime % 86400 === 0) { // Every 24 hours + this.activeUsers.clear(); + } + } + + private cleanOldErrors(): void { + const oneDayAgo = Date.now() - 86400000; // 24 hours + this.recentErrors = this.recentErrors.filter(e => e.timestamp.getTime() > oneDayAgo); + } + + // Daily Report + scheduleDailyReport(): void { + const now = new Date(); + const tomorrow = new Date(now); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(9, 0, 0, 0); // 9 AM daily report + + const msUntilTomorrow = tomorrow.getTime() - now.getTime(); + + setTimeout(() => { + this.sendAlert('info', 'Daily System Report', this.generateReport()); + + // Schedule next daily report + setInterval(() => { + this.sendAlert('info', 'Daily System Report', this.generateReport()); + }, 86400000); // 24 hours + }, msUntilTomorrow); + } +} \ No newline at end of file diff --git a/src/shared/security/input-validator.ts b/src/shared/security/input-validator.ts new file mode 100644 index 0000000..db372b0 --- /dev/null +++ b/src/shared/security/input-validator.ts @@ -0,0 +1,133 @@ +export class InputValidator { + static readonly MAX_TITLE_LENGTH = 100; + static readonly MAX_DESCRIPTION_LENGTH = 500; + static readonly MAX_SUBCITY_LENGTH = 50; + static readonly MAX_NAME_LENGTH = 100; + static readonly MIN_PASSWORD_LENGTH = 6; + static readonly MAX_PASSWORD_LENGTH = 128; + + static sanitizeText(input: string): string { + return input + .trim() + .replace(/[<>\"'&]/g, '') // Remove potentially dangerous characters + .replace(/\s+/g, ' '); // Normalize whitespace + } + + static validateTitle(title: string): { valid: boolean; error?: string } { + const sanitized = this.sanitizeText(title); + + if (!sanitized || sanitized.length === 0) { + return { valid: false, error: 'Title cannot be empty' }; + } + + if (sanitized.length > this.MAX_TITLE_LENGTH) { + return { valid: false, error: `Title too long (max ${this.MAX_TITLE_LENGTH} characters)` }; + } + + return { valid: true }; + } + + static validateDescription(description: string): { valid: boolean; error?: string } { + const sanitized = this.sanitizeText(description); + + if (!sanitized || sanitized.length === 0) { + return { valid: false, error: 'Description cannot be empty' }; + } + + if (sanitized.length > this.MAX_DESCRIPTION_LENGTH) { + return { valid: false, error: `Description too long (max ${this.MAX_DESCRIPTION_LENGTH} characters)` }; + } + + return { valid: true }; + } + + static validateName(name: string): { valid: boolean; error?: string } { + const sanitized = this.sanitizeText(name); + + if (!sanitized || sanitized.length === 0) { + return { valid: false, error: 'Name cannot be empty' }; + } + + if (sanitized.length > this.MAX_NAME_LENGTH) { + return { valid: false, error: `Name too long (max ${this.MAX_NAME_LENGTH} characters)` }; + } + + // Check for valid name pattern (letters, spaces, basic punctuation) + if (!/^[a-zA-Z\s\-\.]+$/.test(sanitized)) { + return { valid: false, error: 'Name contains invalid characters' }; + } + + return { valid: true }; + } + + static validatePassword(password: string): { valid: boolean; error?: string } { + if (!password || password.length === 0) { + return { valid: false, error: 'Password cannot be empty' }; + } + + if (password.length < this.MIN_PASSWORD_LENGTH) { + return { valid: false, error: `Password too short (min ${this.MIN_PASSWORD_LENGTH} characters)` }; + } + + if (password.length > this.MAX_PASSWORD_LENGTH) { + return { valid: false, error: `Password too long (max ${this.MAX_PASSWORD_LENGTH} characters)` }; + } + + return { valid: true }; + } + + static validateEmail(email: string): { valid: boolean; error?: string } { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!email || email.length === 0) { + return { valid: false, error: 'Email cannot be empty' }; + } + + if (!emailRegex.test(email)) { + return { valid: false, error: 'Invalid email format' }; + } + + if (email.length > 254) { // RFC 5321 limit + return { valid: false, error: 'Email too long' }; + } + + return { valid: true }; + } + + static validateNumericInput(input: string, min: number = 0, max: number = Number.MAX_SAFE_INTEGER): { valid: boolean; value?: number; error?: string } { + const num = parseFloat(input); + + if (isNaN(num)) { + return { valid: false, error: 'Please enter a valid number' }; + } + + if (num < min) { + return { valid: false, error: `Value must be at least ${min}` }; + } + + if (num > max) { + return { valid: false, error: `Value must be at most ${max}` }; + } + + return { valid: true, value: num }; + } + + static validateSubcity(subcity: string): { valid: boolean; error?: string } { + const sanitized = this.sanitizeText(subcity); + + if (!sanitized || sanitized.length === 0) { + return { valid: false, error: 'Subcity cannot be empty' }; + } + + if (sanitized.length > this.MAX_SUBCITY_LENGTH) { + return { valid: false, error: `Subcity name too long (max ${this.MAX_SUBCITY_LENGTH} characters)` }; + } + + // Check for valid subcity pattern (letters, spaces, basic punctuation) + if (!/^[a-zA-Z\s\-\.]+$/.test(sanitized)) { + return { valid: false, error: 'Subcity name contains invalid characters' }; + } + + return { valid: true }; + } +} \ No newline at end of file diff --git a/src/shared/security/rate-limiter.ts b/src/shared/security/rate-limiter.ts new file mode 100644 index 0000000..0933c9a --- /dev/null +++ b/src/shared/security/rate-limiter.ts @@ -0,0 +1,37 @@ +export class RateLimiter { + private userActions = new Map(); + private readonly maxRequests: number; + private readonly windowMs: number; + + constructor(maxRequests = 10, windowMs = 60000) { // 10 requests per minute + this.maxRequests = maxRequests; + this.windowMs = windowMs; + } + + isRateLimited(chatId: number): boolean { + const now = Date.now(); + const userRequests = this.userActions.get(chatId) || []; + + // Remove old requests outside the window + const validRequests = userRequests.filter(timestamp => now - timestamp < this.windowMs); + + if (validRequests.length >= this.maxRequests) { + console.log(`⚠️ Rate limit exceeded for user ${chatId}`); + return true; + } + + // Add current request + validRequests.push(now); + this.userActions.set(chatId, validRequests); + + return false; + } + + getRemainingRequests(chatId: number): number { + const now = Date.now(); + const userRequests = this.userActions.get(chatId) || []; + const validRequests = userRequests.filter(timestamp => now - timestamp < this.windowMs); + + return Math.max(0, this.maxRequests - validRequests.length); + } +} \ No newline at end of file diff --git a/src/shared/services/api.service.ts b/src/shared/services/api.service.ts new file mode 100644 index 0000000..38beabd --- /dev/null +++ b/src/shared/services/api.service.ts @@ -0,0 +1,160 @@ +import axios, { AxiosInstance } from 'axios'; +import { ApiResponse } from '../types'; + +export class ApiService { + private client: AxiosInstance; + private accessToken: string | null = null; + + constructor(baseURL: string) { + this.client = axios.create({ + baseURL, + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Add request interceptor to include auth token + this.client.interceptors.request.use((config) => { + if (this.accessToken) { + config.headers.Authorization = `Bearer ${this.accessToken}`; + } + return config; + }); + } + + setAccessToken(token: string): void { + console.log('🔑 Setting access token for API requests'); + this.accessToken = token; + } + + clearAccessToken(): void { + console.log('🔑 Clearing access token'); + this.accessToken = null; + } + + async get(url: string): Promise> { + console.log(`🌐 GET request to: ${this.client.defaults.baseURL}${url}`); + try { + const response = await this.client.get(url); + console.log(`✅ GET ${url} - Status: ${response.status}`); + return { success: true, data: response.data }; + } catch (error: any) { + console.log(`❌ GET ${url} - Error:`, error.response?.status, error.response?.data?.message || error.message); + + // Handle specific connection errors + if (error.code === 'ECONNREFUSED') { + return { + success: false, + message: 'Cannot connect to API server. Please ensure the backend is running.' + }; + } + + if (error.code === 'EPROTO' || error.message?.includes('wrong version number')) { + return { + success: false, + message: 'SSL/TLS protocol error. Check if API URL uses correct protocol (http/https).' + }; + } + + return { + success: false, + message: error.response?.data?.message || error.message + }; + } + } + + async post(url: string, data: any): Promise> { + console.log(`🌐 POST request to: ${this.client.defaults.baseURL}${url}`); + console.log(`📤 POST data:`, { ...data, password: data.password ? '[HIDDEN]' : undefined }); + try { + const response = await this.client.post(url, data); + console.log(`✅ POST ${url} - Status: ${response.status}`); + return { success: true, data: response.data }; + } catch (error: any) { + console.log(`❌ POST ${url} - Error:`, error.response?.status, error.response?.data?.message || error.message); + + // Handle specific connection errors + if (error.code === 'ECONNREFUSED') { + return { + success: false, + message: 'Cannot connect to API server. Please ensure the backend is running.' + }; + } + + if (error.code === 'EPROTO' || error.message?.includes('wrong version number')) { + return { + success: false, + message: 'SSL/TLS protocol error. Check if API URL uses correct protocol (http/https).' + }; + } + + return { + success: false, + message: error.response?.data?.message || error.message + }; + } + } + + async patch(url: string, data: any): Promise> { + console.log(`🌐 PATCH request to: ${this.client.defaults.baseURL}${url}`); + console.log(`📤 PATCH data:`, { ...data, password: data.password ? '[HIDDEN]' : undefined }); + try { + const response = await this.client.patch(url, data); + console.log(`✅ PATCH ${url} - Status: ${response.status}`); + return { success: true, data: response.data }; + } catch (error: any) { + console.log(`❌ PATCH ${url} - Error:`, error.response?.status, error.response?.data?.message || error.message); + + // Handle specific connection errors + if (error.code === 'ECONNREFUSED') { + return { + success: false, + message: 'Cannot connect to API server. Please ensure the backend is running.' + }; + } + + if (error.code === 'EPROTO' || error.message?.includes('wrong version number')) { + return { + success: false, + message: 'SSL/TLS protocol error. Check if API URL uses correct protocol (http/https).' + }; + } + + return { + success: false, + message: error.response?.data?.message || error.message + }; + } + } + + async delete(url: string): Promise> { + console.log(`🌐 DELETE request to: ${this.client.defaults.baseURL}${url}`); + try { + const response = await this.client.delete(url); + console.log(`✅ DELETE ${url} - Status: ${response.status}`); + return { success: true, data: response.data }; + } catch (error: any) { + console.log(`❌ DELETE ${url} - Error:`, error.response?.status, error.response?.data?.message || error.message); + + // Handle specific connection errors + if (error.code === 'ECONNREFUSED') { + return { + success: false, + message: 'Cannot connect to API server. Please ensure the backend is running.' + }; + } + + if (error.code === 'EPROTO' || error.message?.includes('wrong version number')) { + return { + success: false, + message: 'SSL/TLS protocol error. Check if API URL uses correct protocol (http/https).' + }; + } + + return { + success: false, + message: error.response?.data?.message || error.message + }; + } + } +} \ No newline at end of file diff --git a/src/shared/services/session.service.ts b/src/shared/services/session.service.ts new file mode 100644 index 0000000..2a84d4d --- /dev/null +++ b/src/shared/services/session.service.ts @@ -0,0 +1,33 @@ +import { BotSession } from '../types'; + +export class SessionService { + private sessions: Map = new Map(); + + getSession(chatId: number): BotSession { + if (!this.sessions.has(chatId)) { + this.sessions.set(chatId, { + chatId, + step: 'START', + data: {} + }); + } + return this.sessions.get(chatId)!; + } + + updateSession(chatId: number, updates: Partial): void { + const session = this.getSession(chatId); + Object.assign(session, updates); + } + + clearSession(chatId: number): void { + this.sessions.delete(chatId); + } + + setSessionStep(chatId: number, step: string, data?: Record): void { + const session = this.getSession(chatId); + session.step = step; + if (data) { + Object.assign(session.data, data); + } + } +} \ No newline at end of file diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts new file mode 100644 index 0000000..00853ae --- /dev/null +++ b/src/shared/types/index.ts @@ -0,0 +1,45 @@ +export interface User { + id: string; + name: string; + email: string; + phone: string; + role: 'AGENT' | 'USER'; + access_token?: string; +} + +export interface UserRegistration { + name: string; + email: string; + phone: string; + password: string; + role: 'AGENT'; +} + +export interface Property { + id?: string; + title: string; + description: string; + type: 'RENT' | 'SELL'; + price: number; + area: number; + rooms: number; + toilets: number; + subcity: string; + houseType: string; + status: 'DRAFT' | 'PUBLISHED'; + taxRate: number; + isTaxable: boolean; +} + +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; +} + +export interface BotSession { + chatId: number; + step: string; + data: Record; + user?: User; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d1b16ac --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file