From 613ac11e3fb140ed915ecbc3a99f49eaebb4e32a Mon Sep 17 00:00:00 2001 From: debudebuye Date: Mon, 5 Jan 2026 05:31:07 +0300 Subject: [PATCH] feat: automatic notification services --- .env.example | 4 +- .gitignore | 5 +- package-lock.json | 538 ++++++++++++++++++ package.json | 5 +- scripts/docker-run.sh | 1 + scripts/test-webhook.js | 69 +++ src/api.js | 5 +- src/bot.js | 183 +++++- src/features/auth.js | 145 +++-- src/features/menu.js | 49 +- src/services/automaticNotificationService.js | 324 +++++++++++ .../optimizedAutomaticNotificationService.js | 418 ++++++++++++++ .../simpleAutomaticNotificationService.js | 437 ++++++++++++++ src/webhook/listingWebhook.js | 142 +++++ src/webhookServer.js | 117 ++++ 15 files changed, 2369 insertions(+), 73 deletions(-) create mode 100644 scripts/test-webhook.js create mode 100644 src/services/automaticNotificationService.js create mode 100644 src/services/optimizedAutomaticNotificationService.js create mode 100644 src/services/simpleAutomaticNotificationService.js create mode 100644 src/webhook/listingWebhook.js create mode 100644 src/webhookServer.js diff --git a/.env.example b/.env.example index 043c697..79aa901 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ TELEGRAM_BOT_TOKEN=your_bot_token_here API_BASE_URL=your_backend_api_url_here -WEBSITE_URL=https://yaltipia.com/listings \ No newline at end of file +WEBSITE_URL=https://yaltipia.com/listings +WEBHOOK_PORT=3001 +NOTIFICATION_MODE=optimized \ No newline at end of file diff --git a/.gitignore b/.gitignore index c336e81..7e5ba22 100644 --- a/.gitignore +++ b/.gitignore @@ -45,10 +45,7 @@ Thumbs.db # .dockerignore # Documentation (optional - remove if you want to track them) -BACKEND_API_INTEGRATION.md -ERROR_HANDLING_IMPROVEMENTS.md -WEBSITE_INTEGRATION.md -SECURITY_AUDIT_REPORT.md +docs/ # Temporary files tmp/ diff --git a/package-lock.json b/package-lock.json index c8b97fb..64a3f1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,10 @@ "dependencies": { "axios": "^1.6.0", "bcrypt": "^5.1.1", + "cors": "^2.8.5", "crypto": "^1.0.1", "dotenv": "^16.3.1", + "express": "^4.18.2", "node-telegram-bot-api": "^0.67.0", "validator": "^13.11.0" }, @@ -108,6 +110,19 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "license": "ISC" }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -227,6 +242,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/array.prototype.findindex": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/array.prototype.findindex/-/array.prototype.findindex-2.2.4.tgz", @@ -397,6 +418,45 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "license": "MIT" }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -420,6 +480,15 @@ "node": ">=8" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -540,12 +609,61 @@ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "license": "ISC" }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/crypto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", @@ -674,6 +792,25 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "license": "MIT" }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -719,12 +856,27 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -876,12 +1028,88 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "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/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -933,6 +1161,39 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -993,6 +1254,24 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -1350,6 +1629,26 @@ "node": ">= 0.4" } }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/http-signature": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", @@ -1394,6 +1693,18 @@ } } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -1432,6 +1743,15 @@ "node": ">= 0.4" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -1906,6 +2226,33 @@ "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -2003,6 +2350,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -2194,6 +2550,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2220,6 +2588,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2229,6 +2606,12 @@ "node": ">=0.10.0" } }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -2263,6 +2646,19 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -2328,6 +2724,30 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -2658,6 +3078,60 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -2710,6 +3184,12 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -2826,6 +3306,15 @@ "node": ">=0.10.0" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", @@ -3006,6 +3495,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/touch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", @@ -3052,6 +3550,19 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -3160,6 +3671,15 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3186,6 +3706,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -3204,6 +3733,15 @@ "node": ">= 0.10" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/package.json b/package.json index fc046ed..5ec8fb2 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,16 @@ "main": "src/bot.js", "scripts": { "start": "node src/bot.js", - "dev": "nodemon src/bot.js" + "dev": "nodemon src/bot.js", + "test:webhook": "node scripts/test-webhook.js" }, "dependencies": { "axios": "^1.6.0", "bcrypt": "^5.1.1", + "cors": "^2.8.5", "crypto": "^1.0.1", "dotenv": "^16.3.1", + "express": "^4.18.2", "node-telegram-bot-api": "^0.67.0", "validator": "^13.11.0" }, diff --git a/scripts/docker-run.sh b/scripts/docker-run.sh index e69de29..7a6e2fd 100644 --- a/scripts/docker-run.sh +++ b/scripts/docker-run.sh @@ -0,0 +1 @@ +#! \ No newline at end of file diff --git a/scripts/test-webhook.js b/scripts/test-webhook.js new file mode 100644 index 0000000..4ff30fc --- /dev/null +++ b/scripts/test-webhook.js @@ -0,0 +1,69 @@ +#!/usr/bin/env node + +const axios = require('axios'); + +// Test webhook endpoint +const WEBHOOK_URL = process.env.WEBHOOK_URL || 'http://localhost:3001'; + +// Sample listing data for testing +const sampleListing = { + id: `test-listing-${Date.now()}`, + title: 'Test Apartment for Webhook', + type: 'RENT', + status: 'ACTIVE', + price: 35000, + subcity: 'Bole', + houseType: 'Apartment', + createdAt: new Date().toISOString(), + url: 'https://yaltipia.com/listing/test-123' +}; + +async function testWebhook() { + console.log('๐Ÿงช Testing Webhook System...\n'); + + try { + // Test health endpoint + console.log('1. Testing health endpoint...'); + const healthResponse = await axios.get(`${WEBHOOK_URL}/webhook/health`); + console.log('โœ… Health check passed:', healthResponse.data.message); + + // Test status endpoint + console.log('\n2. Testing status endpoint...'); + const statusResponse = await axios.get(`${WEBHOOK_URL}/status`); + console.log('โœ… Status check passed'); + console.log(' Webhook running:', statusResponse.data.webhook.running); + console.log(' Notifications running:', statusResponse.data.automaticNotifications.isRunning); + + // Test new listing webhook + console.log('\n3. Testing new listing webhook...'); + const newListingResponse = await axios.post(`${WEBHOOK_URL}/webhook/new-listing`, sampleListing); + console.log('โœ… New listing webhook passed:', newListingResponse.data.message); + console.log(' Processed listing ID:', newListingResponse.data.listingId); + + // Test listing update webhook + console.log('\n4. Testing listing update webhook...'); + const updatedListing = { ...sampleListing, price: 40000 }; + const updateResponse = await axios.post(`${WEBHOOK_URL}/webhook/update-listing`, updatedListing); + console.log('โœ… Update listing webhook passed:', updateResponse.data.message); + + console.log('\n๐ŸŽ‰ All webhook tests passed successfully!'); + + } catch (error) { + console.error('โŒ Webhook test failed:', error.message); + + if (error.response) { + console.error('Response status:', error.response.status); + console.error('Response data:', error.response.data); + } + + if (error.code === 'ECONNREFUSED') { + console.error('\n๐Ÿ’ก Make sure the bot is running and webhook server is started'); + console.error(' Run: npm start'); + } + + process.exit(1); + } +} + +// Run the test +testWebhook(); \ No newline at end of file diff --git a/src/api.js b/src/api.js index 97decc6..d532ee8 100644 --- a/src/api.js +++ b/src/api.js @@ -50,6 +50,7 @@ class ApiClient { name: userData.name, email: userData.email, phone: userData.phone, + role: userData.role || 'CUSTOMER', telegramUserId: telegramUserId }); @@ -65,6 +66,7 @@ class ApiClient { email: userData.email, phone: userData.phone, password: userData.password, + role: userData.role || 'CUSTOMER', // Include role in registration (uppercase) telegramUserId: telegramUserId.toString() }); @@ -153,7 +155,8 @@ class ApiClient { phone: phone, name: response.data.user?.name || 'User', email: response.data.user?.email, - id: response.data.user?.id + id: response.data.user?.id, + role: response.data.user?.role || 'CUSTOMER' // Extract role, default to CUSTOMER } }; } else if (response.data.hasAccount === false || response.data.flow === 'register') { diff --git a/src/bot.js b/src/bot.js index 2466ae0..bc8d13a 100644 --- a/src/bot.js +++ b/src/bot.js @@ -2,6 +2,10 @@ require('dotenv').config(); const TelegramBot = require('node-telegram-bot-api'); const ApiClient = require('./api'); const NotificationService = require('./services/notificationService'); +const AutomaticNotificationService = require('./services/automaticNotificationService'); +const SimpleAutomaticNotificationService = require('./services/simpleAutomaticNotificationService'); +const OptimizedAutomaticNotificationService = require('./services/optimizedAutomaticNotificationService'); +const WebhookServer = require('./webhookServer'); const ErrorHandler = require('./utils/errorHandler'); // Import feature modules @@ -28,6 +32,40 @@ class YaltipiaBot { this.userStates = new Map(); // Store user conversation states this.userSessions = new Map(); // Store user session data + // Initialize automatic notification service + // Choose the best mode based on configuration + const notificationMode = process.env.NOTIFICATION_MODE || 'optimized'; + + if (notificationMode === 'full') { + console.log('๐Ÿ”ง Using Full Automatic Notification Service (requires backend webhooks)'); + this.automaticNotificationService = new AutomaticNotificationService( + this.bot, + this.api, + this.userSessions + ); + + this.webhookServer = new WebhookServer( + this.automaticNotificationService, + process.env.WEBHOOK_PORT || 3001 + ); + } else if (notificationMode === 'optimized') { + console.log('๐Ÿ”ง Using Optimized Automatic Notification Service (works great with your API)'); + this.automaticNotificationService = new OptimizedAutomaticNotificationService( + this.bot, + this.api, + this.userSessions + ); + this.webhookServer = null; + } else { + console.log('๐Ÿ”ง Using Simple Automatic Notification Service (basic polling)'); + this.automaticNotificationService = new SimpleAutomaticNotificationService( + this.bot, + this.api, + this.userSessions + ); + this.webhookServer = null; + } + console.log('Bot initialized with notification service:', !!this.notificationService); // Initialize features @@ -40,6 +78,30 @@ class YaltipiaBot { this.setupHandlers(); this.setupErrorHandlers(); + + // Start automatic notification service + this.startAutomaticNotifications(); + } + + startAutomaticNotifications() { + if (this.webhookServer) { + // Full mode with webhook server + this.webhookServer.start().then(() => { + setTimeout(() => { + this.automaticNotificationService.start(); + }, 2000); + }).catch(error => { + console.error('Failed to start webhook server:', error); + setTimeout(() => { + this.automaticNotificationService.start(); + }, 2000); + }); + } else { + // Simple mode without webhook server + setTimeout(() => { + this.automaticNotificationService.start(); + }, 5000); // Wait 5 seconds for bot to fully initialize + } } setupErrorHandlers() { @@ -115,6 +177,39 @@ class YaltipiaBot { } }); + // Admin commands for automatic notifications + this.bot.onText(/\/notifications_status/, (msg) => { + const chatId = msg.chat.id; + const status = this.automaticNotificationService.getStatus(); + + this.bot.sendMessage(chatId, + `๐Ÿค– Automatic Notifications Status:\n\n` + + `๐Ÿ”„ Running: ${status.isRunning ? 'โœ… Yes' : 'โŒ No'}\n` + + `๐Ÿ• Last Check: ${status.lastCheckTime.toLocaleString()}\n` + + `โฑ๏ธ Check Interval: ${status.checkInterval / 1000} seconds\n` + + `๐Ÿ“Š Processed Listings: ${status.processedListingsCount}` + ); + }); + + this.bot.onText(/\/notifications_start/, (msg) => { + const chatId = msg.chat.id; + this.automaticNotificationService.start(); + this.bot.sendMessage(chatId, '๐Ÿš€ Automatic notification service started!'); + }); + + this.bot.onText(/\/notifications_stop/, (msg) => { + const chatId = msg.chat.id; + this.automaticNotificationService.stop(); + this.bot.sendMessage(chatId, '๐Ÿ›‘ Automatic notification service stopped!'); + }); + + this.bot.onText(/\/notifications_check/, async (msg) => { + const chatId = msg.chat.id; + this.bot.sendMessage(chatId, '๐Ÿ” Triggering manual notification check...'); + await this.automaticNotificationService.triggerManualCheck(); + this.bot.sendMessage(chatId, 'โœ… Manual notification check completed!'); + }); + // Handle contact sharing this.bot.on('contact', (msg) => { this.auth.handleContact(msg); @@ -138,6 +233,59 @@ class YaltipiaBot { const telegramId = msg.from.id; try { + // Handle reply keyboard button presses + if (msg.text) { + switch (msg.text) { + case '๐Ÿ”” Create Notification': + if (this.auth.isAuthenticated(telegramId)) { + await this.notifications.startNotificationCreation(chatId, telegramId); + // Update menu to show cancel option + this.menu.showMainMenu(chatId, telegramId); + return; + } else { + this.bot.sendMessage(chatId, 'Please login first with /start'); + return; + } + + case '๐Ÿ“‹ View My Notifications': + if (this.auth.isAuthenticated(telegramId)) { + await this.notifications.showUserNotifications(chatId, telegramId); + return; + } else { + this.bot.sendMessage(chatId, 'Please login first with /start'); + return; + } + + case '๐ŸŒ Browse All Listings': + const websiteUrl = process.env.WEBSITE_URL || 'https://yaltipia.com/listings'; + this.bot.sendMessage(chatId, + `๐ŸŒ Browse all available listings on our website:\n\n${websiteUrl}`, + { + reply_markup: { + inline_keyboard: [ + [{ text: '๐ŸŒ Open Website', url: websiteUrl }] + ] + } + } + ); + return; + + case '๐Ÿšช Logout': + await this.auth.handleLogout({ chat: { id: chatId }, from: { id: telegramId } }); + return; + + case 'โŒ Cancel Creation': + // Cancel notification creation and return to main menu + this.userStates.delete(telegramId); + this.bot.sendMessage(chatId, + 'โŒ Notification creation cancelled.\n\n' + + 'Returning to main menu...' + ); + this.menu.showMainMenu(chatId, telegramId); + return; + } + } + // Try each feature's text handler in order const handled = await this.auth.handleRegistrationText(msg) || @@ -148,7 +296,7 @@ class YaltipiaBot { if (!handled) { // If no feature handled the message and user is authenticated, show menu if (this.auth.isAuthenticated(telegramId)) { - this.menu.showMainMenu(chatId); + this.menu.showMainMenu(chatId, telegramId); } else { this.bot.sendMessage(chatId, 'Please start with /start to register or login.'); } @@ -203,7 +351,7 @@ class YaltipiaBot { case 'save_notification': handled = await this.notifications.saveNotification(chatId, telegramId); if (handled) { - this.menu.showMainMenu(chatId); + this.menu.showMainMenu(chatId, telegramId); } break; @@ -264,7 +412,7 @@ class YaltipiaBot { console.log(`Unhandled callback query: ${data}`); // Try to show main menu as fallback if (this.auth.isAuthenticated(telegramId)) { - this.menu.showMainMenu(chatId); + this.menu.showMainMenu(chatId, telegramId); } } @@ -293,7 +441,34 @@ const bot = new YaltipiaBot(); console.log('๐Ÿค– Yaltipia Telegram Bot is running...'); // Graceful shutdown -process.on('SIGINT', () => { +process.on('SIGINT', async () => { console.log('Shutting down bot...'); + + // Stop automatic notification service + if (bot.automaticNotificationService) { + bot.automaticNotificationService.stop(); + } + + // Stop webhook server + if (bot.webhookServer) { + await bot.webhookServer.stop(); + } + + process.exit(0); +}); + +process.on('SIGTERM', async () => { + console.log('Received SIGTERM, shutting down gracefully...'); + + // Stop automatic notification service + if (bot.automaticNotificationService) { + bot.automaticNotificationService.stop(); + } + + // Stop webhook server + if (bot.webhookServer) { + await bot.webhookServer.stop(); + } + process.exit(0); }); \ No newline at end of file diff --git a/src/features/auth.js b/src/features/auth.js index 8f5f575..8287710 100644 --- a/src/features/auth.js +++ b/src/features/auth.js @@ -15,7 +15,7 @@ class AuthFeature { // Check if user is already logged in const existingSession = this.userSessions.get(telegramId); if (existingSession && existingSession.user) { - this.showMainMenu(chatId); + this.showMainMenu(chatId, telegramId); return; } @@ -54,7 +54,7 @@ class AuthFeature { this.bot.sendMessage(chatId, `โœ… You are already logged in as ${existingSession.user.name || 'User'}!` ); - this.showMainMenu(chatId); + this.showMainMenu(chatId, telegramId); return; } @@ -78,7 +78,7 @@ class AuthFeature { } const phoneNumber = msg.contact.phone_number; - console.log('Checking user existence for phone:', phoneNumber); + console.log('Checking user existence and role for phone:', phoneNumber); try { // Check if user exists in backend database @@ -87,33 +87,56 @@ class AuthFeature { console.log('User existence check result:', existingUser); if (existingUser.success) { - // User exists in backend, ask for password to login - console.log('User found in database'); + // User exists in backend, check their role + console.log('User found in database, checking role'); - this.userStates.set(telegramId, { - step: 'waiting_password', - userData: { - phone: phoneNumber, - telegramId: telegramId, - // We might not have the name yet if using /exists endpoint - name: existingUser.user.name || 'User' - } - }); + const userRole = existingUser.user.role || 'CUSTOMER'; // Default to CUSTOMER if no role specified + console.log('User role:', userRole); + + if (userRole.toLowerCase() === 'customer') { + // Role is customer, continue with login process + console.log('User has customer role, proceeding with login'); + + this.userStates.set(telegramId, { + step: 'waiting_password', + userData: { + phone: phoneNumber, + telegramId: telegramId, + name: existingUser.user.name || 'User', + role: userRole + } + }); - const welcomeMessage = existingUser.user.name && existingUser.user.name !== 'User' - ? `Welcome back, ${existingUser.user.name}!` - : 'Welcome back!'; + const welcomeMessage = existingUser.user.name && existingUser.user.name !== 'User' + ? `Welcome back, ${existingUser.user.name}!` + : 'Welcome back!'; - this.bot.sendMessage(chatId, - `โœ… Phone number recognized!\n\n` + - `${welcomeMessage}\n` + - '๐Ÿ” Please enter your password to login:', - { reply_markup: { remove_keyboard: true } } - ); - return; + this.bot.sendMessage(chatId, + `โœ… Phone number recognized!\n\n` + + `${welcomeMessage}\n` + + '๐Ÿ” Please enter your password to login:', + { reply_markup: { remove_keyboard: true } } + ); + return; + } else { + // Role is not customer, show admin message without revealing role + console.log('User has non-customer role:', userRole); + + this.bot.sendMessage(chatId, + `โŒ Account Access Restricted\n\n` + + `This phone number is registered with a different account type.\n\n` + + `๐Ÿ”’ You cannot create a client account with this number.\n\n` + + `๐Ÿ“ž Please contact the administrator for assistance or use a different phone number.`, + { reply_markup: { remove_keyboard: true } } + ); + + // Clear user state since they can't proceed + this.userStates.delete(telegramId); + return; + } } - // User not found in backend, continue with registration + // User not found in backend, continue with registration process console.log('User not found in database, proceeding with registration'); userState.phoneNumber = phoneNumber; @@ -282,13 +305,14 @@ class AuthFeature { // Continue without token - user is authenticated but may need token for some operations } - // Create session with proper structure + // Create session with proper structure including role this.userSessions.set(telegramId, { user: { id: user.id, name: user.name || userData.name || 'User', email: user.email, - phone: userData.phone + phone: userData.phone, + role: user.role || userData.role || 'CUSTOMER' // Store role in session }, phoneNumber: userData.phone, password: password, @@ -309,7 +333,7 @@ class AuthFeature { 'You are now logged in.' ); - this.showMainMenu(chatId); + this.showMainMenu(chatId, telegramId); return true; } else { this.bot.sendMessage(chatId, @@ -352,12 +376,13 @@ class AuthFeature { async completeRegistration(chatId, telegramId, userState) { try { - // Register with backend, passing telegramUserId + // Register with backend, passing telegramUserId and default role as CUSTOMER const registrationResult = await this.api.registerUser({ name: userState.name, email: userState.email, phone: userState.phoneNumber, - password: userState.password + password: userState.password, + role: 'CUSTOMER' // Set default role as CUSTOMER (uppercase) for new registrations }, telegramId); // Pass telegramUserId as second parameter if (!registrationResult.success) { @@ -415,13 +440,14 @@ class AuthFeature { } } - // Create session with proper structure + // Create session with proper structure including customer role this.userSessions.set(telegramId, { user: { id: user.id || registrationResult.data?.id, name: userState.name, email: userState.email, - phone: userState.phoneNumber + phone: userState.phoneNumber, + role: user.role || 'CUSTOMER' // Ensure CUSTOMER role is stored }, phoneNumber: userState.phoneNumber, password: userState.password, @@ -466,7 +492,8 @@ class AuthFeature { if (!userSession || !userSession.user) { this.bot.sendMessage(chatId, 'โŒ You are not logged in.\n\n' + - 'Use /start to register or login.' + 'Use /start to register or login.', + { reply_markup: { remove_keyboard: true } } ); return true; } @@ -488,7 +515,8 @@ class AuthFeature { this.bot.sendMessage(chatId, `โœ… Goodbye ${userName}!\n\n` + 'You have been logged out successfully.\n\n' + - 'Use /start to login again when you want to use the bot.' + 'Use /start to login again when you want to use the bot.', + { reply_markup: { remove_keyboard: true } } ); return true; @@ -508,22 +536,42 @@ class AuthFeature { return; } - const websiteUrl = process.env.WEBSITE_URL || 'https://yaltipia.com/listings'; + // Check if user is in notification creation process + const userState = telegramId ? this.userStates.get(telegramId) : null; + const isCreatingNotification = userState && userState.step && userState.step.startsWith('notification_'); - const keyboard = { - inline_keyboard: [ - [{ text: '๐Ÿ”” Create Notification', callback_data: 'create_notification' }], - [{ text: '๐Ÿ“‹ View My Notifications', callback_data: 'view_notifications' }], - [{ text: '๐ŸŒ Browse All Listings', url: websiteUrl }], - [{ text: '๐Ÿšช Logout', callback_data: 'logout' }] - ] - }; + let keyboard; + + if (isCreatingNotification) { + // Show cancel option during notification creation + keyboard = { + keyboard: [ + ['โŒ Cancel Creation'], + ['๐Ÿšช Logout'] + ], + resize_keyboard: true, + one_time_keyboard: false, + persistent: true + }; + } else { + // Show normal menu + keyboard = { + keyboard: [ + ['๐Ÿ”” Create Notification', '๐Ÿ“‹ View My Notifications'], + ['๐ŸŒ Browse All Listings'], + ['๐Ÿšช Logout'] + ], + resize_keyboard: true, + one_time_keyboard: false, + persistent: true + }; + } - this.bot.sendMessage(chatId, - '๐Ÿ  Yaltipia Home Bot - Main Menu\n\n' + - 'What would you like to do?', - { reply_markup: keyboard } - ); + const message = isCreatingNotification + ? '๐Ÿ”” Creating Notification...\n\nUse "โŒ Cancel Creation" to stop and return to main menu.' + : '๐Ÿ  Yaltipia Home Bot - Main Menu\n\nWhat would you like to do? Use the buttons below:'; + + this.bot.sendMessage(chatId, message, { reply_markup: keyboard }); } // Validate and refresh session if needed @@ -564,6 +612,7 @@ class AuthFeature { userId: session.user?.id, userName: session.user?.name, phone: session.phoneNumber, + role: session.user?.role || 'CUSTOMER', loginTime: session.loginTime }; } diff --git a/src/features/menu.js b/src/features/menu.js index 80e8f35..1bf6ed6 100644 --- a/src/features/menu.js +++ b/src/features/menu.js @@ -7,22 +7,43 @@ class MenuFeature { showMainMenu(chatId, telegramId = null) { // If telegramId is provided, we should validate session in auth feature // For now, just show the menu - const websiteUrl = process.env.WEBSITE_URL || 'https://yaltipia.com/listings'; - const keyboard = { - inline_keyboard: [ - [{ text: '๐Ÿ”” Create Notification', callback_data: 'create_notification' }], - [{ text: '๐Ÿ“‹ View My Notifications', callback_data: 'view_notifications' }], - [{ text: '๐ŸŒ Browse All Listings', url: websiteUrl }], - [{ text: '๐Ÿšช Logout', callback_data: 'logout' }] - ] - }; + // Check if user is in notification creation process + const userState = telegramId ? this.userStates.get(telegramId) : null; + const isCreatingNotification = userState && userState.step && userState.step.startsWith('notification_'); + + let keyboard; + + if (isCreatingNotification) { + // Show cancel option during notification creation + keyboard = { + keyboard: [ + ['โŒ Cancel Creation'], + ['๐Ÿšช Logout'] + ], + resize_keyboard: true, + one_time_keyboard: false, + persistent: true + }; + } else { + // Show normal menu + keyboard = { + keyboard: [ + ['๐Ÿ”” Create Notification', '๐Ÿ“‹ View My Notifications'], + ['๐ŸŒ Browse All Listings'], + ['๐Ÿšช Logout'] + ], + resize_keyboard: true, + one_time_keyboard: false, + persistent: true + }; + } - this.bot.sendMessage(chatId, - '๐Ÿ  Yaltipia Home Bot - Main Menu\n\n' + - 'What would you like to do?', - { reply_markup: keyboard } - ); + const message = isCreatingNotification + ? '๐Ÿ”” Creating Notification...\n\nUse "โŒ Cancel Creation" to stop and return to main menu.' + : '๐Ÿ  Yaltipia Home Bot - Main Menu\n\nWhat would you like to do? Use the buttons below:'; + + this.bot.sendMessage(chatId, message, { reply_markup: keyboard }); } async handleBackToMenu(chatId, telegramId) { diff --git a/src/services/automaticNotificationService.js b/src/services/automaticNotificationService.js new file mode 100644 index 0000000..af74556 --- /dev/null +++ b/src/services/automaticNotificationService.js @@ -0,0 +1,324 @@ +const axios = require('axios'); + +class AutomaticNotificationService { + constructor(bot, api, userSessions) { + this.bot = bot; + this.api = api; + this.userSessions = userSessions; + this.isRunning = false; + this.checkInterval = null; + this.lastCheckTime = new Date(); + this.processedListings = new Set(); // Track processed listings to avoid duplicates + + // Configuration + this.CHECK_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes + this.MAX_NOTIFICATIONS_PER_USER = 5; // Max notifications per check cycle per user + } + + // Start the automatic notification service + start() { + if (this.isRunning) { + console.log('Automatic notification service is already running'); + return; + } + + console.log('๐Ÿš€ Starting automatic notification service...'); + this.isRunning = true; + + // Run initial check + this.checkForNewMatches(); + + // Set up interval for regular checks + this.checkInterval = setInterval(() => { + this.checkForNewMatches(); + }, this.CHECK_INTERVAL_MS); + + console.log(`โœ… Automatic notification service started (checking every ${this.CHECK_INTERVAL_MS / 1000} seconds)`); + } + + // Stop the automatic notification service + stop() { + if (!this.isRunning) { + console.log('Automatic notification service is not running'); + return; + } + + console.log('๐Ÿ›‘ Stopping automatic notification service...'); + this.isRunning = false; + + if (this.checkInterval) { + clearInterval(this.checkInterval); + this.checkInterval = null; + } + + console.log('โœ… Automatic notification service stopped'); + } + + // Main method to check for new matches + async checkForNewMatches() { + if (!this.isRunning) return; + + try { + console.log('๐Ÿ” Checking for new listing matches...'); + + // Get all recent listings (since last check) + const recentListings = await this.getRecentListings(); + + if (!recentListings || recentListings.length === 0) { + console.log('No new listings found'); + return; + } + + console.log(`Found ${recentListings.length} recent listings`); + + // Get all active notifications from all users + const activeNotifications = await this.getAllActiveNotifications(); + + if (!activeNotifications || activeNotifications.length === 0) { + console.log('No active notifications found'); + return; + } + + console.log(`Found ${activeNotifications.length} active notifications`); + + // Check each listing against all notifications + let totalMatches = 0; + + for (const listing of recentListings) { + // Skip if we've already processed this listing + if (this.processedListings.has(listing.id)) { + continue; + } + + const matches = await this.findMatchingNotifications(listing, activeNotifications); + + if (matches.length > 0) { + console.log(`Listing ${listing.id} matches ${matches.length} notifications`); + + // Send notifications to matched users + for (const match of matches) { + await this.sendMatchNotification(match.telegramId, listing, match.notification); + totalMatches++; + } + } + + // Mark listing as processed + this.processedListings.add(listing.id); + } + + // Clean up old processed listings (keep only last 1000) + if (this.processedListings.size > 1000) { + const oldEntries = Array.from(this.processedListings).slice(0, 500); + oldEntries.forEach(id => this.processedListings.delete(id)); + } + + console.log(`โœ… Notification check complete. Sent ${totalMatches} notifications`); + this.lastCheckTime = new Date(); + + } catch (error) { + console.error('โŒ Error in automatic notification check:', error); + } + } + + // Get recent listings from the API + async getRecentListings() { + try { + // Get all listings (you might want to add a date filter to the API) + const result = await this.api.getListings(null, {}); + + if (!result.success) { + console.error('Failed to get listings:', result.error); + return []; + } + + // Filter for recent listings (last 24 hours) + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + + return result.listings.filter(listing => { + // If listing has createdAt or updatedAt, use it + const listingDate = new Date(listing.createdAt || listing.updatedAt || listing.dateAdded); + return listingDate > oneDayAgo; + }); + + } catch (error) { + console.error('Error getting recent listings:', error); + return []; + } + } + + // Get all active notifications from all users + async getAllActiveNotifications() { + try { + // This would ideally be a backend API endpoint to get all active notifications + // For now, we'll try to get notifications for known telegram users + const allNotifications = []; + + // Get all telegram user IDs from active sessions + const telegramIds = Array.from(this.userSessions.keys()); + + for (const telegramId of telegramIds) { + try { + const userSession = this.userSessions.get(telegramId); + if (userSession && userSession.user) { + const result = await this.api.getUserNotifications(telegramId, userSession.user.id); + + if (result.success && result.notifications) { + // Add telegramId to each notification for easy access + result.notifications.forEach(notification => { + notification.telegramId = telegramId; + notification.userId = userSession.user.id; + }); + + allNotifications.push(...result.notifications); + } + } + } catch (error) { + console.error(`Error getting notifications for user ${telegramId}:`, error); + } + } + + // Filter for active notifications only + return allNotifications.filter(notification => + notification.status === 'ACTIVE' || notification.isActive === true + ); + + } catch (error) { + console.error('Error getting all active notifications:', error); + return []; + } + } + + // Find notifications that match a given listing + async findMatchingNotifications(listing, notifications) { + const matches = []; + + for (const notification of notifications) { + if (this.doesListingMatchNotification(listing, notification)) { + matches.push({ + telegramId: notification.telegramId, + userId: notification.userId, + notification: notification + }); + } + } + + return matches; + } + + // Check if a listing matches a notification's criteria + doesListingMatchNotification(listing, notification) { + try { + // Check property type + if (notification.type && listing.type !== notification.type) { + return false; + } + + // Check status + if (notification.status && listing.status !== notification.status) { + return false; + } + + // Check subcity/area + if (notification.subcity && notification.subcity.toLowerCase() !== 'any') { + if (!listing.subcity || + listing.subcity.toLowerCase() !== notification.subcity.toLowerCase()) { + return false; + } + } + + // Check house type + if (notification.houseType && notification.houseType.toLowerCase() !== 'any') { + if (!listing.houseType || + listing.houseType.toLowerCase() !== notification.houseType.toLowerCase()) { + return false; + } + } + + // Check price range + const listingPrice = parseFloat(listing.price) || 0; + + if (notification.minPrice && listingPrice < notification.minPrice) { + return false; + } + + if (notification.maxPrice && listingPrice > notification.maxPrice) { + return false; + } + + return true; + + } catch (error) { + console.error('Error matching listing to notification:', error); + return false; + } + } + + // Send notification to user about a matching listing + async sendMatchNotification(telegramId, listing, notification) { + try { + const message = this.formatMatchNotification(listing, notification); + + // Create inline keyboard with action buttons + const keyboard = { + inline_keyboard: [ + [ + { + text: '๐ŸŒ View Details', + url: listing.url || `${process.env.WEBSITE_URL || 'https://yaltipia.com'}/listing/${listing.id}` + } + ], + [ + { text: '๐Ÿ“‹ View My Notifications', callback_data: 'view_notifications' }, + { text: '๐Ÿ”” Create New Alert', callback_data: 'create_notification' } + ] + ] + }; + + await this.bot.sendMessage(telegramId, message, { + reply_markup: keyboard, + parse_mode: 'HTML' + }); + + console.log(`โœ… Sent notification to user ${telegramId} for listing ${listing.id}`); + + } catch (error) { + console.error(`โŒ Failed to send notification to user ${telegramId}:`, error); + } + } + + // Format the notification message + formatMatchNotification(listing, notification) { + const title = listing.title || listing.name || 'Property'; + const price = listing.price ? `๐Ÿ’ฐ ${listing.price} ETB` : '๐Ÿ’ฐ Price not specified'; + const location = listing.subcity || listing.area || 'Location not specified'; + const houseType = listing.houseType || listing.type || 'Type not specified'; + const status = listing.status || 'Available'; + + return `๐ŸŽฏ New Match Found!\n\n` + + `๐Ÿ  ${title}\n` + + `${price}\n` + + `๐Ÿ“ ${location}\n` + + `๐Ÿก ${houseType}\n` + + `๐Ÿ“Š Status: ${status}\n\n` + + `๐Ÿ“‹ Matched your notification: "${notification.name}"\n\n` + + `Click "View Details" to see more information!`; + } + + // Get service status + getStatus() { + return { + isRunning: this.isRunning, + lastCheckTime: this.lastCheckTime, + checkInterval: this.CHECK_INTERVAL_MS, + processedListingsCount: this.processedListings.size + }; + } + + // Manual trigger for testing + async triggerManualCheck() { + console.log('๐Ÿ”ง Manual notification check triggered'); + await this.checkForNewMatches(); + } +} + +module.exports = AutomaticNotificationService; \ No newline at end of file diff --git a/src/services/optimizedAutomaticNotificationService.js b/src/services/optimizedAutomaticNotificationService.js new file mode 100644 index 0000000..b327327 --- /dev/null +++ b/src/services/optimizedAutomaticNotificationService.js @@ -0,0 +1,418 @@ +const axios = require('axios'); + +class OptimizedAutomaticNotificationService { + constructor(bot, api, userSessions) { + this.bot = bot; + this.api = api; + this.userSessions = userSessions; + this.isRunning = false; + this.checkInterval = null; + this.lastCheckTime = new Date(Date.now() - 24 * 60 * 60 * 1000); + this.processedListings = new Set(); + this.knownTelegramUsers = new Set(); // Track all telegram users who have used the bot + + // Configuration optimized for your API structure + this.CHECK_INTERVAL_MS = 8 * 60 * 1000; // Check every 8 minutes + this.MAX_NOTIFICATIONS_PER_USER = 3; + } + + start() { + if (this.isRunning) { + console.log('Optimized automatic notification service is already running'); + return; + } + + console.log('๐Ÿš€ Starting optimized automatic notification service...'); + this.isRunning = true; + + // Load known telegram users from sessions + this.loadKnownTelegramUsers(); + + // Run initial check after a delay + setTimeout(() => { + this.checkForNewMatches(); + }, 30000); + + // Set up interval for regular checks + this.checkInterval = setInterval(() => { + this.checkForNewMatches(); + }, this.CHECK_INTERVAL_MS); + + console.log(`โœ… Optimized automatic notification service started (checking every ${this.CHECK_INTERVAL_MS / 60000} minutes)`); + } + + stop() { + if (!this.isRunning) { + console.log('Optimized automatic notification service is not running'); + return; + } + + console.log('๐Ÿ›‘ Stopping optimized automatic notification service...'); + this.isRunning = false; + + if (this.checkInterval) { + clearInterval(this.checkInterval); + this.checkInterval = null; + } + + console.log('โœ… Optimized automatic notification service stopped'); + } + + // Load known telegram users from current sessions and expand over time + loadKnownTelegramUsers() { + // Add currently logged-in users + const currentUsers = Array.from(this.userSessions.keys()); + currentUsers.forEach(telegramId => this.knownTelegramUsers.add(telegramId)); + + console.log(`Loaded ${this.knownTelegramUsers.size} known telegram users`); + } + + // Main method optimized for your API structure + async checkForNewMatches() { + if (!this.isRunning) return; + + try { + console.log('๐Ÿ” Checking for new listing matches (optimized for your API)...'); + + // Get all listings and filter for recent ones + const allListings = await this.getAllListings(); + + if (!allListings || allListings.length === 0) { + console.log('No listings found'); + return; + } + + // Filter for listings newer than last check + const recentListings = this.filterRecentListings(allListings); + + if (recentListings.length === 0) { + console.log('No new listings since last check'); + this.lastCheckTime = new Date(); + return; + } + + console.log(`Found ${recentListings.length} new listings since last check`); + + // Get active notifications using your telegram/{telegramUserId} endpoint + const activeNotifications = await this.getAllActiveNotificationsOptimized(); + + if (!activeNotifications || activeNotifications.length === 0) { + console.log('No active notifications found'); + this.lastCheckTime = new Date(); + return; + } + + console.log(`Found ${activeNotifications.length} active notifications`); + + // Check each new listing against all notifications + let totalMatches = 0; + + for (const listing of recentListings) { + if (this.processedListings.has(listing.id)) { + continue; + } + + const matches = this.findMatchingNotifications(listing, activeNotifications); + + if (matches.length > 0) { + console.log(`Listing ${listing.id} matches ${matches.length} notifications`); + + // Send notifications with rate limiting + for (const match of matches.slice(0, this.MAX_NOTIFICATIONS_PER_USER)) { + await this.sendMatchNotification(match.telegramId, listing, match.notification); + totalMatches++; + await this.sleep(1000); // Rate limiting + } + } + + this.processedListings.add(listing.id); + } + + // Cleanup old processed listings + if (this.processedListings.size > 500) { + const oldEntries = Array.from(this.processedListings).slice(0, 250); + oldEntries.forEach(id => this.processedListings.delete(id)); + } + + console.log(`โœ… Optimized notification check complete. Sent ${totalMatches} notifications`); + this.lastCheckTime = new Date(); + + } catch (error) { + console.error('โŒ Error in optimized automatic notification check:', error); + } + } + + // Optimized method using your telegram/{telegramUserId} API endpoint + async getAllActiveNotificationsOptimized() { + const allNotifications = []; + + // Update known users with current sessions + this.loadKnownTelegramUsers(); + + // Get notifications for all known telegram users using your API + for (const telegramId of this.knownTelegramUsers) { + try { + console.log(`Fetching notifications for telegram user: ${telegramId}`); + + // Use your /api/telegram-notifications/telegram/{telegramUserId} endpoint + const result = await this.api.getNotificationsByTelegramId(telegramId); + + if (result.success && result.notifications && Array.isArray(result.notifications)) { + // Add telegramId to each notification + result.notifications.forEach(notification => { + notification.telegramId = telegramId; + }); + + // Filter for active notifications + const activeNotifications = result.notifications.filter(notification => { + return notification.status === 'ACTIVE' || + notification.isActive === true || + notification.active === true || + !notification.hasOwnProperty('status'); + }); + + allNotifications.push(...activeNotifications); + console.log(`Found ${activeNotifications.length} active notifications for user ${telegramId}`); + } + } catch (error) { + console.error(`Error getting notifications for telegram user ${telegramId}:`, error); + // Don't fail the entire process for one user + } + } + + return allNotifications; + } + + // Get all listings using existing API + async getAllListings() { + try { + const result = await this.api.getListings(null, {}); + + if (!result.success) { + console.error('Failed to get listings:', result.error); + return []; + } + + return result.listings || []; + + } catch (error) { + console.error('Error getting all listings:', error); + return []; + } + } + + // Filter listings for recent ones (since last check) + filterRecentListings(listings) { + const recentListings = []; + + for (const listing of listings) { + try { + const listingDate = this.getListingDate(listing); + + if (listingDate && listingDate > this.lastCheckTime) { + recentListings.push(listing); + } + } catch (error) { + console.error('Error parsing listing date:', error); + // Include it to be safe + recentListings.push(listing); + } + } + + return recentListings; + } + + // Extract date from listing (try multiple fields) + getListingDate(listing) { + const dateFields = ['createdAt', 'updatedAt', 'dateAdded', 'created_at', 'updated_at', 'date_added']; + + for (const field of dateFields) { + if (listing[field]) { + const date = new Date(listing[field]); + if (!isNaN(date.getTime())) { + return date; + } + } + } + + // If no date field found, assume it's recent + return new Date(Date.now() - 60 * 60 * 1000); + } + + // Find notifications that match a given listing + findMatchingNotifications(listing, notifications) { + const matches = []; + + for (const notification of notifications) { + if (this.doesListingMatchNotification(listing, notification)) { + matches.push({ + telegramId: notification.telegramId, + notification: notification + }); + } + } + + return matches; + } + + // Enhanced matching algorithm + doesListingMatchNotification(listing, notification) { + try { + // Check property type + if (notification.type) { + const notifType = notification.type.toUpperCase(); + const listingType = (listing.type || '').toUpperCase(); + if (notifType !== listingType) { + return false; + } + } + + // Check status - be flexible with status matching + if (notification.status && notification.status !== 'ACTIVE') { + const notifStatus = notification.status.toUpperCase(); + const listingStatus = (listing.status || '').toUpperCase(); + if (notifStatus !== listingStatus) { + return false; + } + } + + // Check subcity/area with flexible matching + if (notification.subcity && + notification.subcity.toLowerCase() !== 'any' && + notification.subcity.trim() !== '') { + + const notifArea = notification.subcity.toLowerCase().trim(); + const listingArea = (listing.subcity || listing.area || '').toLowerCase().trim(); + + if (listingArea && !listingArea.includes(notifArea) && !notifArea.includes(listingArea)) { + return false; + } + } + + // Check house type with flexible matching + if (notification.houseType && + notification.houseType.toLowerCase() !== 'any' && + notification.houseType.trim() !== '') { + + const notifHouseType = notification.houseType.toLowerCase().trim(); + const listingHouseType = (listing.houseType || listing.propertyType || '').toLowerCase().trim(); + + if (listingHouseType && !listingHouseType.includes(notifHouseType) && !notifHouseType.includes(listingHouseType)) { + return false; + } + } + + // Check price range + const listingPrice = this.parsePrice(listing.price); + + if (notification.minPrice && listingPrice > 0 && listingPrice < notification.minPrice) { + return false; + } + + if (notification.maxPrice && listingPrice > 0 && listingPrice > notification.maxPrice) { + return false; + } + + return true; + + } catch (error) { + console.error('Error matching listing to notification:', error); + return false; + } + } + + // Parse price from various formats + parsePrice(price) { + if (!price) return 0; + if (typeof price === 'number') return price; + + if (typeof price === 'string') { + const numStr = price.replace(/[^\d.]/g, ''); + const num = parseFloat(numStr); + return isNaN(num) ? 0 : num; + } + + return 0; + } + + // Send notification to user + async sendMatchNotification(telegramId, listing, notification) { + try { + const message = this.formatMatchNotification(listing, notification); + + const keyboard = { + inline_keyboard: [ + [ + { + text: '๐ŸŒ View Details', + url: listing.url || `${process.env.WEBSITE_URL || 'https://yaltipia.com'}/listing/${listing.id}` + } + ], + [ + { text: '๐Ÿ“‹ My Notifications', callback_data: 'view_notifications' } + ] + ] + }; + + await this.bot.sendMessage(telegramId, message, { + reply_markup: keyboard, + parse_mode: 'HTML' + }); + + console.log(`โœ… Sent notification to user ${telegramId} for listing ${listing.id}`); + + } catch (error) { + console.error(`โŒ Failed to send notification to user ${telegramId}:`, error); + } + } + + // Format notification message + formatMatchNotification(listing, notification) { + const title = listing.title || listing.name || 'Property'; + const price = listing.price ? `๐Ÿ’ฐ ${listing.price}` : '๐Ÿ’ฐ Price not specified'; + const location = listing.subcity || listing.area || 'Location not specified'; + const houseType = listing.houseType || listing.propertyType || listing.type || 'Type not specified'; + const status = listing.status || 'Available'; + + return `๐ŸŽฏ New Match Found!\n\n` + + `๐Ÿ  ${title}\n` + + `${price}\n` + + `๐Ÿ“ ${location}\n` + + `๐Ÿก ${houseType}\n` + + `๐Ÿ“Š Status: ${status}\n\n` + + `๐Ÿ“‹ Matched: "${notification.name || 'Your notification'}"\n\n` + + `Click "View Details" for more info!`; + } + + // Utility function for delays + sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // Get service status + getStatus() { + return { + isRunning: this.isRunning, + lastCheckTime: this.lastCheckTime, + checkInterval: this.CHECK_INTERVAL_MS, + processedListingsCount: this.processedListings.size, + knownTelegramUsersCount: this.knownTelegramUsers.size, + mode: 'optimized', + activeSessionsCount: this.userSessions.size + }; + } + + // Manual trigger for testing + async triggerManualCheck() { + console.log('๐Ÿ”ง Manual notification check triggered (optimized mode)'); + await this.checkForNewMatches(); + } + + // Add a telegram user to known users (call this when users register/login) + addKnownTelegramUser(telegramId) { + this.knownTelegramUsers.add(telegramId); + console.log(`Added telegram user ${telegramId} to known users. Total: ${this.knownTelegramUsers.size}`); + } +} + +module.exports = OptimizedAutomaticNotificationService; \ No newline at end of file diff --git a/src/services/simpleAutomaticNotificationService.js b/src/services/simpleAutomaticNotificationService.js new file mode 100644 index 0000000..b841332 --- /dev/null +++ b/src/services/simpleAutomaticNotificationService.js @@ -0,0 +1,437 @@ +const axios = require('axios'); + +class SimpleAutomaticNotificationService { + constructor(bot, api, userSessions) { + this.bot = bot; + this.api = api; + this.userSessions = userSessions; + this.isRunning = false; + this.checkInterval = null; + this.lastCheckTime = new Date(Date.now() - 24 * 60 * 60 * 1000); // Start from 24 hours ago + this.processedListings = new Set(); + + // Configuration - more conservative for simple version + this.CHECK_INTERVAL_MS = 10 * 60 * 1000; // Check every 10 minutes + this.MAX_NOTIFICATIONS_PER_USER = 3; // Reduced to be less spammy + } + + // Start the simple automatic notification service + start() { + if (this.isRunning) { + console.log('Simple automatic notification service is already running'); + return; + } + + console.log('๐Ÿš€ Starting simple automatic notification service...'); + this.isRunning = true; + + // Run initial check after a delay + setTimeout(() => { + this.checkForNewMatches(); + }, 30000); // Wait 30 seconds before first check + + // Set up interval for regular checks + this.checkInterval = setInterval(() => { + this.checkForNewMatches(); + }, this.CHECK_INTERVAL_MS); + + console.log(`โœ… Simple automatic notification service started (checking every ${this.CHECK_INTERVAL_MS / 60000} minutes)`); + } + + // Stop the automatic notification service + stop() { + if (!this.isRunning) { + console.log('Simple automatic notification service is not running'); + return; + } + + console.log('๐Ÿ›‘ Stopping simple automatic notification service...'); + this.isRunning = false; + + if (this.checkInterval) { + clearInterval(this.checkInterval); + this.checkInterval = null; + } + + console.log('โœ… Simple automatic notification service stopped'); + } + + // Main method to check for new matches - simplified version + async checkForNewMatches() { + if (!this.isRunning) return; + + try { + console.log('๐Ÿ” Checking for new listing matches (simple mode)...'); + + // Get all listings and filter for recent ones + const allListings = await this.getAllListings(); + + if (!allListings || allListings.length === 0) { + console.log('No listings found'); + return; + } + + // Filter for listings newer than last check + const recentListings = this.filterRecentListings(allListings); + + if (recentListings.length === 0) { + console.log('No new listings since last check'); + this.lastCheckTime = new Date(); + return; + } + + console.log(`Found ${recentListings.length} new listings since last check`); + + // Get active notifications from current user sessions + const activeNotifications = await this.getActiveNotificationsFromSessions(); + + if (!activeNotifications || activeNotifications.length === 0) { + console.log('No active notifications from logged-in users'); + this.lastCheckTime = new Date(); + return; + } + + console.log(`Found ${activeNotifications.length} active notifications from logged-in users`); + + // Check each new listing against all notifications + let totalMatches = 0; + + for (const listing of recentListings) { + // Skip if we've already processed this listing + if (this.processedListings.has(listing.id)) { + continue; + } + + const matches = this.findMatchingNotifications(listing, activeNotifications); + + if (matches.length > 0) { + console.log(`Listing ${listing.id} matches ${matches.length} notifications`); + + // Send notifications to matched users (with rate limiting) + for (const match of matches.slice(0, this.MAX_NOTIFICATIONS_PER_USER)) { + await this.sendMatchNotification(match.telegramId, listing, match.notification); + totalMatches++; + + // Small delay between notifications to avoid rate limiting + await this.sleep(1000); + } + } + + // Mark listing as processed + this.processedListings.add(listing.id); + } + + // Clean up old processed listings (keep only last 500) + if (this.processedListings.size > 500) { + const oldEntries = Array.from(this.processedListings).slice(0, 250); + oldEntries.forEach(id => this.processedListings.delete(id)); + } + + console.log(`โœ… Simple notification check complete. Sent ${totalMatches} notifications`); + this.lastCheckTime = new Date(); + + } catch (error) { + console.error('โŒ Error in simple automatic notification check:', error); + } + } + + // Get all listings using existing API + async getAllListings() { + try { + const result = await this.api.getListings(null, {}); + + if (!result.success) { + console.error('Failed to get listings:', result.error); + return []; + } + + return result.listings || []; + + } catch (error) { + console.error('Error getting all listings:', error); + return []; + } + } + + // Filter listings for recent ones (since last check) + filterRecentListings(listings) { + const recentListings = []; + + for (const listing of listings) { + try { + // Try different possible date fields + const listingDate = this.getListingDate(listing); + + if (listingDate && listingDate > this.lastCheckTime) { + recentListings.push(listing); + } + } catch (error) { + console.error('Error parsing listing date:', error); + // If we can't parse the date, include it to be safe + recentListings.push(listing); + } + } + + return recentListings; + } + + // Extract date from listing (try multiple fields) + getListingDate(listing) { + // Try different possible date field names + const dateFields = ['createdAt', 'updatedAt', 'dateAdded', 'created_at', 'updated_at', 'date_added']; + + for (const field of dateFields) { + if (listing[field]) { + const date = new Date(listing[field]); + if (!isNaN(date.getTime())) { + return date; + } + } + } + + // If no date field found, assume it's recent (within last hour) + return new Date(Date.now() - 60 * 60 * 1000); + } + + // Get active notifications from all known telegram users (optimized for your API) + async getActiveNotificationsFromAllUsers() { + const allNotifications = []; + + // Get notifications for currently logged-in users + const loggedInUsers = await this.getActiveNotificationsFromSessions(); + allNotifications.push(...loggedInUsers); + + // Additionally, we could maintain a list of all telegram user IDs + // who have ever used the bot and check their notifications too + // This would require storing telegram IDs in a persistent way + + return allNotifications; + } + + // Enhanced method to get notifications using your telegram/{telegramUserId} endpoint + async getNotificationsByTelegramId(telegramUserId) { + try { + console.log(`Getting notifications for telegram user: ${telegramUserId}`); + + // Use your existing API endpoint + const result = await this.api.getNotificationsByTelegramId(telegramUserId); + + if (result.success && result.notifications) { + // Add telegramId to each notification for easy access + result.notifications.forEach(notification => { + notification.telegramId = telegramUserId; + }); + + return result.notifications.filter(notification => { + // Filter for active notifications + return notification.status === 'ACTIVE' || + notification.isActive === true || + notification.active === true || + !notification.hasOwnProperty('status'); + }); + } + + return []; + } catch (error) { + console.error(`Error getting notifications for telegram user ${telegramUserId}:`, error); + return []; + } + } + // Get active notifications from current user sessions only + async getActiveNotificationsFromSessions() { + const allNotifications = []; + + // Only check notifications for users who are currently logged in + const telegramIds = Array.from(this.userSessions.keys()); + + for (const telegramId of telegramIds) { + try { + const userSession = this.userSessions.get(telegramId); + if (userSession && userSession.user) { + // Use the optimized telegram ID endpoint + const notifications = await this.getNotificationsByTelegramId(telegramId); + allNotifications.push(...notifications); + } + } catch (error) { + console.error(`Error getting notifications for user ${telegramId}:`, error); + } + } + + return allNotifications; + } + + // Find notifications that match a given listing + findMatchingNotifications(listing, notifications) { + const matches = []; + + for (const notification of notifications) { + if (this.doesListingMatchNotification(listing, notification)) { + matches.push({ + telegramId: notification.telegramId, + userId: notification.userId, + notification: notification + }); + } + } + + return matches; + } + + // Check if a listing matches a notification's criteria (simplified) + doesListingMatchNotification(listing, notification) { + try { + // Check property type (more flexible matching) + if (notification.type) { + const notifType = notification.type.toUpperCase(); + const listingType = (listing.type || '').toUpperCase(); + if (notifType !== listingType) { + return false; + } + } + + // Check status (more flexible matching) + if (notification.status && notification.status !== 'ACTIVE') { + const notifStatus = notification.status.toUpperCase(); + const listingStatus = (listing.status || '').toUpperCase(); + if (notifStatus !== listingStatus) { + return false; + } + } + + // Check subcity/area (flexible matching) + if (notification.subcity && + notification.subcity.toLowerCase() !== 'any' && + notification.subcity.trim() !== '') { + + const notifArea = notification.subcity.toLowerCase().trim(); + const listingArea = (listing.subcity || listing.area || '').toLowerCase().trim(); + + if (listingArea && !listingArea.includes(notifArea) && !notifArea.includes(listingArea)) { + return false; + } + } + + // Check house type (flexible matching) + if (notification.houseType && + notification.houseType.toLowerCase() !== 'any' && + notification.houseType.trim() !== '') { + + const notifHouseType = notification.houseType.toLowerCase().trim(); + const listingHouseType = (listing.houseType || listing.propertyType || '').toLowerCase().trim(); + + if (listingHouseType && !listingHouseType.includes(notifHouseType) && !notifHouseType.includes(listingHouseType)) { + return false; + } + } + + // Check price range (flexible parsing) + const listingPrice = this.parsePrice(listing.price); + + if (notification.minPrice && listingPrice > 0 && listingPrice < notification.minPrice) { + return false; + } + + if (notification.maxPrice && listingPrice > 0 && listingPrice > notification.maxPrice) { + return false; + } + + return true; + + } catch (error) { + console.error('Error matching listing to notification:', error); + return false; + } + } + + // Parse price from various formats + parsePrice(price) { + if (!price) return 0; + + // If it's already a number + if (typeof price === 'number') return price; + + // If it's a string, try to extract number + if (typeof price === 'string') { + const numStr = price.replace(/[^\d.]/g, ''); + const num = parseFloat(numStr); + return isNaN(num) ? 0 : num; + } + + return 0; + } + + // Send notification to user about a matching listing + async sendMatchNotification(telegramId, listing, notification) { + try { + const message = this.formatMatchNotification(listing, notification); + + // Create inline keyboard with action buttons + const keyboard = { + inline_keyboard: [ + [ + { + text: '๐ŸŒ View Details', + url: listing.url || `${process.env.WEBSITE_URL || 'https://yaltipia.com'}/listing/${listing.id}` + } + ], + [ + { text: '๐Ÿ“‹ My Notifications', callback_data: 'view_notifications' } + ] + ] + }; + + await this.bot.sendMessage(telegramId, message, { + reply_markup: keyboard, + parse_mode: 'HTML' + }); + + console.log(`โœ… Sent notification to user ${telegramId} for listing ${listing.id}`); + + } catch (error) { + console.error(`โŒ Failed to send notification to user ${telegramId}:`, error); + } + } + + // Format the notification message (simplified) + formatMatchNotification(listing, notification) { + const title = listing.title || listing.name || 'Property'; + const price = listing.price ? `๐Ÿ’ฐ ${listing.price}` : '๐Ÿ’ฐ Price not specified'; + const location = listing.subcity || listing.area || 'Location not specified'; + const houseType = listing.houseType || listing.propertyType || listing.type || 'Type not specified'; + const status = listing.status || 'Available'; + + return `๐ŸŽฏ New Match Found!\n\n` + + `๐Ÿ  ${title}\n` + + `${price}\n` + + `๐Ÿ“ ${location}\n` + + `๐Ÿก ${houseType}\n` + + `๐Ÿ“Š Status: ${status}\n\n` + + `๐Ÿ“‹ Matched: "${notification.name || 'Your notification'}"\n\n` + + `Click "View Details" for more info!`; + } + + // Utility function for delays + sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // Get service status + getStatus() { + return { + isRunning: this.isRunning, + lastCheckTime: this.lastCheckTime, + checkInterval: this.CHECK_INTERVAL_MS, + processedListingsCount: this.processedListings.size, + mode: 'simple', + activeSessionsCount: this.userSessions.size + }; + } + + // Manual trigger for testing + async triggerManualCheck() { + console.log('๐Ÿ”ง Manual notification check triggered (simple mode)'); + await this.checkForNewMatches(); + } +} + +module.exports = SimpleAutomaticNotificationService; \ No newline at end of file diff --git a/src/webhook/listingWebhook.js b/src/webhook/listingWebhook.js new file mode 100644 index 0000000..0fb9864 --- /dev/null +++ b/src/webhook/listingWebhook.js @@ -0,0 +1,142 @@ +const express = require('express'); +const router = express.Router(); + +class ListingWebhookHandler { + constructor(automaticNotificationService) { + this.automaticNotificationService = automaticNotificationService; + } + + // Handle new listing webhook + async handleNewListing(req, res) { + try { + console.log('๐Ÿ“ฅ Received new listing webhook:', req.body); + + const listing = req.body; + + // Validate required fields + if (!listing.id) { + return res.status(400).json({ + success: false, + error: 'Listing ID is required' + }); + } + + // Trigger immediate check for this specific listing + await this.checkListingAgainstNotifications(listing); + + res.json({ + success: true, + message: 'Listing processed successfully', + listingId: listing.id + }); + + } catch (error) { + console.error('โŒ Error processing listing webhook:', error); + res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } + } + + // Check a specific listing against all notifications + async checkListingAgainstNotifications(listing) { + try { + console.log(`๐Ÿ” Checking listing ${listing.id} against all notifications`); + + // Get all active notifications + const activeNotifications = await this.automaticNotificationService.getAllActiveNotifications(); + + if (!activeNotifications || activeNotifications.length === 0) { + console.log('No active notifications found'); + return; + } + + // Find matching notifications + const matches = await this.automaticNotificationService.findMatchingNotifications( + listing, + activeNotifications + ); + + if (matches.length === 0) { + console.log(`No matches found for listing ${listing.id}`); + return; + } + + console.log(`Found ${matches.length} matches for listing ${listing.id}`); + + // Send notifications to matched users + for (const match of matches) { + await this.automaticNotificationService.sendMatchNotification( + match.telegramId, + listing, + match.notification + ); + } + + console.log(`โœ… Sent ${matches.length} notifications for listing ${listing.id}`); + + } catch (error) { + console.error('Error checking listing against notifications:', error); + } + } + + // Handle listing update webhook + async handleListingUpdate(req, res) { + try { + console.log('๐Ÿ“ Received listing update webhook:', req.body); + + const listing = req.body; + + if (!listing.id) { + return res.status(400).json({ + success: false, + error: 'Listing ID is required' + }); + } + + // For updates, we might want to check if the listing now matches + // notifications that it didn't match before (e.g., price change) + await this.checkListingAgainstNotifications(listing); + + res.json({ + success: true, + message: 'Listing update processed successfully', + listingId: listing.id + }); + + } catch (error) { + console.error('โŒ Error processing listing update webhook:', error); + res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } + } + + // Setup webhook routes + setupRoutes() { + // Webhook for new listings + router.post('/new-listing', (req, res) => { + this.handleNewListing(req, res); + }); + + // Webhook for listing updates + router.post('/update-listing', (req, res) => { + this.handleListingUpdate(req, res); + }); + + // Health check endpoint + router.get('/health', (req, res) => { + res.json({ + success: true, + message: 'Webhook service is running', + timestamp: new Date().toISOString() + }); + }); + + return router; + } +} + +module.exports = ListingWebhookHandler; \ No newline at end of file diff --git a/src/webhookServer.js b/src/webhookServer.js new file mode 100644 index 0000000..bd3be59 --- /dev/null +++ b/src/webhookServer.js @@ -0,0 +1,117 @@ +const express = require('express'); +const cors = require('cors'); +const ListingWebhookHandler = require('./webhook/listingWebhook'); + +class WebhookServer { + constructor(automaticNotificationService, port = 3001) { + this.app = express(); + this.port = port; + this.server = null; + this.automaticNotificationService = automaticNotificationService; + + this.setupMiddleware(); + this.setupRoutes(); + } + + setupMiddleware() { + // Enable CORS + this.app.use(cors()); + + // Parse JSON bodies + this.app.use(express.json({ limit: '10mb' })); + + // Parse URL-encoded bodies + this.app.use(express.urlencoded({ extended: true })); + + // Request logging + this.app.use((req, res, next) => { + console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); + next(); + }); + } + + setupRoutes() { + // Health check + this.app.get('/', (req, res) => { + res.json({ + success: true, + message: 'Yaltipia Bot Webhook Server', + timestamp: new Date().toISOString(), + endpoints: [ + 'POST /webhook/new-listing', + 'POST /webhook/update-listing', + 'GET /webhook/health', + 'GET /status' + ] + }); + }); + + // Status endpoint + this.app.get('/status', (req, res) => { + const notificationStatus = this.automaticNotificationService.getStatus(); + + res.json({ + success: true, + webhook: { + running: true, + port: this.port + }, + automaticNotifications: notificationStatus + }); + }); + + // Setup webhook routes + const webhookHandler = new ListingWebhookHandler(this.automaticNotificationService); + this.app.use('/webhook', webhookHandler.setupRoutes()); + + // Error handling middleware + this.app.use((error, req, res, next) => { + console.error('Webhook server error:', error); + res.status(500).json({ + success: false, + error: 'Internal server error' + }); + }); + + // 404 handler + this.app.use((req, res) => { + res.status(404).json({ + success: false, + error: 'Endpoint not found' + }); + }); + } + + start() { + return new Promise((resolve, reject) => { + this.server = this.app.listen(this.port, (error) => { + if (error) { + console.error('โŒ Failed to start webhook server:', error); + reject(error); + } else { + console.log(`๐ŸŒ Webhook server started on port ${this.port}`); + console.log(`๐Ÿ“ก Webhook endpoints available at:`); + console.log(` - POST http://localhost:${this.port}/webhook/new-listing`); + console.log(` - POST http://localhost:${this.port}/webhook/update-listing`); + console.log(` - GET http://localhost:${this.port}/status`); + resolve(); + } + }); + }); + } + + stop() { + return new Promise((resolve) => { + if (this.server) { + this.server.close(() => { + console.log('๐Ÿ›‘ Webhook server stopped'); + resolve(); + }); + } else { + resolve(); + } + }); + } +} + +module.exports = WebhookServer; \ No newline at end of file