machinaos 0.0.1 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.template +71 -71
- package/LICENSE +21 -21
- package/README.md +145 -87
- package/bin/cli.js +62 -106
- package/client/.dockerignore +45 -45
- package/client/Dockerfile +68 -68
- package/client/dist/assets/index-DFSC53FP.css +1 -0
- package/client/dist/assets/index-fJ-1gTf5.js +613 -0
- package/client/dist/index.html +14 -0
- package/client/eslint.config.js +34 -16
- package/client/nginx.conf +66 -66
- package/client/package.json +61 -48
- package/client/src/App.tsx +27 -27
- package/client/src/Dashboard.tsx +1200 -1172
- package/client/src/ParameterPanel.tsx +302 -300
- package/client/src/components/AIAgentNode.tsx +315 -321
- package/client/src/components/APIKeyValidator.tsx +117 -117
- package/client/src/components/ClaudeChatModelNode.tsx +17 -17
- package/client/src/components/CredentialsModal.tsx +1200 -306
- package/client/src/components/GeminiChatModelNode.tsx +17 -17
- package/client/src/components/GenericNode.tsx +356 -356
- package/client/src/components/LocationParameterPanel.tsx +153 -153
- package/client/src/components/ModelNode.tsx +285 -285
- package/client/src/components/OpenAIChatModelNode.tsx +17 -17
- package/client/src/components/OutputPanel.tsx +470 -470
- package/client/src/components/ParameterRenderer.tsx +1873 -1873
- package/client/src/components/SkillEditorModal.tsx +3 -3
- package/client/src/components/SquareNode.tsx +812 -796
- package/client/src/components/ToolkitNode.tsx +365 -365
- package/client/src/components/auth/LoginPage.tsx +247 -247
- package/client/src/components/auth/ProtectedRoute.tsx +59 -59
- package/client/src/components/base/BaseChatModelNode.tsx +270 -270
- package/client/src/components/icons/AIProviderIcons.tsx +50 -50
- package/client/src/components/maps/GoogleMapsPicker.tsx +136 -136
- package/client/src/components/maps/MapsPreviewPanel.tsx +109 -109
- package/client/src/components/maps/index.ts +25 -25
- package/client/src/components/parameterPanel/InputSection.tsx +1094 -1094
- package/client/src/components/parameterPanel/LocationPanelLayout.tsx +64 -64
- package/client/src/components/parameterPanel/MapsSection.tsx +91 -91
- package/client/src/components/parameterPanel/MiddleSection.tsx +867 -571
- package/client/src/components/parameterPanel/OutputSection.tsx +80 -80
- package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +81 -81
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -436
- package/client/src/components/parameterPanel/index.ts +41 -41
- package/client/src/components/shared/DataPanel.tsx +142 -142
- package/client/src/components/shared/JSONTreeRenderer.tsx +105 -105
- package/client/src/components/ui/AIResultModal.tsx +203 -203
- package/client/src/components/ui/ApiKeyInput.tsx +93 -0
- package/client/src/components/ui/CodeEditor.tsx +81 -81
- package/client/src/components/ui/CollapsibleSection.tsx +87 -87
- package/client/src/components/ui/ComponentItem.tsx +153 -153
- package/client/src/components/ui/ComponentPalette.tsx +320 -320
- package/client/src/components/ui/ConsolePanel.tsx +151 -43
- package/client/src/components/ui/ErrorBoundary.tsx +195 -195
- package/client/src/components/ui/InputNodesPanel.tsx +203 -203
- package/client/src/components/ui/MapSelector.tsx +313 -313
- package/client/src/components/ui/Modal.tsx +151 -148
- package/client/src/components/ui/NodeOutputPanel.tsx +1150 -1150
- package/client/src/components/ui/OutputDisplayPanel.tsx +381 -381
- package/client/src/components/ui/QRCodeDisplay.tsx +182 -0
- package/client/src/components/ui/TopToolbar.tsx +736 -736
- package/client/src/components/ui/WorkflowSidebar.tsx +293 -293
- package/client/src/config/antdTheme.ts +186 -186
- package/client/src/contexts/AuthContext.tsx +221 -221
- package/client/src/contexts/ThemeContext.tsx +42 -42
- package/client/src/contexts/WebSocketContext.tsx +2144 -1971
- package/client/src/factories/baseChatModelFactory.ts +255 -255
- package/client/src/hooks/useAndroidOperations.ts +118 -164
- package/client/src/hooks/useApiKeyValidation.ts +106 -106
- package/client/src/hooks/useApiKeys.ts +238 -238
- package/client/src/hooks/useAppTheme.ts +17 -17
- package/client/src/hooks/useComponentPalette.ts +50 -50
- package/client/src/hooks/useDragAndDrop.ts +123 -123
- package/client/src/hooks/useDragVariable.ts +88 -88
- package/client/src/hooks/useExecution.ts +319 -313
- package/client/src/hooks/useParameterPanel.ts +176 -176
- package/client/src/hooks/useReactFlowNodes.ts +188 -188
- package/client/src/hooks/useToolSchema.ts +209 -209
- package/client/src/hooks/useWhatsApp.ts +196 -196
- package/client/src/hooks/useWorkflowManagement.ts +45 -45
- package/client/src/index.css +314 -314
- package/client/src/nodeDefinitions/aiAgentNodes.ts +335 -335
- package/client/src/nodeDefinitions/aiModelNodes.ts +340 -340
- package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -383
- package/client/src/nodeDefinitions/chatNodes.ts +135 -135
- package/client/src/nodeDefinitions/codeNodes.ts +54 -54
- package/client/src/nodeDefinitions/index.ts +14 -14
- package/client/src/nodeDefinitions/locationNodes.ts +462 -462
- package/client/src/nodeDefinitions/schedulerNodes.ts +220 -220
- package/client/src/nodeDefinitions/skillNodes.ts +17 -5
- package/client/src/nodeDefinitions/utilityNodes.ts +284 -284
- package/client/src/nodeDefinitions/whatsappNodes.ts +821 -865
- package/client/src/nodeDefinitions.ts +101 -103
- package/client/src/services/dynamicParameterService.ts +95 -95
- package/client/src/services/execution/aiAgentExecutionService.ts +34 -34
- package/client/src/services/executionService.ts +227 -231
- package/client/src/services/workflowApi.ts +91 -91
- package/client/src/store/useAppStore.ts +578 -581
- package/client/src/styles/theme.ts +513 -508
- package/client/src/styles/zIndex.ts +16 -16
- package/client/src/types/ComponentTypes.ts +38 -38
- package/client/src/types/INodeProperties.ts +287 -287
- package/client/src/types/NodeTypes.ts +27 -27
- package/client/src/utils/formatters.ts +32 -32
- package/client/src/utils/googleMapsLoader.ts +139 -139
- package/client/src/utils/locationUtils.ts +84 -84
- package/client/src/utils/nodeUtils.ts +30 -30
- package/client/src/utils/workflow.ts +29 -29
- package/client/src/vite-env.d.ts +12 -12
- package/client/tailwind.config.js +59 -59
- package/client/tsconfig.json +25 -25
- package/client/vite.config.js +35 -35
- package/package.json +78 -70
- package/scripts/build.js +153 -45
- package/scripts/clean.js +40 -40
- package/scripts/start.js +234 -210
- package/scripts/stop.js +301 -325
- package/server/.dockerignore +44 -44
- package/server/Dockerfile +45 -45
- package/server/constants.py +244 -249
- package/server/core/cache.py +460 -460
- package/server/core/config.py +127 -127
- package/server/core/container.py +98 -98
- package/server/core/database.py +1296 -1210
- package/server/core/logging.py +313 -313
- package/server/main.py +288 -288
- package/server/middleware/__init__.py +5 -5
- package/server/middleware/auth.py +89 -89
- package/server/models/auth.py +52 -52
- package/server/models/cache.py +24 -24
- package/server/models/database.py +235 -210
- package/server/models/nodes.py +435 -455
- package/server/pyproject.toml +75 -72
- package/server/requirements.txt +83 -83
- package/server/routers/android.py +294 -294
- package/server/routers/auth.py +203 -203
- package/server/routers/database.py +150 -150
- package/server/routers/maps.py +141 -141
- package/server/routers/nodejs_compat.py +288 -288
- package/server/routers/webhook.py +90 -90
- package/server/routers/websocket.py +2239 -2127
- package/server/routers/whatsapp.py +761 -761
- package/server/routers/workflow.py +199 -199
- package/server/services/ai.py +2444 -2414
- package/server/services/android_service.py +588 -588
- package/server/services/auth.py +130 -130
- package/server/services/chat_client.py +160 -160
- package/server/services/deployment/manager.py +706 -706
- package/server/services/event_waiter.py +675 -785
- package/server/services/execution/executor.py +1351 -1351
- package/server/services/execution/models.py +1 -1
- package/server/services/handlers/__init__.py +122 -126
- package/server/services/handlers/ai.py +390 -355
- package/server/services/handlers/android.py +69 -260
- package/server/services/handlers/code.py +278 -278
- package/server/services/handlers/http.py +193 -193
- package/server/services/handlers/tools.py +146 -32
- package/server/services/handlers/triggers.py +107 -107
- package/server/services/handlers/utility.py +822 -822
- package/server/services/handlers/whatsapp.py +423 -476
- package/server/services/maps.py +288 -288
- package/server/services/memory_store.py +103 -103
- package/server/services/node_executor.py +372 -375
- package/server/services/scheduler.py +155 -155
- package/server/services/skill_loader.py +1 -1
- package/server/services/status_broadcaster.py +834 -826
- package/server/services/temporal/__init__.py +23 -23
- package/server/services/temporal/activities.py +344 -344
- package/server/services/temporal/client.py +76 -76
- package/server/services/temporal/executor.py +147 -147
- package/server/services/temporal/worker.py +251 -251
- package/server/services/temporal/workflow.py +355 -355
- package/server/services/temporal/ws_client.py +236 -236
- package/server/services/text.py +110 -110
- package/server/services/user_auth.py +172 -172
- package/server/services/websocket_client.py +29 -29
- package/server/services/workflow.py +597 -597
- package/server/skills/android-skill/SKILL.md +4 -4
- package/server/skills/code-skill/SKILL.md +123 -89
- package/server/skills/maps-skill/SKILL.md +3 -3
- package/server/skills/memory-skill/SKILL.md +1 -1
- package/server/skills/web-search-skill/SKILL.md +154 -0
- package/server/skills/whatsapp-skill/SKILL.md +3 -3
- package/server/uv.lock +461 -100
- package/server/whatsapp-rpc/.dockerignore +30 -30
- package/server/whatsapp-rpc/Dockerfile +44 -44
- package/server/whatsapp-rpc/Dockerfile.web +17 -17
- package/server/whatsapp-rpc/README.md +139 -139
- package/server/whatsapp-rpc/bin/whatsapp-rpc-server +0 -0
- package/server/whatsapp-rpc/cli.js +95 -95
- package/server/whatsapp-rpc/configs/config.yaml +6 -6
- package/server/whatsapp-rpc/docker-compose.yml +35 -35
- package/server/whatsapp-rpc/docs/API.md +410 -410
- package/server/whatsapp-rpc/node_modules/.package-lock.json +259 -0
- package/server/whatsapp-rpc/node_modules/chalk/license +9 -0
- package/server/whatsapp-rpc/node_modules/chalk/package.json +83 -0
- package/server/whatsapp-rpc/node_modules/chalk/readme.md +297 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/index.d.ts +325 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/index.js +225 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/utilities.js +33 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/server/whatsapp-rpc/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/server/whatsapp-rpc/node_modules/commander/LICENSE +22 -0
- package/server/whatsapp-rpc/node_modules/commander/Readme.md +1148 -0
- package/server/whatsapp-rpc/node_modules/commander/esm.mjs +16 -0
- package/server/whatsapp-rpc/node_modules/commander/index.js +26 -0
- package/server/whatsapp-rpc/node_modules/commander/lib/argument.js +145 -0
- package/server/whatsapp-rpc/node_modules/commander/lib/command.js +2179 -0
- package/server/whatsapp-rpc/node_modules/commander/lib/error.js +43 -0
- package/server/whatsapp-rpc/node_modules/commander/lib/help.js +462 -0
- package/server/whatsapp-rpc/node_modules/commander/lib/option.js +329 -0
- package/server/whatsapp-rpc/node_modules/commander/lib/suggestSimilar.js +100 -0
- package/server/whatsapp-rpc/node_modules/commander/package-support.json +16 -0
- package/server/whatsapp-rpc/node_modules/commander/package.json +80 -0
- package/server/whatsapp-rpc/node_modules/commander/typings/esm.d.mts +3 -0
- package/server/whatsapp-rpc/node_modules/commander/typings/index.d.ts +884 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/LICENSE +21 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/README.md +89 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/index.js +39 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/lib/enoent.js +59 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/lib/parse.js +91 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/escape.js +47 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/readShebang.js +23 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/lib/util/resolveCommand.js +52 -0
- package/server/whatsapp-rpc/node_modules/cross-spawn/package.json +73 -0
- package/server/whatsapp-rpc/node_modules/execa/index.d.ts +955 -0
- package/server/whatsapp-rpc/node_modules/execa/index.js +309 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/command.js +119 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/error.js +87 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/kill.js +102 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/pipe.js +42 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/promise.js +36 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/stdio.js +49 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/stream.js +133 -0
- package/server/whatsapp-rpc/node_modules/execa/lib/verbose.js +19 -0
- package/server/whatsapp-rpc/node_modules/execa/license +9 -0
- package/server/whatsapp-rpc/node_modules/execa/package.json +90 -0
- package/server/whatsapp-rpc/node_modules/execa/readme.md +822 -0
- package/server/whatsapp-rpc/node_modules/get-stream/license +9 -0
- package/server/whatsapp-rpc/node_modules/get-stream/package.json +53 -0
- package/server/whatsapp-rpc/node_modules/get-stream/readme.md +291 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/array-buffer.js +84 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/array.js +32 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/buffer.js +20 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/contents.js +101 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/index.d.ts +119 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/index.js +5 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/string.js +36 -0
- package/server/whatsapp-rpc/node_modules/get-stream/source/utils.js +11 -0
- package/server/whatsapp-rpc/node_modules/get-them-args/LICENSE +21 -0
- package/server/whatsapp-rpc/node_modules/get-them-args/README.md +95 -0
- package/server/whatsapp-rpc/node_modules/get-them-args/index.js +97 -0
- package/server/whatsapp-rpc/node_modules/get-them-args/package.json +36 -0
- package/server/whatsapp-rpc/node_modules/human-signals/LICENSE +201 -0
- package/server/whatsapp-rpc/node_modules/human-signals/README.md +168 -0
- package/server/whatsapp-rpc/node_modules/human-signals/build/src/core.js +273 -0
- package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.d.ts +73 -0
- package/server/whatsapp-rpc/node_modules/human-signals/build/src/main.js +70 -0
- package/server/whatsapp-rpc/node_modules/human-signals/build/src/realtime.js +16 -0
- package/server/whatsapp-rpc/node_modules/human-signals/build/src/signals.js +34 -0
- package/server/whatsapp-rpc/node_modules/human-signals/package.json +61 -0
- package/server/whatsapp-rpc/node_modules/is-stream/index.d.ts +81 -0
- package/server/whatsapp-rpc/node_modules/is-stream/index.js +29 -0
- package/server/whatsapp-rpc/node_modules/is-stream/license +9 -0
- package/server/whatsapp-rpc/node_modules/is-stream/package.json +44 -0
- package/server/whatsapp-rpc/node_modules/is-stream/readme.md +60 -0
- package/server/whatsapp-rpc/node_modules/isexe/LICENSE +15 -0
- package/server/whatsapp-rpc/node_modules/isexe/README.md +51 -0
- package/server/whatsapp-rpc/node_modules/isexe/index.js +57 -0
- package/server/whatsapp-rpc/node_modules/isexe/mode.js +41 -0
- package/server/whatsapp-rpc/node_modules/isexe/package.json +31 -0
- package/server/whatsapp-rpc/node_modules/isexe/test/basic.js +221 -0
- package/server/whatsapp-rpc/node_modules/isexe/windows.js +42 -0
- package/server/whatsapp-rpc/node_modules/kill-port/.editorconfig +12 -0
- package/server/whatsapp-rpc/node_modules/kill-port/.gitattributes +1 -0
- package/server/whatsapp-rpc/node_modules/kill-port/LICENSE +21 -0
- package/server/whatsapp-rpc/node_modules/kill-port/README.md +140 -0
- package/server/whatsapp-rpc/node_modules/kill-port/cli.js +25 -0
- package/server/whatsapp-rpc/node_modules/kill-port/example.js +21 -0
- package/server/whatsapp-rpc/node_modules/kill-port/index.js +46 -0
- package/server/whatsapp-rpc/node_modules/kill-port/logo.png +0 -0
- package/server/whatsapp-rpc/node_modules/kill-port/package.json +41 -0
- package/server/whatsapp-rpc/node_modules/kill-port/pnpm-lock.yaml +4606 -0
- package/server/whatsapp-rpc/node_modules/kill-port/test.js +16 -0
- package/server/whatsapp-rpc/node_modules/merge-stream/LICENSE +21 -0
- package/server/whatsapp-rpc/node_modules/merge-stream/README.md +78 -0
- package/server/whatsapp-rpc/node_modules/merge-stream/index.js +41 -0
- package/server/whatsapp-rpc/node_modules/merge-stream/package.json +19 -0
- package/server/whatsapp-rpc/node_modules/mimic-fn/index.d.ts +52 -0
- package/server/whatsapp-rpc/node_modules/mimic-fn/index.js +71 -0
- package/server/whatsapp-rpc/node_modules/mimic-fn/license +9 -0
- package/server/whatsapp-rpc/node_modules/mimic-fn/package.json +45 -0
- package/server/whatsapp-rpc/node_modules/mimic-fn/readme.md +90 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/index.d.ts +90 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/index.js +52 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/license +9 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.d.ts +31 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/index.js +12 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/license +9 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/package.json +41 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/node_modules/path-key/readme.md +57 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/package.json +49 -0
- package/server/whatsapp-rpc/node_modules/npm-run-path/readme.md +104 -0
- package/server/whatsapp-rpc/node_modules/onetime/index.d.ts +59 -0
- package/server/whatsapp-rpc/node_modules/onetime/index.js +41 -0
- package/server/whatsapp-rpc/node_modules/onetime/license +9 -0
- package/server/whatsapp-rpc/node_modules/onetime/package.json +45 -0
- package/server/whatsapp-rpc/node_modules/onetime/readme.md +94 -0
- package/server/whatsapp-rpc/node_modules/path-key/index.d.ts +40 -0
- package/server/whatsapp-rpc/node_modules/path-key/index.js +16 -0
- package/server/whatsapp-rpc/node_modules/path-key/license +9 -0
- package/server/whatsapp-rpc/node_modules/path-key/package.json +39 -0
- package/server/whatsapp-rpc/node_modules/path-key/readme.md +61 -0
- package/server/whatsapp-rpc/node_modules/shebang-command/index.js +19 -0
- package/server/whatsapp-rpc/node_modules/shebang-command/license +9 -0
- package/server/whatsapp-rpc/node_modules/shebang-command/package.json +34 -0
- package/server/whatsapp-rpc/node_modules/shebang-command/readme.md +34 -0
- package/server/whatsapp-rpc/node_modules/shebang-regex/index.d.ts +22 -0
- package/server/whatsapp-rpc/node_modules/shebang-regex/index.js +2 -0
- package/server/whatsapp-rpc/node_modules/shebang-regex/license +9 -0
- package/server/whatsapp-rpc/node_modules/shebang-regex/package.json +35 -0
- package/server/whatsapp-rpc/node_modules/shebang-regex/readme.md +33 -0
- package/server/whatsapp-rpc/node_modules/shell-exec/LICENSE +21 -0
- package/server/whatsapp-rpc/node_modules/shell-exec/README.md +60 -0
- package/server/whatsapp-rpc/node_modules/shell-exec/index.js +47 -0
- package/server/whatsapp-rpc/node_modules/shell-exec/package.json +29 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/LICENSE.txt +16 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/README.md +74 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts +12 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.d.ts.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js +10 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/browser.js.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts +48 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.d.ts.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js +279 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/index.js.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/package.json +3 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts +29 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.d.ts.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js +42 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/cjs/signals.js.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts +12 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.d.ts.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js +4 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/browser.js.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts +48 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.d.ts.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js +275 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/index.js.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/package.json +3 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts +29 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.d.ts.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js +39 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/dist/mjs/signals.js.map +1 -0
- package/server/whatsapp-rpc/node_modules/signal-exit/package.json +106 -0
- package/server/whatsapp-rpc/node_modules/strip-final-newline/index.js +14 -0
- package/server/whatsapp-rpc/node_modules/strip-final-newline/license +9 -0
- package/server/whatsapp-rpc/node_modules/strip-final-newline/package.json +43 -0
- package/server/whatsapp-rpc/node_modules/strip-final-newline/readme.md +35 -0
- package/server/whatsapp-rpc/node_modules/which/CHANGELOG.md +166 -0
- package/server/whatsapp-rpc/node_modules/which/LICENSE +15 -0
- package/server/whatsapp-rpc/node_modules/which/README.md +54 -0
- package/server/whatsapp-rpc/node_modules/which/bin/node-which +52 -0
- package/server/whatsapp-rpc/node_modules/which/package.json +43 -0
- package/server/whatsapp-rpc/node_modules/which/which.js +125 -0
- package/server/whatsapp-rpc/package-lock.json +272 -0
- package/server/whatsapp-rpc/package.json +30 -30
- package/server/whatsapp-rpc/schema.json +1294 -1294
- package/server/whatsapp-rpc/scripts/clean.cjs +66 -66
- package/server/whatsapp-rpc/scripts/cli.js +162 -162
- package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -166
- package/server/whatsapp-rpc/src/python/pyproject.toml +15 -15
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -4
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -427
- package/server/whatsapp-rpc/web/app.py +609 -609
- package/server/whatsapp-rpc/web/requirements.txt +6 -6
- package/server/whatsapp-rpc/web/rpc_client.py +427 -427
- package/server/whatsapp-rpc/web/static/openapi.yaml +59 -59
- package/server/whatsapp-rpc/web/templates/base.html +149 -149
- package/server/whatsapp-rpc/web/templates/contacts.html +240 -240
- package/server/whatsapp-rpc/web/templates/dashboard.html +319 -319
- package/server/whatsapp-rpc/web/templates/groups.html +328 -328
- package/server/whatsapp-rpc/web/templates/messages.html +465 -465
- package/server/whatsapp-rpc/web/templates/messaging.html +680 -680
- package/server/whatsapp-rpc/web/templates/send.html +258 -258
- package/server/whatsapp-rpc/web/templates/settings.html +459 -459
- package/client/src/components/ui/AndroidSettingsPanel.tsx +0 -401
- package/client/src/components/ui/WhatsAppSettingsPanel.tsx +0 -345
- package/client/src/nodeDefinitions/androidDeviceNodes.ts +0 -140
- package/docker-compose.prod.yml +0 -107
- package/docker-compose.yml +0 -104
- package/docs-MachinaOs/README.md +0 -85
- package/docs-MachinaOs/deployment/docker.mdx +0 -228
- package/docs-MachinaOs/deployment/production.mdx +0 -345
- package/docs-MachinaOs/docs.json +0 -75
- package/docs-MachinaOs/faq.mdx +0 -309
- package/docs-MachinaOs/favicon.svg +0 -5
- package/docs-MachinaOs/installation.mdx +0 -160
- package/docs-MachinaOs/introduction.mdx +0 -114
- package/docs-MachinaOs/logo/dark.svg +0 -6
- package/docs-MachinaOs/logo/light.svg +0 -6
- package/docs-MachinaOs/nodes/ai-agent.mdx +0 -216
- package/docs-MachinaOs/nodes/ai-models.mdx +0 -240
- package/docs-MachinaOs/nodes/android.mdx +0 -411
- package/docs-MachinaOs/nodes/overview.mdx +0 -181
- package/docs-MachinaOs/nodes/schedulers.mdx +0 -316
- package/docs-MachinaOs/nodes/webhooks.mdx +0 -330
- package/docs-MachinaOs/nodes/whatsapp.mdx +0 -305
- package/docs-MachinaOs/quickstart.mdx +0 -119
- package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +0 -177
- package/docs-MachinaOs/tutorials/android-automation.mdx +0 -242
- package/docs-MachinaOs/tutorials/first-workflow.mdx +0 -134
- package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +0 -185
- package/nul +0 -0
- package/scripts/check-ports.ps1 +0 -33
- package/scripts/kill-port.ps1 +0 -154
|
@@ -1,761 +1,761 @@
|
|
|
1
|
-
"""
|
|
2
|
-
WhatsApp Service - JSON-RPC 2.0 integration with Go whatsmeow service.
|
|
3
|
-
|
|
4
|
-
This module provides WebSocket handlers for WhatsApp operations.
|
|
5
|
-
All communication goes through the RPCClient to the Go service.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import asyncio
|
|
9
|
-
import base64
|
|
10
|
-
import io
|
|
11
|
-
import json
|
|
12
|
-
import logging
|
|
13
|
-
import os
|
|
14
|
-
import time
|
|
15
|
-
from typing import Any, Optional
|
|
16
|
-
|
|
17
|
-
import qrcode
|
|
18
|
-
import websockets
|
|
19
|
-
from websockets.exceptions import ConnectionClosed
|
|
20
|
-
from fastapi import HTTPException
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def qr_code_to_base64(code: str) -> str:
|
|
24
|
-
"""Convert QR code string to base64 PNG image."""
|
|
25
|
-
qr = qrcode.QRCode(version=1, box_size=10, border=4)
|
|
26
|
-
qr.add_data(code)
|
|
27
|
-
qr.make(fit=True)
|
|
28
|
-
img = qr.make_image(fill_color="black", back_color="white")
|
|
29
|
-
buffer = io.BytesIO()
|
|
30
|
-
img.save(buffer, format="PNG")
|
|
31
|
-
return base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
logger = logging.getLogger(__name__)
|
|
35
|
-
|
|
36
|
-
WHATSAPP_RPC_URL = os.getenv("WHATSAPP_RPC_URL", "ws://localhost:9400/ws/rpc")
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# Inline RPC Client with async event handling
|
|
40
|
-
class RPCClient:
|
|
41
|
-
def __init__(self, url: str):
|
|
42
|
-
self.url, self.ws, self.req_id = url, None, 0
|
|
43
|
-
self.pending: dict[int, asyncio.Future] = {}
|
|
44
|
-
self._connected, self._task = False, None
|
|
45
|
-
self._event_handler = None
|
|
46
|
-
|
|
47
|
-
@property
|
|
48
|
-
def connected(self):
|
|
49
|
-
"""Check if actually connected - verify WebSocket is open."""
|
|
50
|
-
if not self._connected or not self.ws:
|
|
51
|
-
return False
|
|
52
|
-
# websockets 15.x uses state instead of closed (state.value: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)
|
|
53
|
-
try:
|
|
54
|
-
return self.ws.state.value == 1
|
|
55
|
-
except Exception:
|
|
56
|
-
return False
|
|
57
|
-
|
|
58
|
-
def set_event_handler(self, handler):
|
|
59
|
-
"""Set callback for handling async events from Go service."""
|
|
60
|
-
self._event_handler = handler
|
|
61
|
-
|
|
62
|
-
async def connect(self):
|
|
63
|
-
# 2 second timeout for initial connection (fail fast if Go service not running)
|
|
64
|
-
logger.info(f"[WhatsApp RPC] Connecting to {self.url}...")
|
|
65
|
-
self.ws = await asyncio.wait_for(
|
|
66
|
-
websockets.connect(self.url, ping_interval=30, max_size=100*1024*1024),
|
|
67
|
-
timeout=2.0
|
|
68
|
-
)
|
|
69
|
-
self._connected = True
|
|
70
|
-
logger.info("[WhatsApp RPC] WebSocket connected, starting receive loop")
|
|
71
|
-
self._task = asyncio.create_task(self._recv())
|
|
72
|
-
|
|
73
|
-
async def close(self):
|
|
74
|
-
self._connected = False
|
|
75
|
-
if self._task: self._task.cancel()
|
|
76
|
-
if self.ws: await self.ws.close()
|
|
77
|
-
|
|
78
|
-
async def _recv(self):
|
|
79
|
-
try:
|
|
80
|
-
logger.info("[WhatsApp RPC] Receive loop started")
|
|
81
|
-
async for msg in self.ws:
|
|
82
|
-
data = json.loads(msg)
|
|
83
|
-
logger.debug(f"[WhatsApp RPC] Received: {data.get('method', data.get('id', 'unknown'))}")
|
|
84
|
-
if data.get("id") in self.pending:
|
|
85
|
-
self.pending[data["id"]].set_result(data)
|
|
86
|
-
elif "method" in data and "id" not in data:
|
|
87
|
-
await self._handle_event(data)
|
|
88
|
-
except ConnectionClosed as e:
|
|
89
|
-
logger.warning(f"[WhatsApp RPC] Connection closed: {e}")
|
|
90
|
-
self._connected = False
|
|
91
|
-
except Exception as e:
|
|
92
|
-
logger.error(f"[WhatsApp RPC] Receive loop error: {e}")
|
|
93
|
-
self._connected = False
|
|
94
|
-
|
|
95
|
-
async def _handle_event(self, data: dict):
|
|
96
|
-
"""Handle async events from Go service and broadcast to frontend.
|
|
97
|
-
|
|
98
|
-
Events from schema.json:
|
|
99
|
-
- event.connected: {status: "connected", device_id: string}
|
|
100
|
-
- event.disconnected: {status: "disconnected", reason: string}
|
|
101
|
-
- event.connection_failure: {error: string, reason: string}
|
|
102
|
-
- event.logged_out: {on_connect: boolean, reason: string}
|
|
103
|
-
- event.temporary_ban: {code: string, reason: string}
|
|
104
|
-
- event.qr_code: {code: string, filename: string}
|
|
105
|
-
- event.message_sent: {message_id, to, type, timestamp}
|
|
106
|
-
- event.message_received: {message_id, sender, chat_id, ...}
|
|
107
|
-
"""
|
|
108
|
-
method = data.get("method", "")
|
|
109
|
-
params = data.get("params", {})
|
|
110
|
-
logger.debug(f"[WhatsApp RPC] Event: {method}")
|
|
111
|
-
|
|
112
|
-
try:
|
|
113
|
-
from services.status_broadcaster import get_status_broadcaster
|
|
114
|
-
broadcaster = get_status_broadcaster()
|
|
115
|
-
|
|
116
|
-
if method == "event.status":
|
|
117
|
-
# Initial status sent on WebSocket connection
|
|
118
|
-
await broadcaster.update_whatsapp_status(
|
|
119
|
-
connected=params.get("connected", False),
|
|
120
|
-
has_session=params.get("has_session", False),
|
|
121
|
-
running=params.get("running", False),
|
|
122
|
-
pairing=params.get("pairing", False),
|
|
123
|
-
device_id=params.get("device_id"),
|
|
124
|
-
qr=None
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
elif method == "event.connected":
|
|
128
|
-
# Connected successfully with device_id
|
|
129
|
-
await broadcaster.update_whatsapp_status(
|
|
130
|
-
connected=True,
|
|
131
|
-
has_session=True,
|
|
132
|
-
running=True,
|
|
133
|
-
pairing=False,
|
|
134
|
-
device_id=params.get("device_id"),
|
|
135
|
-
qr=None
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
elif method == "event.disconnected":
|
|
139
|
-
# Disconnected - service still running
|
|
140
|
-
await broadcaster.update_whatsapp_status(
|
|
141
|
-
connected=False,
|
|
142
|
-
has_session=False,
|
|
143
|
-
running=True,
|
|
144
|
-
pairing=False,
|
|
145
|
-
device_id=None,
|
|
146
|
-
qr=None
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
elif method == "event.connection_failure":
|
|
150
|
-
# Connection failed
|
|
151
|
-
logger.error(f"[WhatsApp] Connection failure: {params.get('error')} - {params.get('reason')}")
|
|
152
|
-
await broadcaster.update_whatsapp_status(
|
|
153
|
-
connected=False,
|
|
154
|
-
has_session=False,
|
|
155
|
-
running=True,
|
|
156
|
-
pairing=False,
|
|
157
|
-
device_id=None,
|
|
158
|
-
qr=None
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
elif method == "event.logged_out":
|
|
162
|
-
# Logged out - session cleared
|
|
163
|
-
logger.warning(f"[WhatsApp] Logged out: {params.get('reason')}")
|
|
164
|
-
await broadcaster.update_whatsapp_status(
|
|
165
|
-
connected=False,
|
|
166
|
-
has_session=False,
|
|
167
|
-
running=True,
|
|
168
|
-
pairing=False,
|
|
169
|
-
device_id=None,
|
|
170
|
-
qr=None
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
elif method == "event.temporary_ban":
|
|
174
|
-
# Temporary ban
|
|
175
|
-
logger.error(f"[WhatsApp] Temporary ban: code={params.get('code')} reason={params.get('reason')}")
|
|
176
|
-
await broadcaster.update_whatsapp_status(
|
|
177
|
-
connected=False,
|
|
178
|
-
has_session=False,
|
|
179
|
-
running=True,
|
|
180
|
-
pairing=False,
|
|
181
|
-
device_id=None,
|
|
182
|
-
qr=None
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
elif method == "event.qr_code":
|
|
186
|
-
# New QR code available for pairing
|
|
187
|
-
code = params.get("code")
|
|
188
|
-
qr_image = qr_code_to_base64(code) if code else None
|
|
189
|
-
await broadcaster.update_whatsapp_status(
|
|
190
|
-
connected=False,
|
|
191
|
-
has_session=False,
|
|
192
|
-
running=True,
|
|
193
|
-
pairing=True,
|
|
194
|
-
device_id=None,
|
|
195
|
-
qr=qr_image
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
elif method == "event.message_sent":
|
|
199
|
-
# Message sent - broadcast as custom event
|
|
200
|
-
await broadcaster.send_custom_event("whatsapp_message_sent", params)
|
|
201
|
-
|
|
202
|
-
elif method == "event.message_received":
|
|
203
|
-
# Message received - broadcast as custom event for trigger nodes
|
|
204
|
-
await broadcaster.send_custom_event("whatsapp_message_received", params)
|
|
205
|
-
|
|
206
|
-
# Forward to custom handler if set
|
|
207
|
-
if self._event_handler:
|
|
208
|
-
await self._event_handler(method, params)
|
|
209
|
-
|
|
210
|
-
except Exception as e:
|
|
211
|
-
logger.error(f"[WhatsApp RPC] Event handler error: {e}")
|
|
212
|
-
|
|
213
|
-
async def call(self, method: str, params: Any = None, timeout: float = 30) -> Any:
|
|
214
|
-
if not self.connected:
|
|
215
|
-
raise Exception("Not connected to WhatsApp service")
|
|
216
|
-
self.req_id += 1
|
|
217
|
-
req_id = self.req_id # Capture request ID before any await
|
|
218
|
-
req = {"jsonrpc": "2.0", "id": req_id, "method": method}
|
|
219
|
-
if params:
|
|
220
|
-
req["params"] = params
|
|
221
|
-
|
|
222
|
-
# Get current event loop for future
|
|
223
|
-
try:
|
|
224
|
-
loop = asyncio.get_running_loop()
|
|
225
|
-
except RuntimeError:
|
|
226
|
-
loop = asyncio.get_event_loop()
|
|
227
|
-
future = loop.create_future()
|
|
228
|
-
self.pending[req_id] = future
|
|
229
|
-
|
|
230
|
-
try:
|
|
231
|
-
await self.ws.send(json.dumps(req))
|
|
232
|
-
resp = await asyncio.wait_for(future, timeout)
|
|
233
|
-
if resp.get("error"):
|
|
234
|
-
raise Exception(resp["error"].get("message", "RPC Error"))
|
|
235
|
-
return resp.get("result")
|
|
236
|
-
except asyncio.TimeoutError:
|
|
237
|
-
raise Exception(f"RPC call '{method}' timed out after {timeout}s")
|
|
238
|
-
except ConnectionClosed as e:
|
|
239
|
-
logger.error(f"[WhatsApp RPC] Connection closed during {method}: {e}")
|
|
240
|
-
self._connected = False
|
|
241
|
-
raise Exception(f"Connection lost during {method}")
|
|
242
|
-
finally:
|
|
243
|
-
self.pending.pop(req_id, None)
|
|
244
|
-
|
|
245
|
-
_client: Optional[RPCClient] = None
|
|
246
|
-
_lock = asyncio.Lock()
|
|
247
|
-
_send_lock = asyncio.Lock() # Serialize sends - Go service processes sequentially
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
async def reset_client():
|
|
251
|
-
"""Force reset the RPC client connection."""
|
|
252
|
-
global _client
|
|
253
|
-
async with _lock:
|
|
254
|
-
if _client:
|
|
255
|
-
try:
|
|
256
|
-
await _client.close()
|
|
257
|
-
except Exception:
|
|
258
|
-
pass
|
|
259
|
-
_client = None
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
async def get_client(force_reconnect: bool = False) -> RPCClient:
|
|
263
|
-
"""Get or create RPC client. Use force_reconnect=True to ensure fresh connection."""
|
|
264
|
-
global _client
|
|
265
|
-
async with _lock:
|
|
266
|
-
# Force reconnect if requested or if client is stale
|
|
267
|
-
if force_reconnect and _client:
|
|
268
|
-
logger.info("[WhatsApp RPC] Force reconnecting...")
|
|
269
|
-
try:
|
|
270
|
-
await _client.close()
|
|
271
|
-
except Exception:
|
|
272
|
-
pass
|
|
273
|
-
_client = None
|
|
274
|
-
|
|
275
|
-
if not _client or not _client.connected:
|
|
276
|
-
logger.info(f"[WhatsApp RPC] Creating new connection to {WHATSAPP_RPC_URL}")
|
|
277
|
-
_client = RPCClient(WHATSAPP_RPC_URL)
|
|
278
|
-
try:
|
|
279
|
-
await _client.connect()
|
|
280
|
-
logger.info("[WhatsApp RPC] Connected successfully")
|
|
281
|
-
except asyncio.TimeoutError:
|
|
282
|
-
_client = None
|
|
283
|
-
logger.error(f"WhatsApp RPC timeout - Go service not responding at {WHATSAPP_RPC_URL}")
|
|
284
|
-
raise Exception("WhatsApp service timeout - is Go service running?")
|
|
285
|
-
except (ConnectionRefusedError, OSError) as e:
|
|
286
|
-
_client = None
|
|
287
|
-
logger.error(f"WhatsApp RPC connection refused: {e}")
|
|
288
|
-
raise Exception("WhatsApp service not running - start Go whatsmeow service on port 9400")
|
|
289
|
-
except Exception as e:
|
|
290
|
-
_client = None
|
|
291
|
-
logger.error(f"WhatsApp RPC error: {e}")
|
|
292
|
-
raise Exception(f"WhatsApp connection failed: {e}")
|
|
293
|
-
return _client
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
# ============================================================================
|
|
297
|
-
# WebSocket Handlers - used by websocket.py
|
|
298
|
-
# ============================================================================
|
|
299
|
-
|
|
300
|
-
async def handle_whatsapp_status() -> dict:
|
|
301
|
-
"""Get WhatsApp connection status via direct RPC and broadcast to all clients."""
|
|
302
|
-
try:
|
|
303
|
-
client = await get_client()
|
|
304
|
-
status_data = await client.call("status")
|
|
305
|
-
|
|
306
|
-
# Broadcast status update to all connected WebSocket clients
|
|
307
|
-
from services.status_broadcaster import get_status_broadcaster
|
|
308
|
-
broadcaster = get_status_broadcaster()
|
|
309
|
-
await broadcaster.update_whatsapp_status(
|
|
310
|
-
connected=status_data.get("connected", False),
|
|
311
|
-
has_session=status_data.get("has_session", False),
|
|
312
|
-
running=status_data.get("running", False),
|
|
313
|
-
pairing=status_data.get("pairing", False),
|
|
314
|
-
device_id=status_data.get("device_id"),
|
|
315
|
-
qr=None # QR code comes from event.qr_code events
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
return {
|
|
319
|
-
"success": True,
|
|
320
|
-
"data": status_data,
|
|
321
|
-
"connected": status_data.get("connected", False),
|
|
322
|
-
"device_id": status_data.get("device_id"),
|
|
323
|
-
"timestamp": time.time()
|
|
324
|
-
}
|
|
325
|
-
except Exception as e:
|
|
326
|
-
logger.error(f"WhatsApp status check failed: {e}")
|
|
327
|
-
# Return error response immediately - don't broadcast here to avoid race conditions
|
|
328
|
-
# The client will update its local state based on the error response
|
|
329
|
-
return {
|
|
330
|
-
"success": False,
|
|
331
|
-
"error": str(e),
|
|
332
|
-
"connected": False,
|
|
333
|
-
"running": False,
|
|
334
|
-
"timestamp": time.time()
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
async def handle_whatsapp_qr() -> dict:
|
|
339
|
-
"""Get WhatsApp QR code for authentication via direct RPC."""
|
|
340
|
-
try:
|
|
341
|
-
client = await get_client()
|
|
342
|
-
status = await client.call("status")
|
|
343
|
-
|
|
344
|
-
if status.get("connected") and status.get("has_session"):
|
|
345
|
-
return {
|
|
346
|
-
"success": True,
|
|
347
|
-
"connected": True,
|
|
348
|
-
"message": "Already connected with active session",
|
|
349
|
-
"timestamp": time.time()
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
try:
|
|
353
|
-
result = await client.call("qr")
|
|
354
|
-
code = result.get("code")
|
|
355
|
-
if code:
|
|
356
|
-
qr_image = qr_code_to_base64(code)
|
|
357
|
-
return {
|
|
358
|
-
"success": True,
|
|
359
|
-
"connected": False,
|
|
360
|
-
"qr": qr_image,
|
|
361
|
-
"message": "QR code available",
|
|
362
|
-
"timestamp": time.time()
|
|
363
|
-
}
|
|
364
|
-
return {
|
|
365
|
-
"success": True,
|
|
366
|
-
"connected": False,
|
|
367
|
-
"qr": None,
|
|
368
|
-
"message": "No QR code available",
|
|
369
|
-
"timestamp": time.time()
|
|
370
|
-
}
|
|
371
|
-
except Exception as qr_err:
|
|
372
|
-
return {
|
|
373
|
-
"success": True,
|
|
374
|
-
"connected": False,
|
|
375
|
-
"qr": None,
|
|
376
|
-
"message": str(qr_err),
|
|
377
|
-
"timestamp": time.time()
|
|
378
|
-
}
|
|
379
|
-
except Exception as e:
|
|
380
|
-
logger.error(f"WhatsApp QR fetch failed: {e}")
|
|
381
|
-
return {"success": False, "connected": False, "error": str(e)}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
async def handle_whatsapp_send(params: dict) -> dict:
|
|
385
|
-
"""Send a WhatsApp message via direct RPC - supports all message types.
|
|
386
|
-
|
|
387
|
-
Uses _send_lock to serialize sends - Go service processes sequentially.
|
|
388
|
-
|
|
389
|
-
Params from frontend node (snake_case):
|
|
390
|
-
- recipient_type: 'phone' or 'group'
|
|
391
|
-
- phone: recipient phone number (if recipient_type='phone')
|
|
392
|
-
- group_id: group JID (if recipient_type='group')
|
|
393
|
-
- message_type: text, image, video, audio, document, sticker, location, contact
|
|
394
|
-
- message: text content (for text type)
|
|
395
|
-
- media_source: base64, file, url (for media types)
|
|
396
|
-
- media_data/file_path/media_url: media content based on source
|
|
397
|
-
- mime_type, caption, filename: media options
|
|
398
|
-
- latitude, longitude, location_name, address: location data
|
|
399
|
-
- contact_name, vcard: contact data
|
|
400
|
-
- is_reply, reply_message_id, reply_sender, reply_content: reply context
|
|
401
|
-
"""
|
|
402
|
-
async with _send_lock:
|
|
403
|
-
try:
|
|
404
|
-
# Build RPC params matching schema.json
|
|
405
|
-
rpc_params: dict[str, Any] = {}
|
|
406
|
-
|
|
407
|
-
# Recipient (snake_case)
|
|
408
|
-
recipient_type = params.get("recipient_type", "phone")
|
|
409
|
-
if recipient_type == "group":
|
|
410
|
-
group_id = params.get("group_id")
|
|
411
|
-
if not group_id:
|
|
412
|
-
return {"success": False, "error": "group_id is required"}
|
|
413
|
-
rpc_params["group_id"] = group_id
|
|
414
|
-
else:
|
|
415
|
-
phone = params.get("phone")
|
|
416
|
-
if not phone:
|
|
417
|
-
return {"success": False, "error": "phone is required"}
|
|
418
|
-
rpc_params["phone"] = phone
|
|
419
|
-
|
|
420
|
-
# Message type (snake_case)
|
|
421
|
-
msg_type = params.get("message_type", "text")
|
|
422
|
-
rpc_params["type"] = msg_type
|
|
423
|
-
|
|
424
|
-
# Content based on type
|
|
425
|
-
if msg_type == "text":
|
|
426
|
-
message = params.get("message")
|
|
427
|
-
if not message:
|
|
428
|
-
return {"success": False, "error": "message is required for text type"}
|
|
429
|
-
rpc_params["message"] = message
|
|
430
|
-
|
|
431
|
-
elif msg_type in ["image", "video", "audio", "document", "sticker"]:
|
|
432
|
-
media_source = params.get("media_source", "base64")
|
|
433
|
-
media_data = None
|
|
434
|
-
mime_type = params.get("mime_type")
|
|
435
|
-
filename = params.get("filename")
|
|
436
|
-
|
|
437
|
-
if media_source == "base64":
|
|
438
|
-
media_data = params.get("media_data")
|
|
439
|
-
elif media_source == "file":
|
|
440
|
-
file_param = params.get("file_path")
|
|
441
|
-
if isinstance(file_param, dict) and file_param.get("type") == "upload":
|
|
442
|
-
media_data = file_param.get("data")
|
|
443
|
-
mime_type = mime_type or file_param.get("mimeType")
|
|
444
|
-
filename = filename or file_param.get("filename")
|
|
445
|
-
elif file_param:
|
|
446
|
-
import base64 as b64
|
|
447
|
-
try:
|
|
448
|
-
with open(file_param, "rb") as f:
|
|
449
|
-
media_data = b64.b64encode(f.read()).decode("utf-8")
|
|
450
|
-
except Exception as e:
|
|
451
|
-
return {"success": False, "error": f"Failed to read file: {e}"}
|
|
452
|
-
elif media_source == "url":
|
|
453
|
-
media_url = params.get("media_url")
|
|
454
|
-
if media_url:
|
|
455
|
-
import httpx
|
|
456
|
-
import base64 as b64
|
|
457
|
-
try:
|
|
458
|
-
async with httpx.AsyncClient() as http:
|
|
459
|
-
resp = await http.get(media_url, timeout=30)
|
|
460
|
-
media_data = b64.b64encode(resp.content).decode("utf-8")
|
|
461
|
-
except Exception as e:
|
|
462
|
-
return {"success": False, "error": f"Failed to download media: {e}"}
|
|
463
|
-
|
|
464
|
-
if not media_data:
|
|
465
|
-
return {"success": False, "error": f"media data is required for {msg_type} type"}
|
|
466
|
-
|
|
467
|
-
rpc_params["media_data"] = {
|
|
468
|
-
"data": media_data,
|
|
469
|
-
"mime_type": mime_type or _guess_mime_type(msg_type)
|
|
470
|
-
}
|
|
471
|
-
if params.get("caption"):
|
|
472
|
-
rpc_params["media_data"]["caption"] = params["caption"]
|
|
473
|
-
final_filename = filename or params.get("filename")
|
|
474
|
-
if final_filename:
|
|
475
|
-
rpc_params["media_data"]["filename"] = final_filename
|
|
476
|
-
|
|
477
|
-
elif msg_type == "location":
|
|
478
|
-
lat = params.get("latitude")
|
|
479
|
-
lng = params.get("longitude")
|
|
480
|
-
if lat is None or lng is None:
|
|
481
|
-
return {"success": False, "error": "latitude and longitude are required"}
|
|
482
|
-
rpc_params["location"] = {"latitude": float(lat), "longitude": float(lng)}
|
|
483
|
-
if params.get("location_name"):
|
|
484
|
-
rpc_params["location"]["name"] = params["location_name"]
|
|
485
|
-
if params.get("address"):
|
|
486
|
-
rpc_params["location"]["address"] = params["address"]
|
|
487
|
-
|
|
488
|
-
elif msg_type == "contact":
|
|
489
|
-
contact_name = params.get("contact_name")
|
|
490
|
-
vcard = params.get("vcard")
|
|
491
|
-
if not contact_name or not vcard:
|
|
492
|
-
return {"success": False, "error": "contact_name and vcard are required"}
|
|
493
|
-
rpc_params["contact"] = {"display_name": contact_name, "vcard": vcard}
|
|
494
|
-
|
|
495
|
-
# Reply context (snake_case)
|
|
496
|
-
if params.get("is_reply"):
|
|
497
|
-
reply_id = params.get("reply_message_id")
|
|
498
|
-
reply_sender = params.get("reply_sender")
|
|
499
|
-
if reply_id and reply_sender:
|
|
500
|
-
rpc_params["reply"] = {
|
|
501
|
-
"message_id": reply_id,
|
|
502
|
-
"sender": reply_sender,
|
|
503
|
-
"content": params.get("reply_content", "")
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if params.get("metadata"):
|
|
507
|
-
rpc_params["metadata"] = params["metadata"]
|
|
508
|
-
|
|
509
|
-
client = await get_client()
|
|
510
|
-
result = await client.call("send", rpc_params)
|
|
511
|
-
return {
|
|
512
|
-
"success": True,
|
|
513
|
-
"message_id": result.get("message_id"),
|
|
514
|
-
"message_type": msg_type,
|
|
515
|
-
"timestamp": time.time()
|
|
516
|
-
}
|
|
517
|
-
except Exception as e:
|
|
518
|
-
logger.error(f"WhatsApp send failed: {e}")
|
|
519
|
-
return {"success": False, "error": str(e)}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
def _guess_mime_type(msg_type: str) -> str:
|
|
523
|
-
"""Guess default MIME type based on message type."""
|
|
524
|
-
defaults = {
|
|
525
|
-
"image": "image/jpeg",
|
|
526
|
-
"video": "video/mp4",
|
|
527
|
-
"audio": "audio/ogg",
|
|
528
|
-
"document": "application/octet-stream",
|
|
529
|
-
"sticker": "image/webp"
|
|
530
|
-
}
|
|
531
|
-
return defaults.get(msg_type, "application/octet-stream")
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
async def handle_whatsapp_start() -> dict:
|
|
535
|
-
"""Start WhatsApp connection via direct RPC and broadcast running state."""
|
|
536
|
-
try:
|
|
537
|
-
client = await get_client()
|
|
538
|
-
result = await client.call("start")
|
|
539
|
-
|
|
540
|
-
# Broadcast that service is now running (waiting for QR or connection)
|
|
541
|
-
from services.status_broadcaster import get_status_broadcaster
|
|
542
|
-
broadcaster = get_status_broadcaster()
|
|
543
|
-
await broadcaster.update_whatsapp_status(
|
|
544
|
-
connected=False,
|
|
545
|
-
has_session=False,
|
|
546
|
-
running=True,
|
|
547
|
-
pairing=False, # Will be set to True by event.qr_code event
|
|
548
|
-
device_id=None,
|
|
549
|
-
qr=None
|
|
550
|
-
)
|
|
551
|
-
|
|
552
|
-
return {
|
|
553
|
-
"success": True,
|
|
554
|
-
"message": "WhatsApp connection started",
|
|
555
|
-
"data": result,
|
|
556
|
-
"timestamp": time.time()
|
|
557
|
-
}
|
|
558
|
-
except Exception as e:
|
|
559
|
-
logger.error(f"WhatsApp start failed: {e}")
|
|
560
|
-
return {"success": False, "error": str(e)}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
async def handle_whatsapp_restart() -> dict:
|
|
564
|
-
"""Restart WhatsApp connection via direct RPC.
|
|
565
|
-
|
|
566
|
-
This calls the 'restart' RPC method which stops and starts the service,
|
|
567
|
-
unlike 'start' which only starts if not running.
|
|
568
|
-
"""
|
|
569
|
-
try:
|
|
570
|
-
# Force fresh connection to avoid stale WebSocket
|
|
571
|
-
client = await get_client(force_reconnect=True)
|
|
572
|
-
|
|
573
|
-
# Broadcast that we're restarting (brief disconnected state)
|
|
574
|
-
from services.status_broadcaster import get_status_broadcaster
|
|
575
|
-
broadcaster = get_status_broadcaster()
|
|
576
|
-
await broadcaster.update_whatsapp_status(
|
|
577
|
-
connected=False,
|
|
578
|
-
has_session=False,
|
|
579
|
-
running=True,
|
|
580
|
-
pairing=False,
|
|
581
|
-
device_id=None,
|
|
582
|
-
qr=None
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
# Call restart RPC method
|
|
586
|
-
result = await client.call("restart")
|
|
587
|
-
|
|
588
|
-
return {
|
|
589
|
-
"success": True,
|
|
590
|
-
"message": "WhatsApp connection restarted",
|
|
591
|
-
"data": result,
|
|
592
|
-
"timestamp": time.time()
|
|
593
|
-
}
|
|
594
|
-
except HTTPException as e:
|
|
595
|
-
logger.error(f"WhatsApp restart failed: {e.detail}")
|
|
596
|
-
return {"success": False, "error": e.detail}
|
|
597
|
-
except Exception as e:
|
|
598
|
-
logger.error(f"WhatsApp restart failed: {e}")
|
|
599
|
-
return {"success": False, "error": str(e)}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
async def handle_whatsapp_groups() -> dict:
|
|
603
|
-
"""Get list of WhatsApp groups via direct RPC."""
|
|
604
|
-
try:
|
|
605
|
-
client = await get_client()
|
|
606
|
-
groups = await client.call("groups")
|
|
607
|
-
|
|
608
|
-
return {
|
|
609
|
-
"success": True,
|
|
610
|
-
"groups": groups or [],
|
|
611
|
-
"timestamp": time.time()
|
|
612
|
-
}
|
|
613
|
-
except Exception as e:
|
|
614
|
-
logger.error(f"WhatsApp groups fetch failed: {e}")
|
|
615
|
-
return {"success": False, "error": str(e), "groups": []}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
async def handle_whatsapp_group_info(group_id: str) -> dict:
|
|
619
|
-
"""Get group info including participants with resolved phone numbers.
|
|
620
|
-
|
|
621
|
-
Args:
|
|
622
|
-
group_id: Group JID (e.g., '120363422738675920@g.us')
|
|
623
|
-
|
|
624
|
-
Returns:
|
|
625
|
-
Group info with participants containing both 'jid' (LID) and 'phone' (resolved number)
|
|
626
|
-
"""
|
|
627
|
-
try:
|
|
628
|
-
if not group_id:
|
|
629
|
-
return {"success": False, "error": "group_id is required", "participants": []}
|
|
630
|
-
|
|
631
|
-
client = await get_client()
|
|
632
|
-
result = await client.call("group_info", {"group_id": group_id})
|
|
633
|
-
|
|
634
|
-
if not result:
|
|
635
|
-
return {"success": False, "error": "Failed to get group info", "participants": []}
|
|
636
|
-
|
|
637
|
-
# Extract participants with phone numbers
|
|
638
|
-
participants = []
|
|
639
|
-
for p in result.get('participants', []):
|
|
640
|
-
jid = p.get('jid', '')
|
|
641
|
-
phone = p.get('phone', '')
|
|
642
|
-
name = p.get('name', '')
|
|
643
|
-
|
|
644
|
-
# Only include participants with resolved phone numbers
|
|
645
|
-
if phone:
|
|
646
|
-
participants.append({
|
|
647
|
-
"jid": jid,
|
|
648
|
-
"phone": phone,
|
|
649
|
-
"name": name or phone, # Use phone as fallback name
|
|
650
|
-
"is_admin": p.get('is_admin', False),
|
|
651
|
-
"is_super_admin": p.get('is_super_admin', False)
|
|
652
|
-
})
|
|
653
|
-
|
|
654
|
-
return {
|
|
655
|
-
"success": True,
|
|
656
|
-
"group_id": group_id,
|
|
657
|
-
"name": result.get('name', ''),
|
|
658
|
-
"participants": participants,
|
|
659
|
-
"participant_count": len(participants),
|
|
660
|
-
"timestamp": time.time()
|
|
661
|
-
}
|
|
662
|
-
except Exception as e:
|
|
663
|
-
logger.error(f"WhatsApp group_info fetch failed for {group_id}: {e}")
|
|
664
|
-
return {"success": False, "error": str(e), "participants": []}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
async def handle_whatsapp_chat_history(params: dict) -> dict:
|
|
668
|
-
"""Get chat history from WhatsApp via direct RPC.
|
|
669
|
-
|
|
670
|
-
Retrieves stored messages from the Go service's history store.
|
|
671
|
-
Messages are automatically stored from HistorySync (on first login)
|
|
672
|
-
and from real-time incoming messages.
|
|
673
|
-
|
|
674
|
-
Params:
|
|
675
|
-
- chat_id: Direct chat JID (e.g., '919876543210@s.whatsapp.net')
|
|
676
|
-
- phone: Phone number (alternative to chat_id, will be converted)
|
|
677
|
-
- group_id: Group JID (alternative for group chats)
|
|
678
|
-
- limit: Max messages to return (default 50, max 500)
|
|
679
|
-
- offset: Pagination offset (default 0)
|
|
680
|
-
- sender_phone: Filter by sender phone in group chats
|
|
681
|
-
- text_only: Only return text messages (default false)
|
|
682
|
-
|
|
683
|
-
Returns:
|
|
684
|
-
- messages: Array of MessageRecord
|
|
685
|
-
- total: Total matching messages count
|
|
686
|
-
- has_more: Whether more messages exist
|
|
687
|
-
"""
|
|
688
|
-
try:
|
|
689
|
-
client = await get_client()
|
|
690
|
-
|
|
691
|
-
# Build RPC params
|
|
692
|
-
rpc_params = {}
|
|
693
|
-
|
|
694
|
-
# Determine chat_id from various inputs
|
|
695
|
-
chat_id = params.get("chat_id")
|
|
696
|
-
phone = params.get("phone")
|
|
697
|
-
group_id = params.get("group_id")
|
|
698
|
-
|
|
699
|
-
if chat_id:
|
|
700
|
-
rpc_params["chat_id"] = chat_id
|
|
701
|
-
elif phone:
|
|
702
|
-
rpc_params["phone"] = phone
|
|
703
|
-
elif group_id:
|
|
704
|
-
rpc_params["group_id"] = group_id
|
|
705
|
-
else:
|
|
706
|
-
return {"success": False, "error": "Either chat_id, phone, or group_id is required"}
|
|
707
|
-
|
|
708
|
-
# Optional filters
|
|
709
|
-
limit = params.get("limit", 50)
|
|
710
|
-
if limit > 500:
|
|
711
|
-
limit = 500
|
|
712
|
-
rpc_params["limit"] = limit
|
|
713
|
-
|
|
714
|
-
offset = params.get("offset", 0)
|
|
715
|
-
rpc_params["offset"] = offset
|
|
716
|
-
|
|
717
|
-
sender_phone = params.get("sender_phone")
|
|
718
|
-
if sender_phone:
|
|
719
|
-
rpc_params["sender_phone"] = sender_phone
|
|
720
|
-
|
|
721
|
-
text_only = params.get("text_only", False)
|
|
722
|
-
rpc_params["text_only"] = text_only
|
|
723
|
-
|
|
724
|
-
result = await client.call("chat_history", rpc_params)
|
|
725
|
-
|
|
726
|
-
return {
|
|
727
|
-
"success": True,
|
|
728
|
-
"messages": result.get("messages", []),
|
|
729
|
-
"total": result.get("total", 0),
|
|
730
|
-
"has_more": result.get("has_more", False),
|
|
731
|
-
"timestamp": time.time()
|
|
732
|
-
}
|
|
733
|
-
except Exception as e:
|
|
734
|
-
logger.error(f"WhatsApp chat_history fetch failed: {e}")
|
|
735
|
-
return {"success": False, "error": str(e), "messages": [], "total": 0, "has_more": False}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
async def whatsapp_rpc_call(method: str, params: dict = None) -> dict:
|
|
739
|
-
"""Generic RPC call to WhatsApp Go service.
|
|
740
|
-
|
|
741
|
-
Used by handlers/whatsapp.py for operations like:
|
|
742
|
-
- groups: List all groups
|
|
743
|
-
- group_info: Get group details with participants
|
|
744
|
-
- contacts: List contacts with saved names
|
|
745
|
-
- contact_info: Get full contact info (for send/reply)
|
|
746
|
-
- contact_check: Check WhatsApp registration status
|
|
747
|
-
|
|
748
|
-
Args:
|
|
749
|
-
method: RPC method name (e.g., 'groups', 'contact_info')
|
|
750
|
-
params: Method parameters dict
|
|
751
|
-
|
|
752
|
-
Returns:
|
|
753
|
-
RPC result dict or error dict
|
|
754
|
-
"""
|
|
755
|
-
try:
|
|
756
|
-
client = await get_client()
|
|
757
|
-
result = await client.call(method, params or {})
|
|
758
|
-
return result if isinstance(result, dict) else {"result": result, "success": True}
|
|
759
|
-
except Exception as e:
|
|
760
|
-
logger.error(f"WhatsApp RPC call '{method}' failed: {e}")
|
|
761
|
-
return {"success": False, "error": str(e)}
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp Service - JSON-RPC 2.0 integration with Go whatsmeow service.
|
|
3
|
+
|
|
4
|
+
This module provides WebSocket handlers for WhatsApp operations.
|
|
5
|
+
All communication goes through the RPCClient to the Go service.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import base64
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
import qrcode
|
|
18
|
+
import websockets
|
|
19
|
+
from websockets.exceptions import ConnectionClosed
|
|
20
|
+
from fastapi import HTTPException
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def qr_code_to_base64(code: str) -> str:
|
|
24
|
+
"""Convert QR code string to base64 PNG image."""
|
|
25
|
+
qr = qrcode.QRCode(version=1, box_size=10, border=4)
|
|
26
|
+
qr.add_data(code)
|
|
27
|
+
qr.make(fit=True)
|
|
28
|
+
img = qr.make_image(fill_color="black", back_color="white")
|
|
29
|
+
buffer = io.BytesIO()
|
|
30
|
+
img.save(buffer, format="PNG")
|
|
31
|
+
return base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
WHATSAPP_RPC_URL = os.getenv("WHATSAPP_RPC_URL", "ws://localhost:9400/ws/rpc")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Inline RPC Client with async event handling
|
|
40
|
+
class RPCClient:
|
|
41
|
+
def __init__(self, url: str):
|
|
42
|
+
self.url, self.ws, self.req_id = url, None, 0
|
|
43
|
+
self.pending: dict[int, asyncio.Future] = {}
|
|
44
|
+
self._connected, self._task = False, None
|
|
45
|
+
self._event_handler = None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def connected(self):
|
|
49
|
+
"""Check if actually connected - verify WebSocket is open."""
|
|
50
|
+
if not self._connected or not self.ws:
|
|
51
|
+
return False
|
|
52
|
+
# websockets 15.x uses state instead of closed (state.value: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)
|
|
53
|
+
try:
|
|
54
|
+
return self.ws.state.value == 1
|
|
55
|
+
except Exception:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def set_event_handler(self, handler):
|
|
59
|
+
"""Set callback for handling async events from Go service."""
|
|
60
|
+
self._event_handler = handler
|
|
61
|
+
|
|
62
|
+
async def connect(self):
|
|
63
|
+
# 2 second timeout for initial connection (fail fast if Go service not running)
|
|
64
|
+
logger.info(f"[WhatsApp RPC] Connecting to {self.url}...")
|
|
65
|
+
self.ws = await asyncio.wait_for(
|
|
66
|
+
websockets.connect(self.url, ping_interval=30, max_size=100*1024*1024),
|
|
67
|
+
timeout=2.0
|
|
68
|
+
)
|
|
69
|
+
self._connected = True
|
|
70
|
+
logger.info("[WhatsApp RPC] WebSocket connected, starting receive loop")
|
|
71
|
+
self._task = asyncio.create_task(self._recv())
|
|
72
|
+
|
|
73
|
+
async def close(self):
|
|
74
|
+
self._connected = False
|
|
75
|
+
if self._task: self._task.cancel()
|
|
76
|
+
if self.ws: await self.ws.close()
|
|
77
|
+
|
|
78
|
+
async def _recv(self):
|
|
79
|
+
try:
|
|
80
|
+
logger.info("[WhatsApp RPC] Receive loop started")
|
|
81
|
+
async for msg in self.ws:
|
|
82
|
+
data = json.loads(msg)
|
|
83
|
+
logger.debug(f"[WhatsApp RPC] Received: {data.get('method', data.get('id', 'unknown'))}")
|
|
84
|
+
if data.get("id") in self.pending:
|
|
85
|
+
self.pending[data["id"]].set_result(data)
|
|
86
|
+
elif "method" in data and "id" not in data:
|
|
87
|
+
await self._handle_event(data)
|
|
88
|
+
except ConnectionClosed as e:
|
|
89
|
+
logger.warning(f"[WhatsApp RPC] Connection closed: {e}")
|
|
90
|
+
self._connected = False
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f"[WhatsApp RPC] Receive loop error: {e}")
|
|
93
|
+
self._connected = False
|
|
94
|
+
|
|
95
|
+
async def _handle_event(self, data: dict):
|
|
96
|
+
"""Handle async events from Go service and broadcast to frontend.
|
|
97
|
+
|
|
98
|
+
Events from schema.json:
|
|
99
|
+
- event.connected: {status: "connected", device_id: string}
|
|
100
|
+
- event.disconnected: {status: "disconnected", reason: string}
|
|
101
|
+
- event.connection_failure: {error: string, reason: string}
|
|
102
|
+
- event.logged_out: {on_connect: boolean, reason: string}
|
|
103
|
+
- event.temporary_ban: {code: string, reason: string}
|
|
104
|
+
- event.qr_code: {code: string, filename: string}
|
|
105
|
+
- event.message_sent: {message_id, to, type, timestamp}
|
|
106
|
+
- event.message_received: {message_id, sender, chat_id, ...}
|
|
107
|
+
"""
|
|
108
|
+
method = data.get("method", "")
|
|
109
|
+
params = data.get("params", {})
|
|
110
|
+
logger.debug(f"[WhatsApp RPC] Event: {method}")
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
from services.status_broadcaster import get_status_broadcaster
|
|
114
|
+
broadcaster = get_status_broadcaster()
|
|
115
|
+
|
|
116
|
+
if method == "event.status":
|
|
117
|
+
# Initial status sent on WebSocket connection
|
|
118
|
+
await broadcaster.update_whatsapp_status(
|
|
119
|
+
connected=params.get("connected", False),
|
|
120
|
+
has_session=params.get("has_session", False),
|
|
121
|
+
running=params.get("running", False),
|
|
122
|
+
pairing=params.get("pairing", False),
|
|
123
|
+
device_id=params.get("device_id"),
|
|
124
|
+
qr=None
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
elif method == "event.connected":
|
|
128
|
+
# Connected successfully with device_id
|
|
129
|
+
await broadcaster.update_whatsapp_status(
|
|
130
|
+
connected=True,
|
|
131
|
+
has_session=True,
|
|
132
|
+
running=True,
|
|
133
|
+
pairing=False,
|
|
134
|
+
device_id=params.get("device_id"),
|
|
135
|
+
qr=None
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
elif method == "event.disconnected":
|
|
139
|
+
# Disconnected - service still running
|
|
140
|
+
await broadcaster.update_whatsapp_status(
|
|
141
|
+
connected=False,
|
|
142
|
+
has_session=False,
|
|
143
|
+
running=True,
|
|
144
|
+
pairing=False,
|
|
145
|
+
device_id=None,
|
|
146
|
+
qr=None
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
elif method == "event.connection_failure":
|
|
150
|
+
# Connection failed
|
|
151
|
+
logger.error(f"[WhatsApp] Connection failure: {params.get('error')} - {params.get('reason')}")
|
|
152
|
+
await broadcaster.update_whatsapp_status(
|
|
153
|
+
connected=False,
|
|
154
|
+
has_session=False,
|
|
155
|
+
running=True,
|
|
156
|
+
pairing=False,
|
|
157
|
+
device_id=None,
|
|
158
|
+
qr=None
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
elif method == "event.logged_out":
|
|
162
|
+
# Logged out - session cleared
|
|
163
|
+
logger.warning(f"[WhatsApp] Logged out: {params.get('reason')}")
|
|
164
|
+
await broadcaster.update_whatsapp_status(
|
|
165
|
+
connected=False,
|
|
166
|
+
has_session=False,
|
|
167
|
+
running=True,
|
|
168
|
+
pairing=False,
|
|
169
|
+
device_id=None,
|
|
170
|
+
qr=None
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
elif method == "event.temporary_ban":
|
|
174
|
+
# Temporary ban
|
|
175
|
+
logger.error(f"[WhatsApp] Temporary ban: code={params.get('code')} reason={params.get('reason')}")
|
|
176
|
+
await broadcaster.update_whatsapp_status(
|
|
177
|
+
connected=False,
|
|
178
|
+
has_session=False,
|
|
179
|
+
running=True,
|
|
180
|
+
pairing=False,
|
|
181
|
+
device_id=None,
|
|
182
|
+
qr=None
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
elif method == "event.qr_code":
|
|
186
|
+
# New QR code available for pairing
|
|
187
|
+
code = params.get("code")
|
|
188
|
+
qr_image = qr_code_to_base64(code) if code else None
|
|
189
|
+
await broadcaster.update_whatsapp_status(
|
|
190
|
+
connected=False,
|
|
191
|
+
has_session=False,
|
|
192
|
+
running=True,
|
|
193
|
+
pairing=True,
|
|
194
|
+
device_id=None,
|
|
195
|
+
qr=qr_image
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
elif method == "event.message_sent":
|
|
199
|
+
# Message sent - broadcast as custom event
|
|
200
|
+
await broadcaster.send_custom_event("whatsapp_message_sent", params)
|
|
201
|
+
|
|
202
|
+
elif method == "event.message_received":
|
|
203
|
+
# Message received - broadcast as custom event for trigger nodes
|
|
204
|
+
await broadcaster.send_custom_event("whatsapp_message_received", params)
|
|
205
|
+
|
|
206
|
+
# Forward to custom handler if set
|
|
207
|
+
if self._event_handler:
|
|
208
|
+
await self._event_handler(method, params)
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.error(f"[WhatsApp RPC] Event handler error: {e}")
|
|
212
|
+
|
|
213
|
+
async def call(self, method: str, params: Any = None, timeout: float = 30) -> Any:
|
|
214
|
+
if not self.connected:
|
|
215
|
+
raise Exception("Not connected to WhatsApp service")
|
|
216
|
+
self.req_id += 1
|
|
217
|
+
req_id = self.req_id # Capture request ID before any await
|
|
218
|
+
req = {"jsonrpc": "2.0", "id": req_id, "method": method}
|
|
219
|
+
if params:
|
|
220
|
+
req["params"] = params
|
|
221
|
+
|
|
222
|
+
# Get current event loop for future
|
|
223
|
+
try:
|
|
224
|
+
loop = asyncio.get_running_loop()
|
|
225
|
+
except RuntimeError:
|
|
226
|
+
loop = asyncio.get_event_loop()
|
|
227
|
+
future = loop.create_future()
|
|
228
|
+
self.pending[req_id] = future
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
await self.ws.send(json.dumps(req))
|
|
232
|
+
resp = await asyncio.wait_for(future, timeout)
|
|
233
|
+
if resp.get("error"):
|
|
234
|
+
raise Exception(resp["error"].get("message", "RPC Error"))
|
|
235
|
+
return resp.get("result")
|
|
236
|
+
except asyncio.TimeoutError:
|
|
237
|
+
raise Exception(f"RPC call '{method}' timed out after {timeout}s")
|
|
238
|
+
except ConnectionClosed as e:
|
|
239
|
+
logger.error(f"[WhatsApp RPC] Connection closed during {method}: {e}")
|
|
240
|
+
self._connected = False
|
|
241
|
+
raise Exception(f"Connection lost during {method}")
|
|
242
|
+
finally:
|
|
243
|
+
self.pending.pop(req_id, None)
|
|
244
|
+
|
|
245
|
+
_client: Optional[RPCClient] = None
|
|
246
|
+
_lock = asyncio.Lock()
|
|
247
|
+
_send_lock = asyncio.Lock() # Serialize sends - Go service processes sequentially
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def reset_client():
|
|
251
|
+
"""Force reset the RPC client connection."""
|
|
252
|
+
global _client
|
|
253
|
+
async with _lock:
|
|
254
|
+
if _client:
|
|
255
|
+
try:
|
|
256
|
+
await _client.close()
|
|
257
|
+
except Exception:
|
|
258
|
+
pass
|
|
259
|
+
_client = None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def get_client(force_reconnect: bool = False) -> RPCClient:
|
|
263
|
+
"""Get or create RPC client. Use force_reconnect=True to ensure fresh connection."""
|
|
264
|
+
global _client
|
|
265
|
+
async with _lock:
|
|
266
|
+
# Force reconnect if requested or if client is stale
|
|
267
|
+
if force_reconnect and _client:
|
|
268
|
+
logger.info("[WhatsApp RPC] Force reconnecting...")
|
|
269
|
+
try:
|
|
270
|
+
await _client.close()
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
_client = None
|
|
274
|
+
|
|
275
|
+
if not _client or not _client.connected:
|
|
276
|
+
logger.info(f"[WhatsApp RPC] Creating new connection to {WHATSAPP_RPC_URL}")
|
|
277
|
+
_client = RPCClient(WHATSAPP_RPC_URL)
|
|
278
|
+
try:
|
|
279
|
+
await _client.connect()
|
|
280
|
+
logger.info("[WhatsApp RPC] Connected successfully")
|
|
281
|
+
except asyncio.TimeoutError:
|
|
282
|
+
_client = None
|
|
283
|
+
logger.error(f"WhatsApp RPC timeout - Go service not responding at {WHATSAPP_RPC_URL}")
|
|
284
|
+
raise Exception("WhatsApp service timeout - is Go service running?")
|
|
285
|
+
except (ConnectionRefusedError, OSError) as e:
|
|
286
|
+
_client = None
|
|
287
|
+
logger.error(f"WhatsApp RPC connection refused: {e}")
|
|
288
|
+
raise Exception("WhatsApp service not running - start Go whatsmeow service on port 9400")
|
|
289
|
+
except Exception as e:
|
|
290
|
+
_client = None
|
|
291
|
+
logger.error(f"WhatsApp RPC error: {e}")
|
|
292
|
+
raise Exception(f"WhatsApp connection failed: {e}")
|
|
293
|
+
return _client
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ============================================================================
|
|
297
|
+
# WebSocket Handlers - used by websocket.py
|
|
298
|
+
# ============================================================================
|
|
299
|
+
|
|
300
|
+
async def handle_whatsapp_status() -> dict:
|
|
301
|
+
"""Get WhatsApp connection status via direct RPC and broadcast to all clients."""
|
|
302
|
+
try:
|
|
303
|
+
client = await get_client()
|
|
304
|
+
status_data = await client.call("status")
|
|
305
|
+
|
|
306
|
+
# Broadcast status update to all connected WebSocket clients
|
|
307
|
+
from services.status_broadcaster import get_status_broadcaster
|
|
308
|
+
broadcaster = get_status_broadcaster()
|
|
309
|
+
await broadcaster.update_whatsapp_status(
|
|
310
|
+
connected=status_data.get("connected", False),
|
|
311
|
+
has_session=status_data.get("has_session", False),
|
|
312
|
+
running=status_data.get("running", False),
|
|
313
|
+
pairing=status_data.get("pairing", False),
|
|
314
|
+
device_id=status_data.get("device_id"),
|
|
315
|
+
qr=None # QR code comes from event.qr_code events
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
"success": True,
|
|
320
|
+
"data": status_data,
|
|
321
|
+
"connected": status_data.get("connected", False),
|
|
322
|
+
"device_id": status_data.get("device_id"),
|
|
323
|
+
"timestamp": time.time()
|
|
324
|
+
}
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.error(f"WhatsApp status check failed: {e}")
|
|
327
|
+
# Return error response immediately - don't broadcast here to avoid race conditions
|
|
328
|
+
# The client will update its local state based on the error response
|
|
329
|
+
return {
|
|
330
|
+
"success": False,
|
|
331
|
+
"error": str(e),
|
|
332
|
+
"connected": False,
|
|
333
|
+
"running": False,
|
|
334
|
+
"timestamp": time.time()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def handle_whatsapp_qr() -> dict:
|
|
339
|
+
"""Get WhatsApp QR code for authentication via direct RPC."""
|
|
340
|
+
try:
|
|
341
|
+
client = await get_client()
|
|
342
|
+
status = await client.call("status")
|
|
343
|
+
|
|
344
|
+
if status.get("connected") and status.get("has_session"):
|
|
345
|
+
return {
|
|
346
|
+
"success": True,
|
|
347
|
+
"connected": True,
|
|
348
|
+
"message": "Already connected with active session",
|
|
349
|
+
"timestamp": time.time()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
result = await client.call("qr")
|
|
354
|
+
code = result.get("code")
|
|
355
|
+
if code:
|
|
356
|
+
qr_image = qr_code_to_base64(code)
|
|
357
|
+
return {
|
|
358
|
+
"success": True,
|
|
359
|
+
"connected": False,
|
|
360
|
+
"qr": qr_image,
|
|
361
|
+
"message": "QR code available",
|
|
362
|
+
"timestamp": time.time()
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
"success": True,
|
|
366
|
+
"connected": False,
|
|
367
|
+
"qr": None,
|
|
368
|
+
"message": "No QR code available",
|
|
369
|
+
"timestamp": time.time()
|
|
370
|
+
}
|
|
371
|
+
except Exception as qr_err:
|
|
372
|
+
return {
|
|
373
|
+
"success": True,
|
|
374
|
+
"connected": False,
|
|
375
|
+
"qr": None,
|
|
376
|
+
"message": str(qr_err),
|
|
377
|
+
"timestamp": time.time()
|
|
378
|
+
}
|
|
379
|
+
except Exception as e:
|
|
380
|
+
logger.error(f"WhatsApp QR fetch failed: {e}")
|
|
381
|
+
return {"success": False, "connected": False, "error": str(e)}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
async def handle_whatsapp_send(params: dict) -> dict:
|
|
385
|
+
"""Send a WhatsApp message via direct RPC - supports all message types.
|
|
386
|
+
|
|
387
|
+
Uses _send_lock to serialize sends - Go service processes sequentially.
|
|
388
|
+
|
|
389
|
+
Params from frontend node (snake_case):
|
|
390
|
+
- recipient_type: 'phone' or 'group'
|
|
391
|
+
- phone: recipient phone number (if recipient_type='phone')
|
|
392
|
+
- group_id: group JID (if recipient_type='group')
|
|
393
|
+
- message_type: text, image, video, audio, document, sticker, location, contact
|
|
394
|
+
- message: text content (for text type)
|
|
395
|
+
- media_source: base64, file, url (for media types)
|
|
396
|
+
- media_data/file_path/media_url: media content based on source
|
|
397
|
+
- mime_type, caption, filename: media options
|
|
398
|
+
- latitude, longitude, location_name, address: location data
|
|
399
|
+
- contact_name, vcard: contact data
|
|
400
|
+
- is_reply, reply_message_id, reply_sender, reply_content: reply context
|
|
401
|
+
"""
|
|
402
|
+
async with _send_lock:
|
|
403
|
+
try:
|
|
404
|
+
# Build RPC params matching schema.json
|
|
405
|
+
rpc_params: dict[str, Any] = {}
|
|
406
|
+
|
|
407
|
+
# Recipient (snake_case)
|
|
408
|
+
recipient_type = params.get("recipient_type", "phone")
|
|
409
|
+
if recipient_type == "group":
|
|
410
|
+
group_id = params.get("group_id")
|
|
411
|
+
if not group_id:
|
|
412
|
+
return {"success": False, "error": "group_id is required"}
|
|
413
|
+
rpc_params["group_id"] = group_id
|
|
414
|
+
else:
|
|
415
|
+
phone = params.get("phone")
|
|
416
|
+
if not phone:
|
|
417
|
+
return {"success": False, "error": "phone is required"}
|
|
418
|
+
rpc_params["phone"] = phone
|
|
419
|
+
|
|
420
|
+
# Message type (snake_case)
|
|
421
|
+
msg_type = params.get("message_type", "text")
|
|
422
|
+
rpc_params["type"] = msg_type
|
|
423
|
+
|
|
424
|
+
# Content based on type
|
|
425
|
+
if msg_type == "text":
|
|
426
|
+
message = params.get("message")
|
|
427
|
+
if not message:
|
|
428
|
+
return {"success": False, "error": "message is required for text type"}
|
|
429
|
+
rpc_params["message"] = message
|
|
430
|
+
|
|
431
|
+
elif msg_type in ["image", "video", "audio", "document", "sticker"]:
|
|
432
|
+
media_source = params.get("media_source", "base64")
|
|
433
|
+
media_data = None
|
|
434
|
+
mime_type = params.get("mime_type")
|
|
435
|
+
filename = params.get("filename")
|
|
436
|
+
|
|
437
|
+
if media_source == "base64":
|
|
438
|
+
media_data = params.get("media_data")
|
|
439
|
+
elif media_source == "file":
|
|
440
|
+
file_param = params.get("file_path")
|
|
441
|
+
if isinstance(file_param, dict) and file_param.get("type") == "upload":
|
|
442
|
+
media_data = file_param.get("data")
|
|
443
|
+
mime_type = mime_type or file_param.get("mimeType")
|
|
444
|
+
filename = filename or file_param.get("filename")
|
|
445
|
+
elif file_param:
|
|
446
|
+
import base64 as b64
|
|
447
|
+
try:
|
|
448
|
+
with open(file_param, "rb") as f:
|
|
449
|
+
media_data = b64.b64encode(f.read()).decode("utf-8")
|
|
450
|
+
except Exception as e:
|
|
451
|
+
return {"success": False, "error": f"Failed to read file: {e}"}
|
|
452
|
+
elif media_source == "url":
|
|
453
|
+
media_url = params.get("media_url")
|
|
454
|
+
if media_url:
|
|
455
|
+
import httpx
|
|
456
|
+
import base64 as b64
|
|
457
|
+
try:
|
|
458
|
+
async with httpx.AsyncClient() as http:
|
|
459
|
+
resp = await http.get(media_url, timeout=30)
|
|
460
|
+
media_data = b64.b64encode(resp.content).decode("utf-8")
|
|
461
|
+
except Exception as e:
|
|
462
|
+
return {"success": False, "error": f"Failed to download media: {e}"}
|
|
463
|
+
|
|
464
|
+
if not media_data:
|
|
465
|
+
return {"success": False, "error": f"media data is required for {msg_type} type"}
|
|
466
|
+
|
|
467
|
+
rpc_params["media_data"] = {
|
|
468
|
+
"data": media_data,
|
|
469
|
+
"mime_type": mime_type or _guess_mime_type(msg_type)
|
|
470
|
+
}
|
|
471
|
+
if params.get("caption"):
|
|
472
|
+
rpc_params["media_data"]["caption"] = params["caption"]
|
|
473
|
+
final_filename = filename or params.get("filename")
|
|
474
|
+
if final_filename:
|
|
475
|
+
rpc_params["media_data"]["filename"] = final_filename
|
|
476
|
+
|
|
477
|
+
elif msg_type == "location":
|
|
478
|
+
lat = params.get("latitude")
|
|
479
|
+
lng = params.get("longitude")
|
|
480
|
+
if lat is None or lng is None:
|
|
481
|
+
return {"success": False, "error": "latitude and longitude are required"}
|
|
482
|
+
rpc_params["location"] = {"latitude": float(lat), "longitude": float(lng)}
|
|
483
|
+
if params.get("location_name"):
|
|
484
|
+
rpc_params["location"]["name"] = params["location_name"]
|
|
485
|
+
if params.get("address"):
|
|
486
|
+
rpc_params["location"]["address"] = params["address"]
|
|
487
|
+
|
|
488
|
+
elif msg_type == "contact":
|
|
489
|
+
contact_name = params.get("contact_name")
|
|
490
|
+
vcard = params.get("vcard")
|
|
491
|
+
if not contact_name or not vcard:
|
|
492
|
+
return {"success": False, "error": "contact_name and vcard are required"}
|
|
493
|
+
rpc_params["contact"] = {"display_name": contact_name, "vcard": vcard}
|
|
494
|
+
|
|
495
|
+
# Reply context (snake_case)
|
|
496
|
+
if params.get("is_reply"):
|
|
497
|
+
reply_id = params.get("reply_message_id")
|
|
498
|
+
reply_sender = params.get("reply_sender")
|
|
499
|
+
if reply_id and reply_sender:
|
|
500
|
+
rpc_params["reply"] = {
|
|
501
|
+
"message_id": reply_id,
|
|
502
|
+
"sender": reply_sender,
|
|
503
|
+
"content": params.get("reply_content", "")
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if params.get("metadata"):
|
|
507
|
+
rpc_params["metadata"] = params["metadata"]
|
|
508
|
+
|
|
509
|
+
client = await get_client()
|
|
510
|
+
result = await client.call("send", rpc_params)
|
|
511
|
+
return {
|
|
512
|
+
"success": True,
|
|
513
|
+
"message_id": result.get("message_id"),
|
|
514
|
+
"message_type": msg_type,
|
|
515
|
+
"timestamp": time.time()
|
|
516
|
+
}
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.error(f"WhatsApp send failed: {e}")
|
|
519
|
+
return {"success": False, "error": str(e)}
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _guess_mime_type(msg_type: str) -> str:
|
|
523
|
+
"""Guess default MIME type based on message type."""
|
|
524
|
+
defaults = {
|
|
525
|
+
"image": "image/jpeg",
|
|
526
|
+
"video": "video/mp4",
|
|
527
|
+
"audio": "audio/ogg",
|
|
528
|
+
"document": "application/octet-stream",
|
|
529
|
+
"sticker": "image/webp"
|
|
530
|
+
}
|
|
531
|
+
return defaults.get(msg_type, "application/octet-stream")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
async def handle_whatsapp_start() -> dict:
|
|
535
|
+
"""Start WhatsApp connection via direct RPC and broadcast running state."""
|
|
536
|
+
try:
|
|
537
|
+
client = await get_client()
|
|
538
|
+
result = await client.call("start")
|
|
539
|
+
|
|
540
|
+
# Broadcast that service is now running (waiting for QR or connection)
|
|
541
|
+
from services.status_broadcaster import get_status_broadcaster
|
|
542
|
+
broadcaster = get_status_broadcaster()
|
|
543
|
+
await broadcaster.update_whatsapp_status(
|
|
544
|
+
connected=False,
|
|
545
|
+
has_session=False,
|
|
546
|
+
running=True,
|
|
547
|
+
pairing=False, # Will be set to True by event.qr_code event
|
|
548
|
+
device_id=None,
|
|
549
|
+
qr=None
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
"success": True,
|
|
554
|
+
"message": "WhatsApp connection started",
|
|
555
|
+
"data": result,
|
|
556
|
+
"timestamp": time.time()
|
|
557
|
+
}
|
|
558
|
+
except Exception as e:
|
|
559
|
+
logger.error(f"WhatsApp start failed: {e}")
|
|
560
|
+
return {"success": False, "error": str(e)}
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
async def handle_whatsapp_restart() -> dict:
|
|
564
|
+
"""Restart WhatsApp connection via direct RPC.
|
|
565
|
+
|
|
566
|
+
This calls the 'restart' RPC method which stops and starts the service,
|
|
567
|
+
unlike 'start' which only starts if not running.
|
|
568
|
+
"""
|
|
569
|
+
try:
|
|
570
|
+
# Force fresh connection to avoid stale WebSocket
|
|
571
|
+
client = await get_client(force_reconnect=True)
|
|
572
|
+
|
|
573
|
+
# Broadcast that we're restarting (brief disconnected state)
|
|
574
|
+
from services.status_broadcaster import get_status_broadcaster
|
|
575
|
+
broadcaster = get_status_broadcaster()
|
|
576
|
+
await broadcaster.update_whatsapp_status(
|
|
577
|
+
connected=False,
|
|
578
|
+
has_session=False,
|
|
579
|
+
running=True,
|
|
580
|
+
pairing=False,
|
|
581
|
+
device_id=None,
|
|
582
|
+
qr=None
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# Call restart RPC method
|
|
586
|
+
result = await client.call("restart")
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
"success": True,
|
|
590
|
+
"message": "WhatsApp connection restarted",
|
|
591
|
+
"data": result,
|
|
592
|
+
"timestamp": time.time()
|
|
593
|
+
}
|
|
594
|
+
except HTTPException as e:
|
|
595
|
+
logger.error(f"WhatsApp restart failed: {e.detail}")
|
|
596
|
+
return {"success": False, "error": e.detail}
|
|
597
|
+
except Exception as e:
|
|
598
|
+
logger.error(f"WhatsApp restart failed: {e}")
|
|
599
|
+
return {"success": False, "error": str(e)}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
async def handle_whatsapp_groups() -> dict:
|
|
603
|
+
"""Get list of WhatsApp groups via direct RPC."""
|
|
604
|
+
try:
|
|
605
|
+
client = await get_client()
|
|
606
|
+
groups = await client.call("groups")
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
"success": True,
|
|
610
|
+
"groups": groups or [],
|
|
611
|
+
"timestamp": time.time()
|
|
612
|
+
}
|
|
613
|
+
except Exception as e:
|
|
614
|
+
logger.error(f"WhatsApp groups fetch failed: {e}")
|
|
615
|
+
return {"success": False, "error": str(e), "groups": []}
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
async def handle_whatsapp_group_info(group_id: str) -> dict:
|
|
619
|
+
"""Get group info including participants with resolved phone numbers.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
group_id: Group JID (e.g., '120363422738675920@g.us')
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
Group info with participants containing both 'jid' (LID) and 'phone' (resolved number)
|
|
626
|
+
"""
|
|
627
|
+
try:
|
|
628
|
+
if not group_id:
|
|
629
|
+
return {"success": False, "error": "group_id is required", "participants": []}
|
|
630
|
+
|
|
631
|
+
client = await get_client()
|
|
632
|
+
result = await client.call("group_info", {"group_id": group_id})
|
|
633
|
+
|
|
634
|
+
if not result:
|
|
635
|
+
return {"success": False, "error": "Failed to get group info", "participants": []}
|
|
636
|
+
|
|
637
|
+
# Extract participants with phone numbers
|
|
638
|
+
participants = []
|
|
639
|
+
for p in result.get('participants', []):
|
|
640
|
+
jid = p.get('jid', '')
|
|
641
|
+
phone = p.get('phone', '')
|
|
642
|
+
name = p.get('name', '')
|
|
643
|
+
|
|
644
|
+
# Only include participants with resolved phone numbers
|
|
645
|
+
if phone:
|
|
646
|
+
participants.append({
|
|
647
|
+
"jid": jid,
|
|
648
|
+
"phone": phone,
|
|
649
|
+
"name": name or phone, # Use phone as fallback name
|
|
650
|
+
"is_admin": p.get('is_admin', False),
|
|
651
|
+
"is_super_admin": p.get('is_super_admin', False)
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
"success": True,
|
|
656
|
+
"group_id": group_id,
|
|
657
|
+
"name": result.get('name', ''),
|
|
658
|
+
"participants": participants,
|
|
659
|
+
"participant_count": len(participants),
|
|
660
|
+
"timestamp": time.time()
|
|
661
|
+
}
|
|
662
|
+
except Exception as e:
|
|
663
|
+
logger.error(f"WhatsApp group_info fetch failed for {group_id}: {e}")
|
|
664
|
+
return {"success": False, "error": str(e), "participants": []}
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
async def handle_whatsapp_chat_history(params: dict) -> dict:
|
|
668
|
+
"""Get chat history from WhatsApp via direct RPC.
|
|
669
|
+
|
|
670
|
+
Retrieves stored messages from the Go service's history store.
|
|
671
|
+
Messages are automatically stored from HistorySync (on first login)
|
|
672
|
+
and from real-time incoming messages.
|
|
673
|
+
|
|
674
|
+
Params:
|
|
675
|
+
- chat_id: Direct chat JID (e.g., '919876543210@s.whatsapp.net')
|
|
676
|
+
- phone: Phone number (alternative to chat_id, will be converted)
|
|
677
|
+
- group_id: Group JID (alternative for group chats)
|
|
678
|
+
- limit: Max messages to return (default 50, max 500)
|
|
679
|
+
- offset: Pagination offset (default 0)
|
|
680
|
+
- sender_phone: Filter by sender phone in group chats
|
|
681
|
+
- text_only: Only return text messages (default false)
|
|
682
|
+
|
|
683
|
+
Returns:
|
|
684
|
+
- messages: Array of MessageRecord
|
|
685
|
+
- total: Total matching messages count
|
|
686
|
+
- has_more: Whether more messages exist
|
|
687
|
+
"""
|
|
688
|
+
try:
|
|
689
|
+
client = await get_client()
|
|
690
|
+
|
|
691
|
+
# Build RPC params
|
|
692
|
+
rpc_params = {}
|
|
693
|
+
|
|
694
|
+
# Determine chat_id from various inputs
|
|
695
|
+
chat_id = params.get("chat_id")
|
|
696
|
+
phone = params.get("phone")
|
|
697
|
+
group_id = params.get("group_id")
|
|
698
|
+
|
|
699
|
+
if chat_id:
|
|
700
|
+
rpc_params["chat_id"] = chat_id
|
|
701
|
+
elif phone:
|
|
702
|
+
rpc_params["phone"] = phone
|
|
703
|
+
elif group_id:
|
|
704
|
+
rpc_params["group_id"] = group_id
|
|
705
|
+
else:
|
|
706
|
+
return {"success": False, "error": "Either chat_id, phone, or group_id is required"}
|
|
707
|
+
|
|
708
|
+
# Optional filters
|
|
709
|
+
limit = params.get("limit", 50)
|
|
710
|
+
if limit > 500:
|
|
711
|
+
limit = 500
|
|
712
|
+
rpc_params["limit"] = limit
|
|
713
|
+
|
|
714
|
+
offset = params.get("offset", 0)
|
|
715
|
+
rpc_params["offset"] = offset
|
|
716
|
+
|
|
717
|
+
sender_phone = params.get("sender_phone")
|
|
718
|
+
if sender_phone:
|
|
719
|
+
rpc_params["sender_phone"] = sender_phone
|
|
720
|
+
|
|
721
|
+
text_only = params.get("text_only", False)
|
|
722
|
+
rpc_params["text_only"] = text_only
|
|
723
|
+
|
|
724
|
+
result = await client.call("chat_history", rpc_params)
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
"success": True,
|
|
728
|
+
"messages": result.get("messages", []),
|
|
729
|
+
"total": result.get("total", 0),
|
|
730
|
+
"has_more": result.get("has_more", False),
|
|
731
|
+
"timestamp": time.time()
|
|
732
|
+
}
|
|
733
|
+
except Exception as e:
|
|
734
|
+
logger.error(f"WhatsApp chat_history fetch failed: {e}")
|
|
735
|
+
return {"success": False, "error": str(e), "messages": [], "total": 0, "has_more": False}
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
async def whatsapp_rpc_call(method: str, params: dict = None) -> dict:
|
|
739
|
+
"""Generic RPC call to WhatsApp Go service.
|
|
740
|
+
|
|
741
|
+
Used by handlers/whatsapp.py for operations like:
|
|
742
|
+
- groups: List all groups
|
|
743
|
+
- group_info: Get group details with participants
|
|
744
|
+
- contacts: List contacts with saved names
|
|
745
|
+
- contact_info: Get full contact info (for send/reply)
|
|
746
|
+
- contact_check: Check WhatsApp registration status
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
method: RPC method name (e.g., 'groups', 'contact_info')
|
|
750
|
+
params: Method parameters dict
|
|
751
|
+
|
|
752
|
+
Returns:
|
|
753
|
+
RPC result dict or error dict
|
|
754
|
+
"""
|
|
755
|
+
try:
|
|
756
|
+
client = await get_client()
|
|
757
|
+
result = await client.call(method, params or {})
|
|
758
|
+
return result if isinstance(result, dict) else {"result": result, "success": True}
|
|
759
|
+
except Exception as e:
|
|
760
|
+
logger.error(f"WhatsApp RPC call '{method}' failed: {e}")
|
|
761
|
+
return {"success": False, "error": str(e)}
|