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