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,785 +1,675 @@
|
|
|
1
|
-
"""Event Waiter Service - Generic event waiting for trigger nodes.
|
|
2
|
-
|
|
3
|
-
Supports any trigger type (WhatsApp, Email, Webhook, MQTT, etc.)
|
|
4
|
-
Uses Redis Streams when available for persistence, falls back to asyncio.Future.
|
|
5
|
-
|
|
6
|
-
Architecture:
|
|
7
|
-
- Redis mode: Events stored in Redis Streams, waiters poll streams with blocking XREAD
|
|
8
|
-
- Memory mode: Events dispatched to in-memory asyncio.Future waiters
|
|
9
|
-
"""
|
|
10
|
-
import asyncio
|
|
11
|
-
import json
|
|
12
|
-
import uuid
|
|
13
|
-
import time
|
|
14
|
-
from dataclasses import dataclass, field
|
|
15
|
-
from typing import Dict, Any, Optional, Callable, List, TYPE_CHECKING
|
|
16
|
-
|
|
17
|
-
from core.logging import get_logger
|
|
18
|
-
|
|
19
|
-
if TYPE_CHECKING:
|
|
20
|
-
from core.cache import CacheService
|
|
21
|
-
|
|
22
|
-
logger = get_logger(__name__)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# =============================================================================
|
|
26
|
-
# CACHE SERVICE REFERENCE
|
|
27
|
-
# =============================================================================
|
|
28
|
-
|
|
29
|
-
_cache_service: Optional["CacheService"] = None
|
|
30
|
-
_main_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def set_cache_service(cache: "CacheService") -> None:
|
|
34
|
-
"""Set the cache service for Redis Streams support.
|
|
35
|
-
|
|
36
|
-
Called during application startup from main.py.
|
|
37
|
-
"""
|
|
38
|
-
global _cache_service, _main_loop
|
|
39
|
-
_cache_service = cache
|
|
40
|
-
# Store reference to the main event loop for thread-safe dispatch
|
|
41
|
-
try:
|
|
42
|
-
_main_loop = asyncio.get_running_loop()
|
|
43
|
-
except RuntimeError:
|
|
44
|
-
_main_loop = None
|
|
45
|
-
mode = "Redis Streams" if cache and cache.is_redis_available() else "asyncio.Future"
|
|
46
|
-
logger.info(f"[EventWaiter] Initialized with {mode} backend")
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def get_cache_service() -> Optional["CacheService"]:
|
|
50
|
-
"""Get the cache service if available."""
|
|
51
|
-
return _cache_service
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def is_redis_mode() -> bool:
|
|
55
|
-
"""Check if Redis Streams mode is active.
|
|
56
|
-
|
|
57
|
-
Returns True only if Redis is connected AND supports Streams commands.
|
|
58
|
-
This prevents runtime failures when Redis doesn't support XREADGROUP/XADD.
|
|
59
|
-
"""
|
|
60
|
-
return _cache_service is not None and _cache_service.is_streams_available()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# =============================================================================
|
|
64
|
-
# LID TO PHONE RESOLUTION
|
|
65
|
-
# =============================================================================
|
|
66
|
-
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"""
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
Args:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
#
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
""
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
"""
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
# =============================================================================
|
|
636
|
-
#
|
|
637
|
-
# =============================================================================
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
"""
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
"""
|
|
676
|
-
if is_redis_mode():
|
|
677
|
-
# In Redis mode, use async dispatch
|
|
678
|
-
# Handle both async context and thread context (e.g., APScheduler callbacks)
|
|
679
|
-
try:
|
|
680
|
-
# Try to get the current running loop
|
|
681
|
-
loop = asyncio.get_running_loop()
|
|
682
|
-
# We're in an async context - schedule task normally
|
|
683
|
-
asyncio.create_task(dispatch_async(event_type, data))
|
|
684
|
-
except RuntimeError:
|
|
685
|
-
# No running loop - we're in a thread (e.g., APScheduler callback)
|
|
686
|
-
# Use the stored main loop with thread-safe dispatch
|
|
687
|
-
if _main_loop is not None and _main_loop.is_running():
|
|
688
|
-
asyncio.run_coroutine_threadsafe(dispatch_async(event_type, data), _main_loop)
|
|
689
|
-
else:
|
|
690
|
-
logger.warning(f"[EventWaiter] No event loop available for dispatch of {event_type}")
|
|
691
|
-
return 0 # Actual resolution happens in _wait_redis
|
|
692
|
-
|
|
693
|
-
resolved = 0
|
|
694
|
-
to_remove = []
|
|
695
|
-
|
|
696
|
-
for wid, w in _waiters.items():
|
|
697
|
-
if w.event_type == event_type and w.future and not w.future.done():
|
|
698
|
-
try:
|
|
699
|
-
if w.filter_fn(data):
|
|
700
|
-
w.future.set_result(data)
|
|
701
|
-
to_remove.append(wid)
|
|
702
|
-
resolved += 1
|
|
703
|
-
logger.debug(f"[EventWaiter] Resolved {w.node_type} waiter {wid}")
|
|
704
|
-
except Exception as e:
|
|
705
|
-
logger.error(f"[EventWaiter] Filter error for waiter {wid}: {e}")
|
|
706
|
-
|
|
707
|
-
for wid in to_remove:
|
|
708
|
-
_waiters.pop(wid, None)
|
|
709
|
-
|
|
710
|
-
return resolved
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
# =============================================================================
|
|
714
|
-
# WAITER CANCELLATION
|
|
715
|
-
# =============================================================================
|
|
716
|
-
|
|
717
|
-
def cancel(waiter_id: str) -> bool:
|
|
718
|
-
"""Cancel a waiter by ID."""
|
|
719
|
-
if w := _waiters.pop(waiter_id, None):
|
|
720
|
-
w.cancelled = True
|
|
721
|
-
|
|
722
|
-
if w.future and not w.future.done():
|
|
723
|
-
w.future.cancel()
|
|
724
|
-
|
|
725
|
-
# Also remove from Redis if in Redis mode
|
|
726
|
-
if is_redis_mode():
|
|
727
|
-
cache = get_cache_service()
|
|
728
|
-
waiter_key = f"{WAITERS_KEY_PREFIX}{waiter_id}"
|
|
729
|
-
asyncio.create_task(cache.delete(waiter_key))
|
|
730
|
-
|
|
731
|
-
logger.debug(f"[EventWaiter] Cancelled waiter {waiter_id}")
|
|
732
|
-
return True
|
|
733
|
-
|
|
734
|
-
return False
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
def cancel_for_node(node_id: str) -> int:
|
|
738
|
-
"""Cancel all waiters for a node."""
|
|
739
|
-
to_cancel = [wid for wid, w in _waiters.items() if w.node_id == node_id]
|
|
740
|
-
for wid in to_cancel:
|
|
741
|
-
cancel(wid)
|
|
742
|
-
return len(to_cancel)
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
# =============================================================================
|
|
746
|
-
# UTILITY FUNCTIONS
|
|
747
|
-
# =============================================================================
|
|
748
|
-
|
|
749
|
-
def get_active_waiters() -> List[Dict[str, Any]]:
|
|
750
|
-
"""Get info about active waiters (for debugging/UI)."""
|
|
751
|
-
return [
|
|
752
|
-
{
|
|
753
|
-
"id": w.id,
|
|
754
|
-
"node_id": w.node_id,
|
|
755
|
-
"node_type": w.node_type,
|
|
756
|
-
"event_type": w.event_type,
|
|
757
|
-
"done": w.future.done() if w.future else False,
|
|
758
|
-
"cancelled": w.cancelled,
|
|
759
|
-
"age_seconds": time.time() - w.created_at,
|
|
760
|
-
"mode": "redis" if is_redis_mode() else "memory",
|
|
761
|
-
}
|
|
762
|
-
for w in _waiters.values()
|
|
763
|
-
]
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
def clear_all() -> int:
|
|
767
|
-
"""Clear all waiters (for testing/cleanup)."""
|
|
768
|
-
count = len(_waiters)
|
|
769
|
-
for w in _waiters.values():
|
|
770
|
-
w.cancelled = True
|
|
771
|
-
if w.future and not w.future.done():
|
|
772
|
-
w.future.cancel()
|
|
773
|
-
_waiters.clear()
|
|
774
|
-
|
|
775
|
-
# Clear Redis waiter keys if in Redis mode
|
|
776
|
-
if is_redis_mode():
|
|
777
|
-
cache = get_cache_service()
|
|
778
|
-
asyncio.create_task(cache.clear_pattern(f"{WAITERS_KEY_PREFIX}*"))
|
|
779
|
-
|
|
780
|
-
return count
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
def get_backend_mode() -> str:
|
|
784
|
-
"""Get current backend mode for debugging."""
|
|
785
|
-
return "redis" if is_redis_mode() else "memory"
|
|
1
|
+
"""Event Waiter Service - Generic event waiting for trigger nodes.
|
|
2
|
+
|
|
3
|
+
Supports any trigger type (WhatsApp, Email, Webhook, MQTT, etc.)
|
|
4
|
+
Uses Redis Streams when available for persistence, falls back to asyncio.Future.
|
|
5
|
+
|
|
6
|
+
Architecture:
|
|
7
|
+
- Redis mode: Events stored in Redis Streams, waiters poll streams with blocking XREAD
|
|
8
|
+
- Memory mode: Events dispatched to in-memory asyncio.Future waiters
|
|
9
|
+
"""
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import uuid
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Dict, Any, Optional, Callable, List, TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from core.logging import get_logger
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from core.cache import CacheService
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =============================================================================
|
|
26
|
+
# CACHE SERVICE REFERENCE
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
_cache_service: Optional["CacheService"] = None
|
|
30
|
+
_main_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def set_cache_service(cache: "CacheService") -> None:
|
|
34
|
+
"""Set the cache service for Redis Streams support.
|
|
35
|
+
|
|
36
|
+
Called during application startup from main.py.
|
|
37
|
+
"""
|
|
38
|
+
global _cache_service, _main_loop
|
|
39
|
+
_cache_service = cache
|
|
40
|
+
# Store reference to the main event loop for thread-safe dispatch
|
|
41
|
+
try:
|
|
42
|
+
_main_loop = asyncio.get_running_loop()
|
|
43
|
+
except RuntimeError:
|
|
44
|
+
_main_loop = None
|
|
45
|
+
mode = "Redis Streams" if cache and cache.is_redis_available() else "asyncio.Future"
|
|
46
|
+
logger.info(f"[EventWaiter] Initialized with {mode} backend")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_cache_service() -> Optional["CacheService"]:
|
|
50
|
+
"""Get the cache service if available."""
|
|
51
|
+
return _cache_service
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_redis_mode() -> bool:
|
|
55
|
+
"""Check if Redis Streams mode is active.
|
|
56
|
+
|
|
57
|
+
Returns True only if Redis is connected AND supports Streams commands.
|
|
58
|
+
This prevents runtime failures when Redis doesn't support XREADGROUP/XADD.
|
|
59
|
+
"""
|
|
60
|
+
return _cache_service is not None and _cache_service.is_streams_available()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# =============================================================================
|
|
64
|
+
# NOTE: LID TO PHONE RESOLUTION
|
|
65
|
+
# =============================================================================
|
|
66
|
+
# LID (Linked ID) resolution is now handled by the Go WhatsApp RPC (service.go).
|
|
67
|
+
# The Go RPC resolves LIDs to phone numbers before sending the message_received event.
|
|
68
|
+
# The sender_phone field in the event data contains the already-resolved phone number.
|
|
69
|
+
# No Python-side LID cache is needed anymore.
|
|
70
|
+
# =============================================================================
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# TRIGGER CONFIGURATION REGISTRY
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class TriggerConfig:
|
|
79
|
+
"""Configuration for a trigger node type."""
|
|
80
|
+
node_type: str
|
|
81
|
+
event_type: str # Event to wait for (e.g., 'whatsapp_message_received')
|
|
82
|
+
display_name: str
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Registry of supported trigger types (event-based triggers only)
|
|
86
|
+
# Note: cronScheduler is NOT an event-based trigger - it uses APScheduler directly
|
|
87
|
+
TRIGGER_REGISTRY: Dict[str, TriggerConfig] = {
|
|
88
|
+
'start': TriggerConfig(
|
|
89
|
+
node_type='start',
|
|
90
|
+
event_type='deploy_triggered',
|
|
91
|
+
display_name='Deploy Start'
|
|
92
|
+
),
|
|
93
|
+
'whatsappReceive': TriggerConfig(
|
|
94
|
+
node_type='whatsappReceive',
|
|
95
|
+
event_type='whatsapp_message_received',
|
|
96
|
+
display_name='WhatsApp Message'
|
|
97
|
+
),
|
|
98
|
+
'webhookTrigger': TriggerConfig(
|
|
99
|
+
node_type='webhookTrigger',
|
|
100
|
+
event_type='webhook_received',
|
|
101
|
+
display_name='Webhook Request'
|
|
102
|
+
),
|
|
103
|
+
'chatTrigger': TriggerConfig(
|
|
104
|
+
node_type='chatTrigger',
|
|
105
|
+
event_type='chat_message_received',
|
|
106
|
+
display_name='Chat Message'
|
|
107
|
+
),
|
|
108
|
+
# Future triggers - just add to registry:
|
|
109
|
+
# 'emailTrigger': TriggerConfig('emailTrigger', 'email_received', 'Email'),
|
|
110
|
+
# 'mqttTrigger': TriggerConfig('mqttTrigger', 'mqtt_message', 'MQTT Message'),
|
|
111
|
+
# 'telegramTrigger': TriggerConfig('telegramTrigger', 'telegram_message', 'Telegram'),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_trigger_node(node_type: str) -> bool:
|
|
116
|
+
"""Check if a node type is a trigger node (workflow starting point).
|
|
117
|
+
|
|
118
|
+
Uses constants.WORKFLOW_TRIGGER_TYPES for comprehensive trigger detection.
|
|
119
|
+
This includes all trigger types: start, cronScheduler, and event-based triggers.
|
|
120
|
+
"""
|
|
121
|
+
from constants import WORKFLOW_TRIGGER_TYPES
|
|
122
|
+
return node_type in WORKFLOW_TRIGGER_TYPES
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def is_event_trigger_node(node_type: str) -> bool:
|
|
126
|
+
"""Check if a node type is an event-based trigger (waits for events).
|
|
127
|
+
|
|
128
|
+
Event-based triggers are registered in TRIGGER_REGISTRY and wait for
|
|
129
|
+
external events to fire. This excludes 'start' and 'cronScheduler' which
|
|
130
|
+
have their own execution mechanisms.
|
|
131
|
+
"""
|
|
132
|
+
return node_type in TRIGGER_REGISTRY
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_trigger_config(node_type: str) -> Optional[TriggerConfig]:
|
|
136
|
+
"""Get trigger configuration for a node type."""
|
|
137
|
+
return TRIGGER_REGISTRY.get(node_type)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# =============================================================================
|
|
141
|
+
# FILTER BUILDERS - One per trigger type
|
|
142
|
+
# =============================================================================
|
|
143
|
+
|
|
144
|
+
def build_whatsapp_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
145
|
+
"""Build filter function for WhatsApp messages.
|
|
146
|
+
|
|
147
|
+
Based on Go RPC handleIncomingMessage() event fields (service.go):
|
|
148
|
+
- message_id: string - unique message ID
|
|
149
|
+
- sender: string - Sender JID (may be LID for groups)
|
|
150
|
+
- sender_phone: string - RESOLVED phone number (LID already resolved by Go RPC!)
|
|
151
|
+
- chat_id: string - Chat JID (same as sender for DMs, group JID for groups)
|
|
152
|
+
- timestamp: time - message timestamp
|
|
153
|
+
- is_from_me: boolean - true if sent by connected account
|
|
154
|
+
- is_group: boolean - true if message is in a group chat
|
|
155
|
+
- message_type: string - text, image, video, audio, document, sticker, location, contact, contacts
|
|
156
|
+
- text: string - text content (for text messages)
|
|
157
|
+
- is_forwarded: boolean - true if message is forwarded
|
|
158
|
+
- forwarding_score: int - forwarding count
|
|
159
|
+
- group_info: object - present for group messages:
|
|
160
|
+
- group_jid: string
|
|
161
|
+
- sender_jid: string
|
|
162
|
+
- sender_phone: string - RESOLVED phone number
|
|
163
|
+
- sender_name: string - push name if available
|
|
164
|
+
|
|
165
|
+
Note: The Go RPC already resolves LIDs to phone numbers before sending the event.
|
|
166
|
+
The sender_phone field contains the resolved phone number - no manual LID resolution needed!
|
|
167
|
+
"""
|
|
168
|
+
msg_type = params.get('messageTypeFilter', 'all')
|
|
169
|
+
sender_filter = params.get('filter', 'all')
|
|
170
|
+
contact_phone = params.get('contactPhone', '')
|
|
171
|
+
group_id = params.get('group_id') or params.get('groupId', '')
|
|
172
|
+
sender_number = params.get('senderNumber', '') # Optional sender filter within group
|
|
173
|
+
keywords = [k.strip().lower() for k in params.get('keywords', '').split(',') if k.strip()]
|
|
174
|
+
ignore_own = params.get('ignoreOwnMessages', True)
|
|
175
|
+
forwarded_filter = params.get('forwardedFilter', 'all') # 'all', 'only_forwarded', 'ignore_forwarded'
|
|
176
|
+
|
|
177
|
+
logger.debug(f"[WhatsAppFilter] Built: type={msg_type}, filter={sender_filter}, group_id='{group_id}', forwarded={forwarded_filter}")
|
|
178
|
+
|
|
179
|
+
def matches(m: Dict) -> bool:
|
|
180
|
+
msg_chat_id = m.get('chat_id', '')
|
|
181
|
+
is_group = m.get('is_group', False)
|
|
182
|
+
group_info = m.get('group_info', {})
|
|
183
|
+
|
|
184
|
+
# Use sender_phone directly - Go RPC already resolves LIDs to phone numbers!
|
|
185
|
+
# For group messages, prefer group_info.sender_phone, fall back to root sender_phone
|
|
186
|
+
if is_group:
|
|
187
|
+
sender_phone = group_info.get('sender_phone', '') or m.get('sender_phone', '')
|
|
188
|
+
else:
|
|
189
|
+
sender_phone = m.get('sender_phone', '')
|
|
190
|
+
|
|
191
|
+
# Fallback: extract phone from sender JID if sender_phone not available
|
|
192
|
+
if not sender_phone:
|
|
193
|
+
sender = m.get('sender', '')
|
|
194
|
+
sender_phone = sender.split('@')[0] if '@' in sender else sender
|
|
195
|
+
|
|
196
|
+
# Message type filter (schema field: message_type)
|
|
197
|
+
if msg_type != 'all' and m.get('message_type') != msg_type:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
# Sender filter - for contact filter, use actual phone number
|
|
201
|
+
if sender_filter == 'any_contact':
|
|
202
|
+
# Only accept non-group messages (individual/contact messages)
|
|
203
|
+
if is_group:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
if sender_filter == 'contact':
|
|
207
|
+
if contact_phone not in sender_phone:
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
if sender_filter == 'group':
|
|
211
|
+
# For group filter, check if message is from that group
|
|
212
|
+
if not is_group:
|
|
213
|
+
return False
|
|
214
|
+
if msg_chat_id != group_id:
|
|
215
|
+
return False
|
|
216
|
+
# Optional: filter by specific sender within group using resolved phone number
|
|
217
|
+
if sender_number:
|
|
218
|
+
if sender_number not in sender_phone:
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
if sender_filter == 'keywords':
|
|
222
|
+
text = (m.get('text') or '').lower()
|
|
223
|
+
if not any(kw in text for kw in keywords):
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
# Ignore own messages (schema field: is_from_me)
|
|
227
|
+
if ignore_own and m.get('is_from_me'):
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
# Forwarded message filter (schema field: is_forwarded)
|
|
231
|
+
is_forwarded = m.get('is_forwarded', False)
|
|
232
|
+
if forwarded_filter == 'only_forwarded' and not is_forwarded:
|
|
233
|
+
return False
|
|
234
|
+
if forwarded_filter == 'ignore_forwarded' and is_forwarded:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
logger.debug(f"[WhatsAppFilter] Matched message from {sender_phone}")
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
return matches
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def build_webhook_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
244
|
+
"""Build filter function for webhook requests.
|
|
245
|
+
|
|
246
|
+
Filters by webhook path to ensure the event is for the correct trigger node.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
params: Node parameters with 'path' field
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Filter function that checks if event path matches
|
|
253
|
+
"""
|
|
254
|
+
webhook_path = params.get('path', '')
|
|
255
|
+
|
|
256
|
+
def matches(data: Dict) -> bool:
|
|
257
|
+
event_path = data.get('path', '')
|
|
258
|
+
if webhook_path and event_path != webhook_path:
|
|
259
|
+
return False
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
return matches
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def build_chat_filter(params: Dict) -> Callable[[Dict], bool]:
|
|
266
|
+
"""Build filter function for chat messages from console input.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
params: Node parameters with 'sessionId' field
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Filter function that checks if event session_id matches
|
|
273
|
+
"""
|
|
274
|
+
session_id = params.get('sessionId', 'default')
|
|
275
|
+
|
|
276
|
+
def matches(data: Dict) -> bool:
|
|
277
|
+
event_session = data.get('session_id', 'default')
|
|
278
|
+
if session_id != 'default' and event_session != session_id:
|
|
279
|
+
return False
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
return matches
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# Registry of filter builders per trigger type
|
|
286
|
+
FILTER_BUILDERS: Dict[str, Callable[[Dict], Callable[[Dict], bool]]] = {
|
|
287
|
+
'whatsappReceive': build_whatsapp_filter,
|
|
288
|
+
'webhookTrigger': build_webhook_filter,
|
|
289
|
+
'chatTrigger': build_chat_filter,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def build_filter(node_type: str, params: Dict) -> Callable[[Dict], bool]:
|
|
294
|
+
"""Build a filter function for the given trigger type and parameters."""
|
|
295
|
+
builder = FILTER_BUILDERS.get(node_type)
|
|
296
|
+
if builder:
|
|
297
|
+
return builder(params)
|
|
298
|
+
# Default: accept all events
|
|
299
|
+
return lambda x: True
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# =============================================================================
|
|
303
|
+
# WAITER DATA STRUCTURES
|
|
304
|
+
# =============================================================================
|
|
305
|
+
|
|
306
|
+
@dataclass
|
|
307
|
+
class Waiter:
|
|
308
|
+
"""Single event waiter.
|
|
309
|
+
|
|
310
|
+
In memory mode: uses asyncio.Future
|
|
311
|
+
In Redis mode: uses stream polling with stored metadata
|
|
312
|
+
"""
|
|
313
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
314
|
+
node_id: str = ""
|
|
315
|
+
node_type: str = ""
|
|
316
|
+
event_type: str = ""
|
|
317
|
+
params: Dict = field(default_factory=dict) # Store params for Redis mode filter rebuild
|
|
318
|
+
filter_fn: Callable[[Dict], bool] = field(default_factory=lambda: lambda x: True)
|
|
319
|
+
future: Optional[asyncio.Future] = None # Only used in memory mode
|
|
320
|
+
cancelled: bool = False
|
|
321
|
+
created_at: float = field(default_factory=time.time)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# Module-level waiter storage (used in both modes for tracking)
|
|
325
|
+
_waiters: Dict[str, Waiter] = {}
|
|
326
|
+
|
|
327
|
+
# Redis stream names
|
|
328
|
+
EVENTS_STREAM_PREFIX = "events:"
|
|
329
|
+
WAITERS_KEY_PREFIX = "waiters:"
|
|
330
|
+
# NOTE: Each waiter uses its own consumer group to ensure ALL waiters receive ALL messages.
|
|
331
|
+
# Redis consumer groups deliver each message to only ONE consumer in the group.
|
|
332
|
+
# For trigger nodes, we want broadcast semantics where every waiter evaluates every event.
|
|
333
|
+
CONSUMER_GROUP_PREFIX = "waiter_group_" # Each waiter gets: waiter_group_{waiter_id}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _get_stream_name(event_type: str) -> str:
|
|
337
|
+
"""Get Redis stream name for event type."""
|
|
338
|
+
return f"{EVENTS_STREAM_PREFIX}{event_type}"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# =============================================================================
|
|
342
|
+
# WAITER REGISTRATION
|
|
343
|
+
# =============================================================================
|
|
344
|
+
|
|
345
|
+
async def register(node_type: str, node_id: str, params: Dict) -> Waiter:
|
|
346
|
+
"""Register a waiter for a trigger node.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
node_type: Type of trigger node (e.g., 'whatsappReceive')
|
|
350
|
+
node_id: ID of the node waiting
|
|
351
|
+
params: Node parameters for building filter
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Waiter object to await
|
|
355
|
+
"""
|
|
356
|
+
config = get_trigger_config(node_type)
|
|
357
|
+
if not config:
|
|
358
|
+
raise ValueError(f"Unknown trigger type: {node_type}")
|
|
359
|
+
|
|
360
|
+
# Create waiter
|
|
361
|
+
waiter = Waiter(
|
|
362
|
+
node_id=node_id,
|
|
363
|
+
node_type=node_type,
|
|
364
|
+
event_type=config.event_type,
|
|
365
|
+
params=params,
|
|
366
|
+
filter_fn=build_filter(node_type, params),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if is_redis_mode():
|
|
370
|
+
# Redis mode: store waiter metadata in Redis
|
|
371
|
+
cache = get_cache_service()
|
|
372
|
+
waiter_key = f"{WAITERS_KEY_PREFIX}{waiter.id}"
|
|
373
|
+
|
|
374
|
+
# Each waiter gets its own consumer group for broadcast semantics
|
|
375
|
+
# This ensures ALL waiters receive ALL messages (not load-balanced)
|
|
376
|
+
consumer_group = f"{CONSUMER_GROUP_PREFIX}{waiter.id}"
|
|
377
|
+
|
|
378
|
+
waiter_data = {
|
|
379
|
+
"id": waiter.id,
|
|
380
|
+
"node_id": node_id,
|
|
381
|
+
"node_type": node_type,
|
|
382
|
+
"event_type": config.event_type,
|
|
383
|
+
"params": json.dumps(params),
|
|
384
|
+
"created_at": waiter.created_at,
|
|
385
|
+
"consumer_group": consumer_group, # Store for cleanup
|
|
386
|
+
}
|
|
387
|
+
await cache.set(waiter_key, waiter_data, ttl=86400) # 24 hour TTL
|
|
388
|
+
|
|
389
|
+
# Create unique consumer group for this waiter
|
|
390
|
+
# start_id='$' means only new messages from this point forward
|
|
391
|
+
stream_name = _get_stream_name(config.event_type)
|
|
392
|
+
await cache.stream_create_group(stream_name, consumer_group, start_id='$')
|
|
393
|
+
|
|
394
|
+
logger.debug(f"[EventWaiter] Registered {node_type} waiter {waiter.id} (Redis)")
|
|
395
|
+
else:
|
|
396
|
+
# Memory mode: create asyncio.Future
|
|
397
|
+
try:
|
|
398
|
+
loop = asyncio.get_running_loop()
|
|
399
|
+
waiter.future = loop.create_future()
|
|
400
|
+
except RuntimeError:
|
|
401
|
+
waiter.future = asyncio.get_event_loop().create_future()
|
|
402
|
+
|
|
403
|
+
logger.debug(f"[EventWaiter] Registered {node_type} waiter {waiter.id}")
|
|
404
|
+
|
|
405
|
+
_waiters[waiter.id] = waiter
|
|
406
|
+
return waiter
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
async def wait_for_event(waiter: Waiter, timeout: Optional[float] = None) -> Dict:
|
|
410
|
+
"""Wait for an event matching the waiter's filter.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
waiter: The registered waiter
|
|
414
|
+
timeout: Optional timeout in seconds (None = wait forever)
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Event data when matched
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
asyncio.CancelledError: If waiter was cancelled
|
|
421
|
+
asyncio.TimeoutError: If timeout exceeded
|
|
422
|
+
"""
|
|
423
|
+
if is_redis_mode():
|
|
424
|
+
return await _wait_redis(waiter, timeout)
|
|
425
|
+
else:
|
|
426
|
+
return await _wait_memory(waiter, timeout)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
async def _wait_memory(waiter: Waiter, timeout: Optional[float]) -> Dict:
|
|
430
|
+
"""Wait using asyncio.Future (memory mode)."""
|
|
431
|
+
if waiter.future is None:
|
|
432
|
+
raise RuntimeError("Waiter has no Future (memory mode not initialized)")
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
if timeout:
|
|
436
|
+
return await asyncio.wait_for(waiter.future, timeout)
|
|
437
|
+
else:
|
|
438
|
+
return await waiter.future
|
|
439
|
+
except asyncio.CancelledError:
|
|
440
|
+
_cleanup_waiter(waiter.id)
|
|
441
|
+
raise
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
async def _wait_redis(waiter: Waiter, timeout: Optional[float]) -> Dict:
|
|
445
|
+
"""Wait using Redis Streams polling.
|
|
446
|
+
|
|
447
|
+
Polls the event stream with blocking XREAD, checking each message against the filter.
|
|
448
|
+
Each waiter has its own consumer group for broadcast semantics.
|
|
449
|
+
"""
|
|
450
|
+
cache = get_cache_service()
|
|
451
|
+
stream_name = _get_stream_name(waiter.event_type)
|
|
452
|
+
|
|
453
|
+
# Use waiter-specific consumer group for broadcast (all waiters see all messages)
|
|
454
|
+
consumer_group = f"{CONSUMER_GROUP_PREFIX}{waiter.id}"
|
|
455
|
+
consumer_name = f"consumer_{waiter.id}"
|
|
456
|
+
|
|
457
|
+
# Start reading from now (new messages only)
|
|
458
|
+
last_id = '$'
|
|
459
|
+
block_ms = 5000 # 5 second blocks to allow cancellation checks
|
|
460
|
+
|
|
461
|
+
start_time = time.time()
|
|
462
|
+
|
|
463
|
+
while not waiter.cancelled:
|
|
464
|
+
# Check timeout
|
|
465
|
+
if timeout and (time.time() - start_time) > timeout:
|
|
466
|
+
raise asyncio.TimeoutError(f"Waiter {waiter.id} timed out after {timeout}s")
|
|
467
|
+
|
|
468
|
+
# Read from stream with blocking using waiter's own consumer group
|
|
469
|
+
try:
|
|
470
|
+
result = await cache.stream_read_group(
|
|
471
|
+
consumer_group, # Each waiter has its own group
|
|
472
|
+
consumer_name,
|
|
473
|
+
{stream_name: '>'}, # '>' = new messages for this consumer
|
|
474
|
+
count=10,
|
|
475
|
+
block=block_ms
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
if not result:
|
|
479
|
+
# No messages, continue polling
|
|
480
|
+
continue
|
|
481
|
+
|
|
482
|
+
# Process messages
|
|
483
|
+
for stream_data in result:
|
|
484
|
+
stream, messages = stream_data
|
|
485
|
+
for msg_id, fields in messages:
|
|
486
|
+
# Deserialize event data
|
|
487
|
+
event_data = {}
|
|
488
|
+
for k, v in fields.items():
|
|
489
|
+
try:
|
|
490
|
+
event_data[k] = json.loads(v)
|
|
491
|
+
except (json.JSONDecodeError, TypeError):
|
|
492
|
+
event_data[k] = v
|
|
493
|
+
|
|
494
|
+
# Check filter
|
|
495
|
+
if waiter.filter_fn(event_data):
|
|
496
|
+
# Match found - acknowledge and return
|
|
497
|
+
await cache.stream_ack(stream_name, consumer_group, msg_id)
|
|
498
|
+
_cleanup_waiter(waiter.id)
|
|
499
|
+
logger.info(f"[EventWaiter] Waiter {waiter.id} matched event {msg_id}")
|
|
500
|
+
return event_data
|
|
501
|
+
else:
|
|
502
|
+
# No match - acknowledge but continue waiting
|
|
503
|
+
await cache.stream_ack(stream_name, consumer_group, msg_id)
|
|
504
|
+
|
|
505
|
+
except asyncio.CancelledError:
|
|
506
|
+
_cleanup_waiter(waiter.id)
|
|
507
|
+
raise
|
|
508
|
+
|
|
509
|
+
# Waiter was cancelled via cancel() flag
|
|
510
|
+
_cleanup_waiter(waiter.id)
|
|
511
|
+
raise asyncio.CancelledError(f"Waiter {waiter.id} cancelled")
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _cleanup_waiter(waiter_id: str) -> None:
|
|
515
|
+
"""Remove waiter from storage."""
|
|
516
|
+
_waiters.pop(waiter_id, None)
|
|
517
|
+
|
|
518
|
+
# Also remove from Redis if in Redis mode
|
|
519
|
+
if is_redis_mode():
|
|
520
|
+
cache = get_cache_service()
|
|
521
|
+
waiter_key = f"{WAITERS_KEY_PREFIX}{waiter_id}"
|
|
522
|
+
asyncio.create_task(cache.delete(waiter_key))
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# =============================================================================
|
|
526
|
+
# EVENT DISPATCH
|
|
527
|
+
# =============================================================================
|
|
528
|
+
|
|
529
|
+
async def dispatch_async(event_type: str, data: Dict) -> int:
|
|
530
|
+
"""Dispatch event asynchronously (for Redis mode).
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
event_type: Type of event (e.g., 'whatsapp_message_received')
|
|
534
|
+
data: Event data
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
1 if event was added to stream, 0 otherwise
|
|
538
|
+
"""
|
|
539
|
+
logger.debug(f"[EventWaiter] dispatch_async: event_type='{event_type}'")
|
|
540
|
+
|
|
541
|
+
if is_redis_mode():
|
|
542
|
+
cache = get_cache_service()
|
|
543
|
+
stream_name = _get_stream_name(event_type)
|
|
544
|
+
msg_id = await cache.stream_add(stream_name, data)
|
|
545
|
+
if msg_id:
|
|
546
|
+
logger.debug(f"[EventWaiter] Added event to stream {stream_name}: {msg_id}")
|
|
547
|
+
return 1
|
|
548
|
+
return 0
|
|
549
|
+
else:
|
|
550
|
+
# Fall back to sync dispatch for memory mode
|
|
551
|
+
return dispatch(event_type, data)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def dispatch(event_type: str, data: Dict) -> int:
|
|
555
|
+
"""Dispatch event to matching waiters (synchronous, memory mode).
|
|
556
|
+
|
|
557
|
+
Thread-safe: Can be called from APScheduler threads or async context.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
event_type: Type of event (e.g., 'whatsapp_message_received')
|
|
561
|
+
data: Event data
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Number of waiters resolved
|
|
565
|
+
"""
|
|
566
|
+
if is_redis_mode():
|
|
567
|
+
# In Redis mode, use async dispatch
|
|
568
|
+
# Handle both async context and thread context (e.g., APScheduler callbacks)
|
|
569
|
+
try:
|
|
570
|
+
# Try to get the current running loop
|
|
571
|
+
loop = asyncio.get_running_loop()
|
|
572
|
+
# We're in an async context - schedule task normally
|
|
573
|
+
asyncio.create_task(dispatch_async(event_type, data))
|
|
574
|
+
except RuntimeError:
|
|
575
|
+
# No running loop - we're in a thread (e.g., APScheduler callback)
|
|
576
|
+
# Use the stored main loop with thread-safe dispatch
|
|
577
|
+
if _main_loop is not None and _main_loop.is_running():
|
|
578
|
+
asyncio.run_coroutine_threadsafe(dispatch_async(event_type, data), _main_loop)
|
|
579
|
+
else:
|
|
580
|
+
logger.warning(f"[EventWaiter] No event loop available for dispatch of {event_type}")
|
|
581
|
+
return 0 # Actual resolution happens in _wait_redis
|
|
582
|
+
|
|
583
|
+
resolved = 0
|
|
584
|
+
to_remove = []
|
|
585
|
+
|
|
586
|
+
for wid, w in _waiters.items():
|
|
587
|
+
if w.event_type == event_type and w.future and not w.future.done():
|
|
588
|
+
try:
|
|
589
|
+
if w.filter_fn(data):
|
|
590
|
+
w.future.set_result(data)
|
|
591
|
+
to_remove.append(wid)
|
|
592
|
+
resolved += 1
|
|
593
|
+
logger.debug(f"[EventWaiter] Resolved {w.node_type} waiter {wid}")
|
|
594
|
+
except Exception as e:
|
|
595
|
+
logger.error(f"[EventWaiter] Filter error for waiter {wid}: {e}")
|
|
596
|
+
|
|
597
|
+
for wid in to_remove:
|
|
598
|
+
_waiters.pop(wid, None)
|
|
599
|
+
|
|
600
|
+
return resolved
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# =============================================================================
|
|
604
|
+
# WAITER CANCELLATION
|
|
605
|
+
# =============================================================================
|
|
606
|
+
|
|
607
|
+
def cancel(waiter_id: str) -> bool:
|
|
608
|
+
"""Cancel a waiter by ID."""
|
|
609
|
+
if w := _waiters.pop(waiter_id, None):
|
|
610
|
+
w.cancelled = True
|
|
611
|
+
|
|
612
|
+
if w.future and not w.future.done():
|
|
613
|
+
w.future.cancel()
|
|
614
|
+
|
|
615
|
+
# Also remove from Redis if in Redis mode
|
|
616
|
+
if is_redis_mode():
|
|
617
|
+
cache = get_cache_service()
|
|
618
|
+
waiter_key = f"{WAITERS_KEY_PREFIX}{waiter_id}"
|
|
619
|
+
asyncio.create_task(cache.delete(waiter_key))
|
|
620
|
+
|
|
621
|
+
logger.debug(f"[EventWaiter] Cancelled waiter {waiter_id}")
|
|
622
|
+
return True
|
|
623
|
+
|
|
624
|
+
return False
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def cancel_for_node(node_id: str) -> int:
|
|
628
|
+
"""Cancel all waiters for a node."""
|
|
629
|
+
to_cancel = [wid for wid, w in _waiters.items() if w.node_id == node_id]
|
|
630
|
+
for wid in to_cancel:
|
|
631
|
+
cancel(wid)
|
|
632
|
+
return len(to_cancel)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
# =============================================================================
|
|
636
|
+
# UTILITY FUNCTIONS
|
|
637
|
+
# =============================================================================
|
|
638
|
+
|
|
639
|
+
def get_active_waiters() -> List[Dict[str, Any]]:
|
|
640
|
+
"""Get info about active waiters (for debugging/UI)."""
|
|
641
|
+
return [
|
|
642
|
+
{
|
|
643
|
+
"id": w.id,
|
|
644
|
+
"node_id": w.node_id,
|
|
645
|
+
"node_type": w.node_type,
|
|
646
|
+
"event_type": w.event_type,
|
|
647
|
+
"done": w.future.done() if w.future else False,
|
|
648
|
+
"cancelled": w.cancelled,
|
|
649
|
+
"age_seconds": time.time() - w.created_at,
|
|
650
|
+
"mode": "redis" if is_redis_mode() else "memory",
|
|
651
|
+
}
|
|
652
|
+
for w in _waiters.values()
|
|
653
|
+
]
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def clear_all() -> int:
|
|
657
|
+
"""Clear all waiters (for testing/cleanup)."""
|
|
658
|
+
count = len(_waiters)
|
|
659
|
+
for w in _waiters.values():
|
|
660
|
+
w.cancelled = True
|
|
661
|
+
if w.future and not w.future.done():
|
|
662
|
+
w.future.cancel()
|
|
663
|
+
_waiters.clear()
|
|
664
|
+
|
|
665
|
+
# Clear Redis waiter keys if in Redis mode
|
|
666
|
+
if is_redis_mode():
|
|
667
|
+
cache = get_cache_service()
|
|
668
|
+
asyncio.create_task(cache.clear_pattern(f"{WAITERS_KEY_PREFIX}*"))
|
|
669
|
+
|
|
670
|
+
return count
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def get_backend_mode() -> str:
|
|
674
|
+
"""Get current backend mode for debugging."""
|
|
675
|
+
return "redis" if is_redis_mode() else "memory"
|