tambahannya

This commit is contained in:
bipproduction
2025-10-27 14:41:17 +08:00
parent 38c29d2af0
commit 42333587c6
9 changed files with 441 additions and 211 deletions

View File

@@ -21,6 +21,7 @@
"@types/jwt-decode": "^3.1.0",
"@types/lodash": "^4.17.20",
"@types/qrcode-terminal": "^0.12.2",
"@types/sharp": "^0.32.0",
"add": "^2.0.6",
"colors": "^1.4.0",
"dayjs": "^1.11.18",
@@ -30,6 +31,7 @@
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"meta-cloud-api": "^1.3.0",
"mime": "^4.1.0",
"node-fetch": "^3.3.2",
"pino": "^10.1.0",
"pino-pretty": "^13.1.2",
@@ -38,6 +40,7 @@
"react-dom": "^19.2.0",
"react-qr-code": "^2.0.18",
"react-router-dom": "^7.9.4",
"sharp": "^0.34.4",
"swr": "^2.3.6",
"uuid": "^13.0.0",
"whatsapp-api-js": "^6.1.1",
@@ -69,6 +72,8 @@
"@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="],
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
@@ -79,6 +84,52 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.3" }, "os": "darwin", "cpu": "x64" }, "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.3", "", { "os": "linux", "cpu": "arm" }, "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.3" }, "os": "linux", "cpu": "arm" }, "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.3" }, "os": "linux", "cpu": "s390x" }, "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.4", "", { "dependencies": { "@emnapi/runtime": "^1.5.0" }, "cpu": "none" }, "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.4", "", { "os": "win32", "cpu": "x64" }, "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig=="],
"@lglab/react-qr-code": ["@lglab/react-qr-code@1.4.5", "", { "peerDependencies": { "react": "^18 || ^19" } }, "sha512-kfaWOsbqqN+iskfRJLSHaPCHX0FAnRtZefj86rPq8WAZUVe9fnRzpr/xlOlXSsC+g1KWdm6LgXeoKgkvOa2F7g=="],
"@mantine/core": ["@mantine/core@8.3.4", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", "react-number-format": "^5.4.4", "react-remove-scroll": "^2.7.1", "react-textarea-autosize": "8.5.9", "type-fest": "^4.41.0" }, "peerDependencies": { "@mantine/hooks": "8.3.4", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-RJ5QUe2FLLJ1uF8xWUpNhDqRFbaOn4S5yTjqLuaurqtZvzee85O/T90dRcR8UNDuE8e/Qqie/jsF/G9RiSxC6g=="],
@@ -167,6 +218,8 @@
"@types/serve-static": ["@types/serve-static@1.15.9", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA=="],
"@types/sharp": ["@types/sharp@0.32.0", "", { "dependencies": { "sharp": "*" } }, "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw=="],
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
@@ -275,6 +328,8 @@
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"devtools-protocol": ["devtools-protocol@0.0.1045489", "", {}, "sha512-D+PTmWulkuQW4D1NTiCRCFxF7pQPn0hgp4YyX4wAQ6xYXKOadSWPR3ENGDQ47MW/Ewc9v2rpC/UEEGahgBYpSQ=="],
@@ -423,7 +478,7 @@
"meta-cloud-api": ["meta-cloud-api@1.3.0", "", {}, "sha512-NVBhIx41Ond5zI26lWu/1IDddYV2exLkfq89ELkkEaTp+HLROJMNspE6btLES2MUJrR7rVqT5mW+tUvjE+YunQ=="],
"mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
"mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
@@ -571,10 +626,14 @@
"secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
"sharp": ["sharp@0.34.4", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="],
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -703,6 +762,8 @@
"whatsapp-client-sdk/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"whatsapp-web.js/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
"whatsapp-web.js/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"zip-stream/archiver-utils": ["archiver-utils@3.0.4", "", { "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="],

View File

@@ -27,6 +27,7 @@
"@types/jwt-decode": "^3.1.0",
"@types/lodash": "^4.17.20",
"@types/qrcode-terminal": "^0.12.2",
"@types/sharp": "^0.32.0",
"add": "^2.0.6",
"colors": "^1.4.0",
"dayjs": "^1.11.18",
@@ -36,6 +37,7 @@
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"meta-cloud-api": "^1.3.0",
"mime": "^4.1.0",
"node-fetch": "^3.3.2",
"pino": "^10.1.0",
"pino-pretty": "^13.1.2",
@@ -44,6 +46,7 @@
"react-dom": "^19.2.0",
"react-qr-code": "^2.0.18",
"react-router-dom": "^7.9.4",
"sharp": "^0.34.4",
"swr": "^2.3.6",
"uuid": "^13.0.0",
"whatsapp-api-js": "^6.1.1",

View File

@@ -1,9 +1,10 @@
import { Navigate, Outlet } from "react-router-dom";
import useSWR from "swr";
import apiFetch from "@/lib/apiFetch";
import { Badge, Button, Chip, Group, Pill, Stack } from "@mantine/core";
import { Badge, Button, Chip, Group, Pill, Stack, Text } from "@mantine/core";
import { useState } from "react";
import clientRoutes from "@/clientRoutes";
import { modals } from "@mantine/modals";
export default function WajsLayout() {
const [loading, setLoading] = useState(false);
@@ -41,6 +42,28 @@ export default function WajsLayout() {
>
Reconnect
</Button>
<Button
color="red"
onClick={() => {
setLoading(true);
modals.openConfirmModal({
title: "Rescan QR",
children: <Text>Are you sure you want to rescan QR?</Text>,
confirmProps: { color: "red" },
labels: {
cancel: "Cancel",
confirm: "Rescan QR",
},
onCancel: () => setLoading(false),
onConfirm: () => {
apiFetch.api.wa.restart.post();
setLoading(false);
},
});
}}
>
Rescan QR
</Button>
</Group>
<Outlet />
</Stack>

116
src/server/lib/mim_utils.ts Normal file
View File

@@ -0,0 +1,116 @@
// ✅ Tipe kategori utama MIME
export type MimeCategory = "image" | "video" | "audio" | "document" | "archive" | "other";
// ✅ Struktur detail MIME
export interface MimeDetail {
type: string;
category: MimeCategory;
exampleMime: string[];
extensions: string[];
}
// ✅ Full list mimetype yang bisa dikembangkan
export const MimeMap: Record<string, MimeDetail> = {
image: {
type: "image",
category: "image",
exampleMime: ["image/jpeg", "image/png", "image/gif"],
extensions: ["jpg", "jpeg", "png", "gif", "webp"]
},
video: {
type: "video",
category: "video",
exampleMime: ["video/mp4", "video/mkv", "video/webm"],
extensions: ["mp4", "mkv", "avi", "mov", "webm"]
},
audio: {
type: "audio",
category: "audio",
exampleMime: ["audio/mpeg", "audio/wav", "audio/aac"],
extensions: ["mp3", "wav", "aac", "ogg", "flac"]
},
document: {
type: "application",
category: "document",
exampleMime: ["application/pdf", "application/msword"],
extensions: ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt"]
},
archive: {
type: "application",
category: "archive",
exampleMime: ["application/zip", "application/x-rar-compressed"],
extensions: ["zip", "rar", "7z", "tar", "gz"]
}
};
// ✅ Ambil semua ekstensi valid
const allExtensions = Object.values(MimeMap).flatMap(m => m.extensions);
// ✅ Type Guard untuk menghindari "never"
export function isFileExtension(ext: string): ext is (typeof allExtensions)[number] {
return allExtensions.includes(ext as any);
}
// ✅ Class utama
export class MimeType {
private input: string;
private ext?: string;
private type?: string;
constructor(input: string) {
this.input = input.toLowerCase().trim();
this.parseInput();
}
private parseInput() {
if (this.input.includes("/")) {
const [type, ext] = this.input.split("/");
this.type = type;
this.ext = ext;
} else if (isFileExtension(this.input.replace(/^\./, ""))) {
this.ext = this.input.replace(/^\./, "");
this.type = Object.values(MimeMap).find(m => m.extensions.includes(this.ext!))?.type;
}
}
// ✅ Dapatkan MIME Type lengkap: "image/png"
getType(): string | undefined {
if (this.type && this.ext) return `${this.type}/${this.ext}`;
return undefined;
}
// ✅ Dapatkan ekstensi yang digunakan
getExtension(): string | undefined {
return this.ext;
}
// ✅ Semua ekstensi dalam grup/type
getExtensions(): string[] | undefined {
const cat = this.getCategory();
if (!cat) return;
return MimeMap[cat]?.extensions;
}
// ✅ Ambil kategori: "image", "video", dll.
getCategory(): MimeCategory | undefined {
if (this.type) {
const found = Object.values(MimeMap).find(m => m.type === this.type);
return found?.category;
}
if (this.ext) {
const found = Object.values(MimeMap).find(m => m.extensions.includes(this.ext!));
return found?.category;
}
return undefined;
}
// ✅ Check cepat berdasarkan kategori
isImage() { return this.getCategory() === "image"; }
isVideo() { return this.getCategory() === "video"; }
isAudio() { return this.getCategory() === "audio"; }
isDocument() { return this.getCategory() === "document"; }
isArchive() { return this.getCategory() === "archive"; }
}
// ✅ Export default agar bisa: import MimeType from "./..."
export default MimeType;

View File

@@ -6,6 +6,44 @@ import { v4 as uuid } from 'uuid';
import { prisma } from '../prisma';
import { getValueByPath } from '../get_value_by_path';
import "colors"
import { logger } from '../logger';
import _ from 'lodash';
import MimeType from '../mim_utils';
import sharp from "sharp";
interface Base64ImageResult {
fileName: string;
base64: string;
sizeBeforeKB: number;
sizeAfterKB: number;
}
export async function convertImageToPngBase64(
inputPath: string,
): Promise<Base64ImageResult> {
// Baca buffer asli
const originalBuffer = await fs.readFile(inputPath);
const sizeBeforeKB = originalBuffer.length / 1024;
// Konversi & kompres ke PNG
const optimizedBuffer = await sharp(originalBuffer)
.png({ compressionLevel: 9 })
.toBuffer();
const sizeAfterKB = optimizedBuffer.length / 1024;
// Convert ke base64
const base64 = `data:image/png;base64,${optimizedBuffer.toString("base64")}`;
return {
fileName: inputPath.split("/").pop() || "image.png",
base64,
sizeBeforeKB,
sizeAfterKB,
};
}
const MEDIA_DIR = path.join(process.cwd(), 'downloads');
await ensureDir(MEDIA_DIR);
@@ -27,7 +65,7 @@ type DataMessage = {
type: WAWebJS.MessageTypes;
to: string;
deviceType: string;
media: any[] | null;
media: Record<string, any>;
notifyName: string;
}
@@ -180,17 +218,6 @@ async function startClient() {
}
}
function detectFileCategory(mime: string) {
if (mime.startsWith("image/")) return "image";
if (mime.startsWith("audio/")) return "audio";
if (mime.startsWith("video/")) return "video";
if (mime === "application/pdf") return "pdf";
if (mime.includes("spreadsheet") || mime.includes("excel")) return "excel";
if (mime.includes("word")) return "document";
if (mime.includes("presentation") || mime.includes("powerpoint")) return "presentation";
return "file";
}
// === HANDLER PESAN MASUK ===
async function handleIncomingMessage(msg: WAWebJS.Message) {
const chat = await msg.getChat();
@@ -205,6 +232,18 @@ async function handleIncomingMessage(msg: WAWebJS.Message) {
return;
}
console.log("kirim ke webhook")
const res = await fetch("https://n8n.wibudev.com/webhook/dc164759-b7ba-47d5-b5d8-ffd9d5840090", {
body: JSON.stringify(msg),
method: "POST",
headers: {
"Content-Type": "application/json",
},
})
const json = await res.text();
console.log(json);
try {
const notifyName = (msg as any)._data.notifyName;
@@ -217,107 +256,118 @@ async function handleIncomingMessage(msg: WAWebJS.Message) {
type: msg.type,
to: msg.to,
deviceType: msg.deviceType,
media: null,
media: {},
notifyName,
};
// === HANDLE MEDIA ===
if (msg.hasMedia) {
const media = await msg.downloadMedia();
// Pastikan formatnya data:<mimetype>;base64,<data>
const mime = media.mimetype || 'application/octet-stream';
const prefixedBase64 = `data:${mime};base64,${media.data}`;
dataMessage.media = [{
type: "file:full",
data: prefixedBase64,
mime: mime,
name: media.filename || `${uuid()}.${mime.split('/')[1] || 'bin'}`
}];
// await fs.writeFile(path.join(MEDIA_DIR, dataMessage.media[0].name), Buffer.from(media.data, 'base64'));
}
// === KIRIM KE WEBHOOK ===
try {
const webhooks = await prisma.webHook.findMany({ where: { enabled: true } });
if (!webhooks.length) {
log('🚫 Tidak ada webhook yang aktif');
return;
}
// try {
// const webhooks = await prisma.webHook.findMany({ where: { enabled: true } });
// if (!webhooks.length) {
// log('🚫 Tidak ada webhook yang aktif');
// return;
// }
await Promise.allSettled(
webhooks.map(async (hook) => {
try {
log(`🌐 Mengirim webhook ke ${hook.url}`);
let body = payloadConverter({
payload: hook.payload ?? JSON.stringify(dataMessage),
data: dataMessage,
});
if (dataMessage.hasMedia) {
const bodyMedia = JSON.parse(body);
bodyMedia.question = msg.body ?? dataMessage.media?.[0].mime;
bodyMedia.uploads = dataMessage.media;
body = JSON.stringify(bodyMedia);
}
// // await Promise.allSettled(
// // webhooks.map(async (hook) => {
// // try {
// // log(`🌐 Mengirim webhook ke ${hook.url}`);
// await fs.writeFile(path.join(process.cwd(), 'webhook.json'), body);
// // let res: Response = {} as Response;
// // if (!dataMessage.hasMedia) {
// // logger.info(`[SEND NO MEDIA] ${hook.url}`);
// // res = await fetch(hook.url, {
// // method: hook.method,
// // headers: {
// // "Content-Type": "application/json",
// // Authorization: `Bearer ${hook.apiToken}`,
// // },
// // body: JSON.stringify({
// // question: msg.body,
// // overrideConfig: {
// // sessionId: `${_.kebabCase(dataMessage.fromNumber)}_x_${dataMessage.fromNumber}`,
// // vars: { userName: _.kebabCase(dataMessage.fromNumber), userPhone: dataMessage.fromNumber },
// // }
// // }),
// // });
// // }
const res = await fetch(hook.url, {
method: hook.method,
headers: {
...(JSON.parse(hook.headers ?? '{}') as Record<string, string>),
...(hook.apiToken ? { Authorization: `Bearer ${hook.apiToken}` } : {}),
},
body,
});
// // if (dataMessage.hasMedia) {
// // logger.info(`[SEND MEDIA] ${hook.url}`);
// // const media = await msg.downloadMedia();
const responseText = await res.text();
// // const mimeMessage = media.mimetype || 'application/octet-stream';
// // const typeMime = new MimeType(mimeMessage);
if (!res.ok) {
log(`⚠️ Webhook ${hook.url} gagal: ${res.status}`);
log(responseText);
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR01]");
return;
}
// // const prefixedBase64 = `data:${mimeMessage};base64,${media.data}`;
const responseJson = JSON.parse(responseText);
// // dataMessage.media = {
// // type: typeMime.getCategory() === "image" ? "file" : "file:full",
// // data: prefixedBase64,
// // mime: mimeMessage,
// // name: media.filename || `${uuid()}.${typeMime.getExtension()}`
// // };
if (hook.replay) {
try {
const textResponseRaw = hook.replayKey
? getValueByPath(responseJson, hook.replayKey, JSON.stringify(responseJson))
: JSON.stringify(responseJson, null, 2);
// // res = await fetch(hook.url, {
// // method: hook.method,
// // headers: {
// // "Content-Type": "application/json",
// // Authorization: `Bearer ${hook.apiToken}`,
// // },
// // body: JSON.stringify({
// // question: msg.body || dataMessage.media.mime,
// // overrideConfig: {
// // sessionId: `${_.kebabCase(dataMessage.fromNumber)}_x_${dataMessage.fromNumber}`,
// // vars: { userName: _.kebabCase(dataMessage.fromNumber), userPhone: dataMessage.fromNumber },
// // },
// // uploads: [dataMessage.media],
// // }),
// // });
// // }
const typingDelay = Math.min(5000, Math.max(1500, textResponseRaw.length * 20));
await new Promise((r) => setTimeout(r, typingDelay));
// // const responseText = await res.text();
await chat.clearState();
// send message
await chat.sendMessage(textResponseRaw);
log(`💬 Balasan dikirim ke ${msg.from} setelah mengetik selama ${typingDelay}ms`);
} catch (err) {
log('⚠️ Gagal menampilkan status mengetik:', err);
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR03]");
}
}
} catch (err) {
log(`❌ Gagal kirim ke ${hook.url}:`, err);
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR04]");
}
})
);
} catch (error) {
log('❌ Error mengirim webhook:', error);
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR05]");
}
// // if (!res.ok) {
// // log(`⚠️ Webhook ${hook.url} gagal: ${res.status}`);
// // logger.error(`[REPLY] Response: ${responseText}`);
// // await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR01]");
// // return;
// // }
// // const responseJson = JSON.parse(responseText);
// // logger.info(`[REPLY] Response: ${responseJson.text}`);
// // if (hook.replay) {
// // try {
// // const textResponseRaw = hook.replayKey
// // ? getValueByPath(responseJson, hook.replayKey, JSON.stringify(responseJson))
// // : JSON.stringify(responseJson, null, 2);
// // await chat.clearState();
// // // send message
// // await chat.sendMessage(textResponseRaw);
// // logger.info(`💬 Balasan dikirim ke ${msg.from}`);
// // } catch (err) {
// // logger.error(`⚠️ Gagal menampilkan status mengetik: ${err}`);
// // await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR03]");
// // }
// // }
// // } catch (err) {
// // logger.error(`❌ Gagal kirim ke ${hook.url}: ${err}`);
// // await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR04]");
// // }
// // })
// // );
// } catch (error) {
// logger.error(`❌ Error mengirim webhook [ERR05]: ${error}`);
// await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR05]");
// }
} catch (err) {
log('❌ Error handling pesan:', err);
logger.error(`❌ Error handling pesan [ERR06]: ${err}`);
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR06]");
} finally {
await chat.clearState();
@@ -325,46 +375,6 @@ async function handleIncomingMessage(msg: WAWebJS.Message) {
}
function payloadConverter({ payload, data }: { payload: string; data: DataMessage }) {
try {
const map: Record<string, any> = {
'data.from': data.from,
'data.fromNumber': data.fromNumber,
'data.fromMe': data.fromMe,
'data.body': data.body,
'data.hasMedia': data.hasMedia,
'data.type': data.type,
'data.to': data.to,
'data.deviceType': data.deviceType,
'data.notifyName': data.notifyName,
'data.media': data.media
};
let result = payload;
for (const [key, value] of Object.entries(map)) {
let safeValue: string;
if (value === null || value === undefined) {
safeValue = '';
} else if (typeof value === 'object') {
// Perbaikan di sini — objek seperti media dikonversi ke JSON string
safeValue = JSON.stringify(value);
} else {
safeValue = String(value);
}
result = result.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), safeValue);
}
return result;
} catch (err) {
console.error("⚠️ payloadConverter error:", err);
return JSON.stringify(data);
}
}
// === CLEANUP SAAT EXIT ===
process.on('SIGINT', async () => {
log('🛑 SIGINT diterima, menutup client...');

View File

@@ -1,4 +1,4 @@
import Elysia from "elysia";
import Elysia, { t } from "elysia";
import { startClient, getState } from "../lib/wa/wa_service";
import _ from "lodash";
@@ -48,5 +48,36 @@ const WaRoute = new Elysia({
state: _.omit(state, "client"),
};
})
.post("send-text", async ({body}) => {
const state = getState();
if (!state.ready) {
return {
message: "WhatsApp route not ready",
};
}
const client = state.client;
if (!client) {
return {
message: "WhatsApp client not ready",
};
}
const chat = await client.getChatById(`${body.number}@c.us`);
await chat.sendMessage(body.text);
return {
message: "WhatsApp route ready",
};
},{
body: t.Object({
number: t.String(),
text: t.String(),
}),
detail: {
description: "Send text to WhatsApp",
tags: ["WhatsApp"],
}
})
export default WaRoute;

10
x.sh
View File

@@ -1,7 +1,3 @@
TOKEN="EAALP22EWyC4BPnZCfcPQNmD5pGLKV6Ao3GIeWZCc81aPivDFc2FXGA1ZBgrRGcB60LaZCdAr1sbnfP1ufrH3dGthxQzpf18BTjDZBkgG3vBiYZAMpHa7MEZBiRIUZCBe4BDXe8KV0r7DsDmQHJqhA3yZBDKPOL1PKJPEqIq40tLxPwMqWYg4o7xf0sBmZCzx2wI1KtJL8I20MV1ggldngHZCIcnOKDL0uPzDAhc2LAQuI7ZBsgZDZD"
MEDIA_ID="24893686766920074"
BUSINESS_PHONE_NUMBER_ID="783866307805501"
curl 'https://graph.facebook.com/v19.0/$MEDIA_ID?phone_number_id=$BUSINESS_PHONE_NUMBER_ID' \
-H 'Authorization: Bearer $TOKEN' \
-H 'Content-Type: application/json'
curl -X POST https://n8n.wibudev.com/webhook/dc164759-b7ba-47d5-b5d8-ffd9d5840090 \
-H "Content-Type: application/json" \
-d '{"question": "kirimkan pesan ke 089697338821 pesannya hari ini bagaimana ya?", "sessionId": "dc164759-b7ba-47d5-b5d8-ffd9d5840090"}'

53
x.yml Normal file
View File

@@ -0,0 +1,53 @@
services:
n8n:
image: docker.n8n.io/n8nio/n8n
container_name: n8n
environment:
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
- N8N_RUNNERS_ENABLED=true
- N8N_HOST=n8n.wibudev.com
- N8N_PORT=5678
- WEBHOOK_URL=https://n8n.wibudev.com/
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=wibu@bip.com
- N8N_BASIC_AUTH_PASSWORD=Production_123
- N8N_PROTOCOL=https
- NODE_ENV=production
volumes:
- ./data/n8n:/home/node/.n8n
- ./data/n8n/local-files:/files
restart: always
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5678"]
interval: 30s
timeout: 5s
retries: 5
start_period: 20s
networks:
- n8n-network
n8n-frpc:
image: snowdreamtech/frpc:latest
container_name: n8n-frpc
restart: always
volumes:
- ./data/frpc/frpc.toml:/etc/frp/frpc.toml:ro
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
depends_on:
n8n:
condition: service_healthy
networks:
- n8n-network
networks:
n8n-network:
driver: bridge

67
xx.ts
View File

@@ -1,66 +1,3 @@
import fs from "fs";
import { parse, stringify } from "yaml";
import mime from "mime";
export interface LogRotateOptions {
maxSize?: string;
maxFile?: string;
}
/**
* Tambahkan log rotate (logging.driver json-file) ke semua service
* yang belum memiliki konfigurasi logging di docker-compose.yml.
*/
export async function applyLogRotateCompose(
filePath: string,
options: LogRotateOptions = {}
) {
const { maxSize = "10m", maxFile = "3" } = options;
// Pastikan file ada
if (!fs.existsSync(filePath)) {
throw new Error(`❌ File not found: ${filePath}`);
}
const raw = fs.readFileSync(filePath, "utf8");
const compose = parse(raw); // ✅ Pakai yaml.parse()
if (!compose.services) {
throw new Error("❌ Tidak ditemukan 'services:' di docker-compose.yml");
}
let modified = false;
for (const [name, service] of Object.entries<any>(compose.services)) {
if (!service.logging) {
service.logging = {
driver: "json-file",
options: {
"max-size": maxSize,
"max-file": maxFile,
},
};
console.log(`✅ Log rotate ditambahkan ke: ${name}`);
modified = true;
} else {
console.log(`⚠️ Lewati (sudah ada logging): ${name}`);
}
}
if (!modified) {
console.log("👌 Semua service sudah punya log-rotate, tidak ada perubahan.");
return;
}
// Backup file lama
const backupPath = `${filePath}.backup-${Date.now()}`;
fs.writeFileSync(backupPath, raw, "utf8");
// Simpan file baru
const updated = stringify(compose); // ✅ Pakai yaml.stringify()
fs.writeFileSync(filePath, updated, "utf8");
console.log(`✅ Selesai update file: ${filePath}`);
console.log(`📦 Backup dibuat: ${backupPath}`);
}
applyLogRotateCompose("compose.yml");
console.log(mime.getAllExtensions("image/png"));