feat: automatic notification services
This commit is contained in:
parent
c58ed95370
commit
613ac11e3f
|
|
@ -1,3 +1,5 @@
|
||||||
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
||||||
API_BASE_URL=your_backend_api_url_here
|
API_BASE_URL=your_backend_api_url_here
|
||||||
WEBSITE_URL=https://yaltipia.com/listings
|
WEBSITE_URL=https://yaltipia.com/listings
|
||||||
|
WEBHOOK_PORT=3001
|
||||||
|
NOTIFICATION_MODE=optimized
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -45,10 +45,7 @@ Thumbs.db
|
||||||
# .dockerignore
|
# .dockerignore
|
||||||
|
|
||||||
# Documentation (optional - remove if you want to track them)
|
# Documentation (optional - remove if you want to track them)
|
||||||
BACKEND_API_INTEGRATION.md
|
docs/
|
||||||
ERROR_HANDLING_IMPROVEMENTS.md
|
|
||||||
WEBSITE_INTEGRATION.md
|
|
||||||
SECURITY_AUDIT_REPORT.md
|
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
tmp/
|
tmp/
|
||||||
|
|
|
||||||
538
package-lock.json
generated
538
package-lock.json
generated
|
|
@ -11,8 +11,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
"node-telegram-bot-api": "^0.67.0",
|
"node-telegram-bot-api": "^0.67.0",
|
||||||
"validator": "^13.11.0"
|
"validator": "^13.11.0"
|
||||||
},
|
},
|
||||||
|
|
@ -108,6 +110,19 @@
|
||||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
|
@ -227,6 +242,12 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/array.prototype.findindex": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/array.prototype.findindex/-/array.prototype.findindex-2.2.4.tgz",
|
"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==",
|
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
|
@ -420,6 +480,15 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
|
|
@ -540,12 +609,61 @@
|
||||||
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
|
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/core-util-is": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/crypto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
|
||||||
|
|
@ -674,6 +792,25 @@
|
||||||
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
|
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
|
@ -719,12 +856,27 @@
|
||||||
"safer-buffer": "^2.1.0"
|
"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": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/end-of-stream": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
"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"
|
"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": {
|
"node_modules/eventemitter3": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
|
||||||
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==",
|
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/extend": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
|
|
@ -933,6 +1161,39 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.11",
|
"version": "1.15.11",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
|
@ -993,6 +1254,24 @@
|
||||||
"node": ">= 6"
|
"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": {
|
"node_modules/fs-minipass": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||||
|
|
@ -1350,6 +1629,26 @@
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/http-signature": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz",
|
"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": {
|
"node_modules/ignore-by-default": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||||
|
|
@ -1432,6 +1743,15 @@
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
|
|
@ -1906,6 +2226,33 @@
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/mime": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
|
|
@ -2003,6 +2350,15 @@
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/node-addon-api": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
"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"
|
"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": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
|
@ -2220,6 +2588,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/path-is-absolute": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
|
@ -2229,6 +2606,12 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/performance-now": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
"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==",
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
|
@ -2328,6 +2724,30 @@
|
||||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/readable-stream": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
|
@ -2658,6 +3078,60 @@
|
||||||
"node": ">=10"
|
"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": {
|
"node_modules/set-blocking": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
|
@ -2710,6 +3184,12 @@
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
|
@ -2826,6 +3306,15 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/stealthy-require": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
||||||
|
|
@ -3006,6 +3495,15 @@
|
||||||
"node": ">=8.0"
|
"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": {
|
"node_modules/touch": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
||||||
|
|
@ -3052,6 +3550,19 @@
|
||||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
|
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
|
||||||
"license": "Unlicense"
|
"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": {
|
"node_modules/typed-array-buffer": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
||||||
|
|
@ -3160,6 +3671,15 @@
|
||||||
"node": ">= 4.0.0"
|
"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": {
|
"node_modules/uri-js": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
|
|
@ -3186,6 +3706,15 @@
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/uuid": {
|
||||||
"version": "8.3.2",
|
"version": "8.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
|
@ -3204,6 +3733,15 @@
|
||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/verror": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,16 @@
|
||||||
"main": "src/bot.js",
|
"main": "src/bot.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/bot.js",
|
"start": "node src/bot.js",
|
||||||
"dev": "nodemon src/bot.js"
|
"dev": "nodemon src/bot.js",
|
||||||
|
"test:webhook": "node scripts/test-webhook.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
"node-telegram-bot-api": "^0.67.0",
|
"node-telegram-bot-api": "^0.67.0",
|
||||||
"validator": "^13.11.0"
|
"validator": "^13.11.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
#!
|
||||||
69
scripts/test-webhook.js
Normal file
69
scripts/test-webhook.js
Normal file
|
|
@ -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();
|
||||||
|
|
@ -50,6 +50,7 @@ class ApiClient {
|
||||||
name: userData.name,
|
name: userData.name,
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
phone: userData.phone,
|
phone: userData.phone,
|
||||||
|
role: userData.role || 'CUSTOMER',
|
||||||
telegramUserId: telegramUserId
|
telegramUserId: telegramUserId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -65,6 +66,7 @@ class ApiClient {
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
phone: userData.phone,
|
phone: userData.phone,
|
||||||
password: userData.password,
|
password: userData.password,
|
||||||
|
role: userData.role || 'CUSTOMER', // Include role in registration (uppercase)
|
||||||
telegramUserId: telegramUserId.toString()
|
telegramUserId: telegramUserId.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -153,7 +155,8 @@ class ApiClient {
|
||||||
phone: phone,
|
phone: phone,
|
||||||
name: response.data.user?.name || 'User',
|
name: response.data.user?.name || 'User',
|
||||||
email: response.data.user?.email,
|
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') {
|
} else if (response.data.hasAccount === false || response.data.flow === 'register') {
|
||||||
|
|
|
||||||
183
src/bot.js
183
src/bot.js
|
|
@ -2,6 +2,10 @@ require('dotenv').config();
|
||||||
const TelegramBot = require('node-telegram-bot-api');
|
const TelegramBot = require('node-telegram-bot-api');
|
||||||
const ApiClient = require('./api');
|
const ApiClient = require('./api');
|
||||||
const NotificationService = require('./services/notificationService');
|
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');
|
const ErrorHandler = require('./utils/errorHandler');
|
||||||
|
|
||||||
// Import feature modules
|
// Import feature modules
|
||||||
|
|
@ -28,6 +32,40 @@ class YaltipiaBot {
|
||||||
this.userStates = new Map(); // Store user conversation states
|
this.userStates = new Map(); // Store user conversation states
|
||||||
this.userSessions = new Map(); // Store user session data
|
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);
|
console.log('Bot initialized with notification service:', !!this.notificationService);
|
||||||
|
|
||||||
// Initialize features
|
// Initialize features
|
||||||
|
|
@ -40,6 +78,30 @@ class YaltipiaBot {
|
||||||
|
|
||||||
this.setupHandlers();
|
this.setupHandlers();
|
||||||
this.setupErrorHandlers();
|
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() {
|
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
|
// Handle contact sharing
|
||||||
this.bot.on('contact', (msg) => {
|
this.bot.on('contact', (msg) => {
|
||||||
this.auth.handleContact(msg);
|
this.auth.handleContact(msg);
|
||||||
|
|
@ -138,6 +233,59 @@ class YaltipiaBot {
|
||||||
const telegramId = msg.from.id;
|
const telegramId = msg.from.id;
|
||||||
|
|
||||||
try {
|
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
|
// Try each feature's text handler in order
|
||||||
const handled =
|
const handled =
|
||||||
await this.auth.handleRegistrationText(msg) ||
|
await this.auth.handleRegistrationText(msg) ||
|
||||||
|
|
@ -148,7 +296,7 @@ class YaltipiaBot {
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
// If no feature handled the message and user is authenticated, show menu
|
// If no feature handled the message and user is authenticated, show menu
|
||||||
if (this.auth.isAuthenticated(telegramId)) {
|
if (this.auth.isAuthenticated(telegramId)) {
|
||||||
this.menu.showMainMenu(chatId);
|
this.menu.showMainMenu(chatId, telegramId);
|
||||||
} else {
|
} else {
|
||||||
this.bot.sendMessage(chatId, 'Please start with /start to register or login.');
|
this.bot.sendMessage(chatId, 'Please start with /start to register or login.');
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +351,7 @@ class YaltipiaBot {
|
||||||
case 'save_notification':
|
case 'save_notification':
|
||||||
handled = await this.notifications.saveNotification(chatId, telegramId);
|
handled = await this.notifications.saveNotification(chatId, telegramId);
|
||||||
if (handled) {
|
if (handled) {
|
||||||
this.menu.showMainMenu(chatId);
|
this.menu.showMainMenu(chatId, telegramId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -264,7 +412,7 @@ class YaltipiaBot {
|
||||||
console.log(`Unhandled callback query: ${data}`);
|
console.log(`Unhandled callback query: ${data}`);
|
||||||
// Try to show main menu as fallback
|
// Try to show main menu as fallback
|
||||||
if (this.auth.isAuthenticated(telegramId)) {
|
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...');
|
console.log('🤖 Yaltipia Telegram Bot is running...');
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', async () => {
|
||||||
console.log('Shutting down bot...');
|
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);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
@ -15,7 +15,7 @@ class AuthFeature {
|
||||||
// Check if user is already logged in
|
// Check if user is already logged in
|
||||||
const existingSession = this.userSessions.get(telegramId);
|
const existingSession = this.userSessions.get(telegramId);
|
||||||
if (existingSession && existingSession.user) {
|
if (existingSession && existingSession.user) {
|
||||||
this.showMainMenu(chatId);
|
this.showMainMenu(chatId, telegramId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ class AuthFeature {
|
||||||
this.bot.sendMessage(chatId,
|
this.bot.sendMessage(chatId,
|
||||||
`✅ You are already logged in as ${existingSession.user.name || 'User'}!`
|
`✅ You are already logged in as ${existingSession.user.name || 'User'}!`
|
||||||
);
|
);
|
||||||
this.showMainMenu(chatId);
|
this.showMainMenu(chatId, telegramId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ class AuthFeature {
|
||||||
}
|
}
|
||||||
|
|
||||||
const phoneNumber = msg.contact.phone_number;
|
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 {
|
try {
|
||||||
// Check if user exists in backend database
|
// Check if user exists in backend database
|
||||||
|
|
@ -87,33 +87,56 @@ class AuthFeature {
|
||||||
console.log('User existence check result:', existingUser);
|
console.log('User existence check result:', existingUser);
|
||||||
|
|
||||||
if (existingUser.success) {
|
if (existingUser.success) {
|
||||||
// User exists in backend, ask for password to login
|
// User exists in backend, check their role
|
||||||
console.log('User found in database');
|
console.log('User found in database, checking role');
|
||||||
|
|
||||||
this.userStates.set(telegramId, {
|
const userRole = existingUser.user.role || 'CUSTOMER'; // Default to CUSTOMER if no role specified
|
||||||
step: 'waiting_password',
|
console.log('User role:', userRole);
|
||||||
userData: {
|
|
||||||
phone: phoneNumber,
|
|
||||||
telegramId: telegramId,
|
|
||||||
// We might not have the name yet if using /exists endpoint
|
|
||||||
name: existingUser.user.name || 'User'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const welcomeMessage = existingUser.user.name && existingUser.user.name !== 'User'
|
if (userRole.toLowerCase() === 'customer') {
|
||||||
? `Welcome back, ${existingUser.user.name}!`
|
// Role is customer, continue with login process
|
||||||
: 'Welcome back!';
|
console.log('User has customer role, proceeding with login');
|
||||||
|
|
||||||
this.bot.sendMessage(chatId,
|
this.userStates.set(telegramId, {
|
||||||
`✅ Phone number recognized!\n\n` +
|
step: 'waiting_password',
|
||||||
`${welcomeMessage}\n` +
|
userData: {
|
||||||
'🔐 Please enter your password to login:',
|
phone: phoneNumber,
|
||||||
{ reply_markup: { remove_keyboard: true } }
|
telegramId: telegramId,
|
||||||
);
|
name: existingUser.user.name || 'User',
|
||||||
return;
|
role: userRole
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
} 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');
|
console.log('User not found in database, proceeding with registration');
|
||||||
|
|
||||||
userState.phoneNumber = phoneNumber;
|
userState.phoneNumber = phoneNumber;
|
||||||
|
|
@ -282,13 +305,14 @@ class AuthFeature {
|
||||||
// Continue without token - user is authenticated but may need token for some operations
|
// 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, {
|
this.userSessions.set(telegramId, {
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name || userData.name || 'User',
|
name: user.name || userData.name || 'User',
|
||||||
email: user.email,
|
email: user.email,
|
||||||
phone: userData.phone
|
phone: userData.phone,
|
||||||
|
role: user.role || userData.role || 'CUSTOMER' // Store role in session
|
||||||
},
|
},
|
||||||
phoneNumber: userData.phone,
|
phoneNumber: userData.phone,
|
||||||
password: password,
|
password: password,
|
||||||
|
|
@ -309,7 +333,7 @@ class AuthFeature {
|
||||||
'You are now logged in.'
|
'You are now logged in.'
|
||||||
);
|
);
|
||||||
|
|
||||||
this.showMainMenu(chatId);
|
this.showMainMenu(chatId, telegramId);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
this.bot.sendMessage(chatId,
|
this.bot.sendMessage(chatId,
|
||||||
|
|
@ -352,12 +376,13 @@ class AuthFeature {
|
||||||
|
|
||||||
async completeRegistration(chatId, telegramId, userState) {
|
async completeRegistration(chatId, telegramId, userState) {
|
||||||
try {
|
try {
|
||||||
// Register with backend, passing telegramUserId
|
// Register with backend, passing telegramUserId and default role as CUSTOMER
|
||||||
const registrationResult = await this.api.registerUser({
|
const registrationResult = await this.api.registerUser({
|
||||||
name: userState.name,
|
name: userState.name,
|
||||||
email: userState.email,
|
email: userState.email,
|
||||||
phone: userState.phoneNumber,
|
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
|
}, telegramId); // Pass telegramUserId as second parameter
|
||||||
|
|
||||||
if (!registrationResult.success) {
|
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, {
|
this.userSessions.set(telegramId, {
|
||||||
user: {
|
user: {
|
||||||
id: user.id || registrationResult.data?.id,
|
id: user.id || registrationResult.data?.id,
|
||||||
name: userState.name,
|
name: userState.name,
|
||||||
email: userState.email,
|
email: userState.email,
|
||||||
phone: userState.phoneNumber
|
phone: userState.phoneNumber,
|
||||||
|
role: user.role || 'CUSTOMER' // Ensure CUSTOMER role is stored
|
||||||
},
|
},
|
||||||
phoneNumber: userState.phoneNumber,
|
phoneNumber: userState.phoneNumber,
|
||||||
password: userState.password,
|
password: userState.password,
|
||||||
|
|
@ -466,7 +492,8 @@ class AuthFeature {
|
||||||
if (!userSession || !userSession.user) {
|
if (!userSession || !userSession.user) {
|
||||||
this.bot.sendMessage(chatId,
|
this.bot.sendMessage(chatId,
|
||||||
'❌ You are not logged in.\n\n' +
|
'❌ 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -488,7 +515,8 @@ class AuthFeature {
|
||||||
this.bot.sendMessage(chatId,
|
this.bot.sendMessage(chatId,
|
||||||
`✅ Goodbye ${userName}!\n\n` +
|
`✅ Goodbye ${userName}!\n\n` +
|
||||||
'You have been logged out successfully.\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;
|
return true;
|
||||||
|
|
@ -508,22 +536,42 @@ class AuthFeature {
|
||||||
return;
|
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 = {
|
let 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' }]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
this.bot.sendMessage(chatId,
|
if (isCreatingNotification) {
|
||||||
'🏠 Yaltipia Home Bot - Main Menu\n\n' +
|
// Show cancel option during notification creation
|
||||||
'What would you like to do?',
|
keyboard = {
|
||||||
{ reply_markup: 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Validate and refresh session if needed
|
||||||
|
|
@ -564,6 +612,7 @@ class AuthFeature {
|
||||||
userId: session.user?.id,
|
userId: session.user?.id,
|
||||||
userName: session.user?.name,
|
userName: session.user?.name,
|
||||||
phone: session.phoneNumber,
|
phone: session.phoneNumber,
|
||||||
|
role: session.user?.role || 'CUSTOMER',
|
||||||
loginTime: session.loginTime
|
loginTime: session.loginTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,22 +7,43 @@ class MenuFeature {
|
||||||
showMainMenu(chatId, telegramId = null) {
|
showMainMenu(chatId, telegramId = null) {
|
||||||
// If telegramId is provided, we should validate session in auth feature
|
// If telegramId is provided, we should validate session in auth feature
|
||||||
// For now, just show the menu
|
// For now, just show the menu
|
||||||
const websiteUrl = process.env.WEBSITE_URL || 'https://yaltipia.com/listings';
|
|
||||||
|
|
||||||
const keyboard = {
|
// Check if user is in notification creation process
|
||||||
inline_keyboard: [
|
const userState = telegramId ? this.userStates.get(telegramId) : null;
|
||||||
[{ text: '🔔 Create Notification', callback_data: 'create_notification' }],
|
const isCreatingNotification = userState && userState.step && userState.step.startsWith('notification_');
|
||||||
[{ text: '📋 View My Notifications', callback_data: 'view_notifications' }],
|
|
||||||
[{ text: '🌐 Browse All Listings', url: websiteUrl }],
|
|
||||||
[{ text: '🚪 Logout', callback_data: 'logout' }]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
this.bot.sendMessage(chatId,
|
let keyboard;
|
||||||
'🏠 Yaltipia Home Bot - Main Menu\n\n' +
|
|
||||||
'What would you like to do?',
|
if (isCreatingNotification) {
|
||||||
{ reply_markup: keyboard }
|
// 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
async handleBackToMenu(chatId, telegramId) {
|
||||||
|
|
|
||||||
324
src/services/automaticNotificationService.js
Normal file
324
src/services/automaticNotificationService.js
Normal file
|
|
@ -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 `🎯 <b>New Match Found!</b>\n\n` +
|
||||||
|
`🏠 <b>${title}</b>\n` +
|
||||||
|
`${price}\n` +
|
||||||
|
`📍 ${location}\n` +
|
||||||
|
`🏡 ${houseType}\n` +
|
||||||
|
`📊 Status: ${status}\n\n` +
|
||||||
|
`📋 <i>Matched your notification: "${notification.name}"</i>\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;
|
||||||
418
src/services/optimizedAutomaticNotificationService.js
Normal file
418
src/services/optimizedAutomaticNotificationService.js
Normal file
|
|
@ -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 `🎯 <b>New Match Found!</b>\n\n` +
|
||||||
|
`🏠 <b>${title}</b>\n` +
|
||||||
|
`${price}\n` +
|
||||||
|
`📍 ${location}\n` +
|
||||||
|
`🏡 ${houseType}\n` +
|
||||||
|
`📊 Status: ${status}\n\n` +
|
||||||
|
`📋 <i>Matched: "${notification.name || 'Your notification'}"</i>\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;
|
||||||
437
src/services/simpleAutomaticNotificationService.js
Normal file
437
src/services/simpleAutomaticNotificationService.js
Normal file
|
|
@ -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 `🎯 <b>New Match Found!</b>\n\n` +
|
||||||
|
`🏠 <b>${title}</b>\n` +
|
||||||
|
`${price}\n` +
|
||||||
|
`📍 ${location}\n` +
|
||||||
|
`🏡 ${houseType}\n` +
|
||||||
|
`📊 Status: ${status}\n\n` +
|
||||||
|
`📋 <i>Matched: "${notification.name || 'Your notification'}"</i>\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;
|
||||||
142
src/webhook/listingWebhook.js
Normal file
142
src/webhook/listingWebhook.js
Normal file
|
|
@ -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;
|
||||||
117
src/webhookServer.js
Normal file
117
src/webhookServer.js
Normal file
|
|
@ -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;
|
||||||
Loading…
Reference in New Issue
Block a user