diff --git a/omni_chat/package-lock.json b/omni_chat/package-lock.json index 3d58907..197238c 100644 --- a/omni_chat/package-lock.json +++ b/omni_chat/package-lock.json @@ -5,8 +5,14 @@ "packages": { "": { "name": "crm-omni-chat", + "hasInstallScript": true, + "dependencies": { + "@prisma/client": "^6.16.1", + "bullmq": "^5.70.0" + }, "devDependencies": { "@types/node": "^22.13.9", + "prisma": "^6.16.1", "tsx": "^4.20.5", "typescript": "^5.9.2" } @@ -453,6 +459,182 @@ "node": ">=18" } }, + "node_modules/@ioredis/commands": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", + "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", + "license": "MIT" + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@prisma/client": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.1.tgz", + "integrity": "sha512-QaBCOY29lLAxEFFJgBPyW3WInCW52fJeQTmWx/h6YsP5u0bwuqP51aP0uhqFvhK9DaZPwvai/M4tSDYLVE9vRg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.1.tgz", + "integrity": "sha512-sz3uxRPNL62QrJ0EYiujCFkIGZ3hg+9hgC1Ae1HjoYuj0BxCqHua4JNijYvYCrh9LlofZDZcRBX3tHBfLvAngA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.1.tgz", + "integrity": "sha512-RWv/VisW5vJE4cDRTuAHeVedtGoItXTnhuLHsSlJ9202QKz60uiXWywBlVcqXVq8bFeIZoCoWH+R1duZJPwqLw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.1.tgz", + "integrity": "sha512-EOnEM5HlosPudBqbI+jipmaW/vQEaF0bKBo4gVkGabasINHR6RpC6h44fKZEqx4GD8CvH+einD2+b49DQrwrAg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.1", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/fetch-engine": "6.16.1", + "@prisma/get-platform": "6.16.1" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43.tgz", + "integrity": "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.1.tgz", + "integrity": "sha512-fl/PKQ8da5YTayw86WD3O9OmKJEM43gD3vANy2hS5S1CnfW2oPXk+Q03+gUWqcKK306QqhjjIHRFuTZ31WaosQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.1", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/get-platform": "6.16.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.1.tgz", + "integrity": "sha512-kUfg4vagBG7dnaGRcGd1c0ytQFcDj2SUABiuveIpL3bthFdTLI6PJeLEia6Q8Dgh+WhPdo0N2q0Fzjk63XTyaA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", @@ -463,6 +645,208 @@ "undici-types": "~6.21.0" } }, + "node_modules/bullmq": { + "version": "5.70.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.70.0.tgz", + "integrity": "sha512-HlBSEJqG7MJ97+d/N/8rtGOcpisjGP3WD/zaXZia0hsmckJqAPTVWN6Yfw32FVfVSUVVInZQ2nUgMd2zCRghKg==", + "license": "MIT", + "dependencies": { + "cron-parser": "4.9.0", + "ioredis": "5.9.2", + "msgpackr": "1.11.5", + "node-abort-controller": "3.1.1", + "semver": "7.7.4", + "tslib": "2.8.1", + "uuid": "11.1.0" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/effect": { + "version": "3.16.12", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", + "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -505,6 +889,36 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -533,6 +947,291 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/ioredis": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz", + "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/prisma": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.1.tgz", + "integrity": "sha512-MFkMU0eaDDKAT4R/By2IA9oQmwLTxokqv2wegAErr9Rf+oIe7W2sYpE/Uxq0H2DliIR7vnV63PkC1bEwUtl98w==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.16.1", + "@prisma/engines": "6.16.1" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -543,6 +1242,40 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -567,7 +1300,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -583,6 +1316,19 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } } } } diff --git a/omni_chat/package.json b/omni_chat/package.json index 43afbc6..490c3a4 100644 --- a/omni_chat/package.json +++ b/omni_chat/package.json @@ -4,11 +4,17 @@ "type": "module", "scripts": { "start": "tsx src/index.ts", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "postinstall": "node ./node_modules/prisma/build/index.js generate --schema ./prisma/schema.prisma" }, "devDependencies": { "@types/node": "^22.13.9", + "prisma": "^6.16.1", "tsx": "^4.20.5", "typescript": "^5.9.2" + }, + "dependencies": { + "@prisma/client": "^6.16.1", + "bullmq": "^5.70.0" } } diff --git a/omni_chat/prisma/schema.prisma b/omni_chat/prisma/schema.prisma new file mode 100644 index 0000000..e344556 --- /dev/null +++ b/omni_chat/prisma/schema.prisma @@ -0,0 +1,392 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum TeamRole { + OWNER + MEMBER +} + +enum MessageDirection { + IN + OUT +} + +enum MessageChannel { + TELEGRAM + WHATSAPP + INSTAGRAM + PHONE + EMAIL + INTERNAL +} + +enum ContactMessageKind { + MESSAGE + CALL +} + +enum ChatRole { + USER + ASSISTANT + SYSTEM +} + +enum OmniMessageStatus { + PENDING + SENT + FAILED + DELIVERED + READ +} + +enum FeedCardDecision { + PENDING + ACCEPTED + REJECTED +} + +enum WorkspaceDocumentType { + Regulation + Playbook + Policy + Template +} + +model Team { + id String @id @default(cuid()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + members TeamMember[] + contacts Contact[] + calendarEvents CalendarEvent[] + deals Deal[] + conversations ChatConversation[] + chatMessages ChatMessage[] + + omniThreads OmniThread[] + omniMessages OmniMessage[] + omniIdentities OmniContactIdentity[] + telegramBusinessConnections TelegramBusinessConnection[] + + feedCards FeedCard[] + contactPins ContactPin[] + documents WorkspaceDocument[] +} + +model User { + id String @id @default(cuid()) + phone String @unique + passwordHash String + email String? @unique + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + memberships TeamMember[] + conversations ChatConversation[] @relation("ConversationCreator") + chatMessages ChatMessage[] @relation("ChatAuthor") +} + +model TeamMember { + id String @id @default(cuid()) + teamId String + userId String + role TeamRole @default(MEMBER) + createdAt DateTime @default(now()) + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([teamId, userId]) + @@index([userId]) +} + +model Contact { + id String @id @default(cuid()) + teamId String + name String + company String? + country String? + location String? + avatarUrl String? + email String? + phone String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + note ContactNote? + messages ContactMessage[] + events CalendarEvent[] + deals Deal[] + feedCards FeedCard[] + pins ContactPin[] + + omniThreads OmniThread[] + omniMessages OmniMessage[] + omniIdentities OmniContactIdentity[] + + @@index([teamId, updatedAt]) +} + +model ContactNote { + id String @id @default(cuid()) + contactId String @unique + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) +} + +model ContactMessage { + id String @id @default(cuid()) + contactId String + kind ContactMessageKind @default(MESSAGE) + direction MessageDirection + channel MessageChannel + content String + audioUrl String? + durationSec Int? + transcriptJson Json? + occurredAt DateTime @default(now()) + createdAt DateTime @default(now()) + + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + + @@index([contactId, occurredAt]) +} + +model OmniContactIdentity { + id String @id @default(cuid()) + teamId String + contactId String + channel MessageChannel + externalId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + + @@unique([teamId, channel, externalId]) + @@index([contactId]) + @@index([teamId, updatedAt]) +} + +model OmniThread { + id String @id @default(cuid()) + teamId String + contactId String + channel MessageChannel + externalChatId String + businessConnectionId String? + title String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + messages OmniMessage[] + + @@unique([teamId, channel, externalChatId, businessConnectionId]) + @@index([teamId, updatedAt]) + @@index([contactId, updatedAt]) +} + +model OmniMessage { + id String @id @default(cuid()) + teamId String + contactId String + threadId String + direction MessageDirection + channel MessageChannel + status OmniMessageStatus @default(PENDING) + text String + providerMessageId String? + providerUpdateId String? + rawJson Json? + occurredAt DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + thread OmniThread @relation(fields: [threadId], references: [id], onDelete: Cascade) + + @@unique([threadId, providerMessageId]) + @@index([teamId, occurredAt]) + @@index([threadId, occurredAt]) +} + +model TelegramBusinessConnection { + id String @id @default(cuid()) + teamId String + businessConnectionId String + isEnabled Boolean? + canReply Boolean? + rawJson Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + + @@unique([teamId, businessConnectionId]) + @@index([teamId, updatedAt]) +} + +model CalendarEvent { + id String @id @default(cuid()) + teamId String + contactId String? + title String + startsAt DateTime + endsAt DateTime? + note String? + isArchived Boolean @default(false) + archiveNote String? + archivedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull) + + @@index([startsAt]) + @@index([contactId, startsAt]) + @@index([teamId, startsAt]) +} + +model Deal { + id String @id @default(cuid()) + teamId String + contactId String + title String + stage String + amount Int? + nextStep String? + summary String? + currentStepId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + steps DealStep[] + + @@index([teamId, updatedAt]) + @@index([contactId, updatedAt]) + @@index([currentStepId]) +} + +model DealStep { + id String @id @default(cuid()) + dealId String + title String + description String? + status String @default("todo") + dueAt DateTime? + order Int @default(0) + completedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + deal Deal @relation(fields: [dealId], references: [id], onDelete: Cascade) + + @@index([dealId, order]) + @@index([status, dueAt]) +} + +model ChatConversation { + id String @id @default(cuid()) + teamId String + createdByUserId String + title String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + createdByUser User @relation("ConversationCreator", fields: [createdByUserId], references: [id], onDelete: Cascade) + messages ChatMessage[] + + @@index([teamId, updatedAt]) + @@index([createdByUserId]) +} + +model ChatMessage { + id String @id @default(cuid()) + teamId String + conversationId String + authorUserId String? + role ChatRole + text String + planJson Json? + createdAt DateTime @default(now()) + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + conversation ChatConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) + authorUser User? @relation("ChatAuthor", fields: [authorUserId], references: [id], onDelete: SetNull) + + @@index([createdAt]) + @@index([teamId, createdAt]) + @@index([conversationId, createdAt]) +} + +model FeedCard { + id String @id @default(cuid()) + teamId String + contactId String? + happenedAt DateTime + text String + proposalJson Json + decision FeedCardDecision @default(PENDING) + decisionNote String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull) + + @@index([teamId, happenedAt]) + @@index([contactId, happenedAt]) +} + +model ContactPin { + id String @id @default(cuid()) + teamId String + contactId String + text String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + + @@index([teamId, updatedAt]) + @@index([contactId, updatedAt]) +} + +model WorkspaceDocument { + id String @id @default(cuid()) + teamId String + title String + type WorkspaceDocumentType + owner String + scope String + summary String + body String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + + @@index([teamId, updatedAt]) +} diff --git a/omni_chat/src/index.ts b/omni_chat/src/index.ts index fadce9e..b11390a 100644 --- a/omni_chat/src/index.ts +++ b/omni_chat/src/index.ts @@ -1,15 +1,20 @@ import { createServer } from "node:http"; +import { closeReceiverWorker, RECEIVER_FLOW_QUEUE_NAME, receiverQueue, startReceiverWorker } from "./worker"; const port = Number(process.env.PORT || 8090); const service = "omni_chat"; -const server = createServer((req, res) => { +const server = createServer(async (req, res) => { if (req.method === "GET" && req.url === "/health") { + const q = receiverQueue(); + const counts = await q.getJobCounts("wait", "active", "failed", "completed", "delayed"); + await q.close(); const payload = JSON.stringify({ ok: true, service, - receiverFlow: process.env.RECEIVER_FLOW_QUEUE_NAME || "receiver.flow", + receiverFlow: RECEIVER_FLOW_QUEUE_NAME, senderFlow: process.env.SENDER_FLOW_QUEUE_NAME || "sender.flow", + queue: counts, now: new Date().toISOString(), }); res.statusCode = 200; @@ -23,14 +28,25 @@ const server = createServer((req, res) => { res.end(JSON.stringify({ ok: false, error: "not_found" })); }); +startReceiverWorker(); + server.listen(port, "0.0.0.0", () => { console.log(`[omni_chat] listening on :${port}`); + console.log(`[omni_chat] receiver worker started for queue ${RECEIVER_FLOW_QUEUE_NAME}`); }); -function shutdown(signal: string) { +async function shutdown(signal: string) { console.log(`[omni_chat] shutting down by ${signal}`); - server.close(() => process.exit(0)); + try { + await closeReceiverWorker(); + } finally { + server.close(() => process.exit(0)); + } } -process.on("SIGINT", () => shutdown("SIGINT")); -process.on("SIGTERM", () => shutdown("SIGTERM")); +process.on("SIGINT", () => { + void shutdown("SIGINT"); +}); +process.on("SIGTERM", () => { + void shutdown("SIGTERM"); +}); diff --git a/omni_chat/src/utils/prisma.ts b/omni_chat/src/utils/prisma.ts new file mode 100644 index 0000000..27c97b7 --- /dev/null +++ b/omni_chat/src/utils/prisma.ts @@ -0,0 +1,16 @@ +import { PrismaClient } from "@prisma/client"; + +declare global { + // eslint-disable-next-line no-var + var __omniChatPrisma: PrismaClient | undefined; +} + +export const prisma = + globalThis.__omniChatPrisma ?? + new PrismaClient({ + log: ["error", "warn"], + }); + +if (process.env.NODE_ENV !== "production") { + globalThis.__omniChatPrisma = prisma; +} diff --git a/omni_chat/src/worker.ts b/omni_chat/src/worker.ts new file mode 100644 index 0000000..bf6f5ca --- /dev/null +++ b/omni_chat/src/worker.ts @@ -0,0 +1,283 @@ +import { Queue, Worker, type ConnectionOptions, type Job } from "bullmq"; +import { Prisma } from "@prisma/client"; +import { prisma } from "./utils/prisma"; + +type JsonObject = Record; + +type OmniInboundEnvelopeV1 = { + version: 1; + idempotencyKey: string; + provider: string; + channel: "TELEGRAM" | "WHATSAPP" | "INSTAGRAM" | "PHONE" | "EMAIL" | "INTERNAL"; + direction: "IN"; + providerEventId: string; + providerMessageId: string | null; + eventType: string; + occurredAt: string; + receivedAt: string; + payloadRaw: unknown; + payloadNormalized: { + threadExternalId: string | null; + contactExternalId: string | null; + text: string | null; + businessConnectionId: string | null; + updateId?: string | null; + [key: string]: unknown; + }; +}; + +export const RECEIVER_FLOW_QUEUE_NAME = (process.env.RECEIVER_FLOW_QUEUE_NAME || "receiver.flow").trim(); + +function redisConnectionFromEnv(): ConnectionOptions { + const raw = (process.env.REDIS_URL || "redis://localhost:6379").trim(); + const parsed = new URL(raw); + return { + host: parsed.hostname, + port: parsed.port ? Number(parsed.port) : 6379, + username: parsed.username ? decodeURIComponent(parsed.username) : undefined, + password: parsed.password ? decodeURIComponent(parsed.password) : undefined, + db: parsed.pathname && parsed.pathname !== "/" ? Number(parsed.pathname.slice(1)) : undefined, + maxRetriesPerRequest: null, + }; +} + +function normalizeText(input: unknown) { + const t = String(input ?? "").trim(); + return t || "[no text]"; +} + +function parseOccurredAt(input: string | null | undefined) { + const d = new Date(String(input ?? "")); + if (Number.isNaN(d.getTime())) return new Date(); + return d; +} + +async function resolveTeamId(env: OmniInboundEnvelopeV1) { + const n = env.payloadNormalized ?? ({} as OmniInboundEnvelopeV1["payloadNormalized"]); + const bcId = String(n.businessConnectionId ?? "").trim(); + if (bcId) { + const linked = await prisma.telegramBusinessConnection.findFirst({ + where: { businessConnectionId: bcId }, + orderBy: { updatedAt: "desc" }, + select: { teamId: true }, + }); + if (linked?.teamId) return linked.teamId; + } + + const externalContactId = String(n.contactExternalId ?? n.threadExternalId ?? "").trim(); + if (externalContactId) { + const pseudo = `link:${externalContactId}`; + const linked = await prisma.telegramBusinessConnection.findFirst({ + where: { businessConnectionId: pseudo }, + orderBy: { updatedAt: "desc" }, + select: { teamId: true }, + }); + if (linked?.teamId) return linked.teamId; + } + + const fallbackTeamId = String(process.env.DEFAULT_TEAM_ID || "").trim(); + if (fallbackTeamId) return fallbackTeamId; + + const demo = await prisma.team.findFirst({ + where: { id: "demo-team" }, + select: { id: true }, + }); + return demo?.id ?? null; +} + +async function resolveContact(input: { teamId: string; externalContactId: string }) { + const existingIdentity = await prisma.omniContactIdentity.findFirst({ + where: { + teamId: input.teamId, + channel: "TELEGRAM", + externalId: input.externalContactId, + }, + select: { contactId: true }, + }); + if (existingIdentity?.contactId) { + return existingIdentity.contactId; + } + + const contact = await prisma.contact.create({ + data: { + teamId: input.teamId, + name: `Telegram ${input.externalContactId}`, + company: "", + country: "", + location: "", + }, + select: { id: true }, + }); + + await prisma.omniContactIdentity.create({ + data: { + teamId: input.teamId, + contactId: contact.id, + channel: "TELEGRAM", + externalId: input.externalContactId, + }, + }); + + return contact.id; +} + +async function upsertThread(input: { + teamId: string; + contactId: string; + externalChatId: string; + businessConnectionId: string | null; +}) { + const existing = await prisma.omniThread.findFirst({ + where: { + teamId: input.teamId, + channel: "TELEGRAM", + externalChatId: input.externalChatId, + businessConnectionId: input.businessConnectionId, + }, + select: { id: true }, + }); + + if (existing) { + await prisma.omniThread.update({ + where: { id: existing.id }, + data: { contactId: input.contactId }, + }); + return existing; + } + + return prisma.omniThread.create({ + data: { + teamId: input.teamId, + contactId: input.contactId, + channel: "TELEGRAM", + externalChatId: input.externalChatId, + businessConnectionId: input.businessConnectionId, + title: null, + }, + select: { id: true }, + }); +} + +async function ingestInbound(env: OmniInboundEnvelopeV1) { + if (env.channel !== "TELEGRAM" || env.direction !== "IN") return; + + const teamId = await resolveTeamId(env); + if (!teamId) { + console.warn("[omni_chat] skip inbound: team not resolved", env.providerEventId); + return; + } + + const n = env.payloadNormalized ?? ({} as OmniInboundEnvelopeV1["payloadNormalized"]); + const externalContactId = String(n.contactExternalId ?? n.threadExternalId ?? "").trim(); + const externalChatId = String(n.threadExternalId ?? n.contactExternalId ?? "").trim(); + + if (!externalContactId || !externalChatId) { + console.warn("[omni_chat] skip inbound: missing contact/chat ids", env.providerEventId); + return; + } + + const businessConnectionId = String(n.businessConnectionId ?? "").trim() || null; + const text = normalizeText(n.text); + const occurredAt = parseOccurredAt(env.occurredAt); + + const contactId = await resolveContact({ teamId, externalContactId }); + const thread = await upsertThread({ + teamId, + contactId, + externalChatId, + businessConnectionId, + }); + + if (env.providerMessageId) { + await prisma.omniMessage.upsert({ + where: { + threadId_providerMessageId: { + threadId: thread.id, + providerMessageId: env.providerMessageId, + }, + }, + create: { + teamId, + contactId, + threadId: thread.id, + direction: "IN", + channel: "TELEGRAM", + status: "DELIVERED", + text, + providerMessageId: env.providerMessageId, + providerUpdateId: String((n.updateId as string | null | undefined) ?? env.providerEventId), + rawJson: (env.payloadRaw ?? null) as Prisma.InputJsonValue, + occurredAt, + }, + update: { + text, + providerUpdateId: String((n.updateId as string | null | undefined) ?? env.providerEventId), + rawJson: (env.payloadRaw ?? null) as Prisma.InputJsonValue, + occurredAt, + }, + }); + } else { + await prisma.omniMessage.create({ + data: { + teamId, + contactId, + threadId: thread.id, + direction: "IN", + channel: "TELEGRAM", + status: "DELIVERED", + text, + providerMessageId: null, + providerUpdateId: String((n.updateId as string | null | undefined) ?? env.providerEventId), + rawJson: (env.payloadRaw ?? null) as Prisma.InputJsonValue, + occurredAt, + }, + }); + } + + await prisma.contactMessage.create({ + data: { + contactId, + kind: "MESSAGE", + direction: "IN", + channel: "TELEGRAM", + content: text, + occurredAt, + }, + }); +} + +let workerInstance: Worker | null = null; + +export function startReceiverWorker() { + if (workerInstance) return workerInstance; + + const worker = new Worker( + RECEIVER_FLOW_QUEUE_NAME, + async (job) => { + await ingestInbound(job.data); + }, + { + connection: redisConnectionFromEnv(), + concurrency: Number(process.env.OMNI_CHAT_WORKER_CONCURRENCY || 4), + }, + ); + + worker.on("failed", (job: Job | undefined, err: Error) => { + console.error(`[omni_chat] receiver job failed id=${job?.id || "unknown"}: ${err?.message || err}`); + }); + + workerInstance = worker; + return worker; +} + +export async function closeReceiverWorker() { + if (!workerInstance) return; + await workerInstance.close(); + workerInstance = null; +} + +export function receiverQueue() { + return new Queue(RECEIVER_FLOW_QUEUE_NAME, { + connection: redisConnectionFromEnv(), + }); +}