machinaos 0.0.1

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.
Files changed (288) hide show
  1. package/.env.template +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -0
  4. package/bin/cli.js +159 -0
  5. package/client/.dockerignore +45 -0
  6. package/client/Dockerfile +68 -0
  7. package/client/eslint.config.js +29 -0
  8. package/client/index.html +13 -0
  9. package/client/nginx.conf +66 -0
  10. package/client/package.json +48 -0
  11. package/client/src/App.tsx +27 -0
  12. package/client/src/Dashboard.tsx +1173 -0
  13. package/client/src/ParameterPanel.tsx +301 -0
  14. package/client/src/components/AIAgentNode.tsx +321 -0
  15. package/client/src/components/APIKeyValidator.tsx +118 -0
  16. package/client/src/components/ClaudeChatModelNode.tsx +18 -0
  17. package/client/src/components/ConditionalEdge.tsx +189 -0
  18. package/client/src/components/CredentialsModal.tsx +306 -0
  19. package/client/src/components/EdgeConditionEditor.tsx +443 -0
  20. package/client/src/components/GeminiChatModelNode.tsx +18 -0
  21. package/client/src/components/GenericNode.tsx +357 -0
  22. package/client/src/components/LocationParameterPanel.tsx +154 -0
  23. package/client/src/components/ModelNode.tsx +286 -0
  24. package/client/src/components/OpenAIChatModelNode.tsx +18 -0
  25. package/client/src/components/OutputPanel.tsx +471 -0
  26. package/client/src/components/ParameterRenderer.tsx +1874 -0
  27. package/client/src/components/SkillEditorModal.tsx +417 -0
  28. package/client/src/components/SquareNode.tsx +797 -0
  29. package/client/src/components/StartNode.tsx +250 -0
  30. package/client/src/components/ToolkitNode.tsx +365 -0
  31. package/client/src/components/TriggerNode.tsx +463 -0
  32. package/client/src/components/auth/LoginPage.tsx +247 -0
  33. package/client/src/components/auth/ProtectedRoute.tsx +59 -0
  34. package/client/src/components/base/BaseChatModelNode.tsx +271 -0
  35. package/client/src/components/icons/AIProviderIcons.tsx +50 -0
  36. package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
  37. package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
  38. package/client/src/components/maps/index.ts +26 -0
  39. package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
  40. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
  41. package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
  42. package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
  43. package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
  44. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
  45. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
  46. package/client/src/components/parameterPanel/index.ts +42 -0
  47. package/client/src/components/shared/DataPanel.tsx +142 -0
  48. package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
  49. package/client/src/components/ui/AIResultModal.tsx +204 -0
  50. package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
  51. package/client/src/components/ui/CodeEditor.tsx +81 -0
  52. package/client/src/components/ui/CollapsibleSection.tsx +88 -0
  53. package/client/src/components/ui/ComponentItem.tsx +154 -0
  54. package/client/src/components/ui/ComponentPalette.tsx +321 -0
  55. package/client/src/components/ui/ConsolePanel.tsx +1074 -0
  56. package/client/src/components/ui/ErrorBoundary.tsx +196 -0
  57. package/client/src/components/ui/InputNodesPanel.tsx +204 -0
  58. package/client/src/components/ui/MapSelector.tsx +314 -0
  59. package/client/src/components/ui/Modal.tsx +149 -0
  60. package/client/src/components/ui/NodeContextMenu.tsx +192 -0
  61. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
  62. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
  63. package/client/src/components/ui/SettingsPanel.tsx +243 -0
  64. package/client/src/components/ui/TopToolbar.tsx +736 -0
  65. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
  66. package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
  67. package/client/src/config/antdTheme.ts +186 -0
  68. package/client/src/config/api.ts +54 -0
  69. package/client/src/contexts/AuthContext.tsx +221 -0
  70. package/client/src/contexts/ThemeContext.tsx +42 -0
  71. package/client/src/contexts/WebSocketContext.tsx +1971 -0
  72. package/client/src/factories/baseChatModelFactory.ts +256 -0
  73. package/client/src/hooks/useAndroidOperations.ts +164 -0
  74. package/client/src/hooks/useApiKeyValidation.ts +107 -0
  75. package/client/src/hooks/useApiKeys.ts +238 -0
  76. package/client/src/hooks/useAppTheme.ts +17 -0
  77. package/client/src/hooks/useComponentPalette.ts +51 -0
  78. package/client/src/hooks/useCopyPaste.ts +155 -0
  79. package/client/src/hooks/useDragAndDrop.ts +124 -0
  80. package/client/src/hooks/useDragVariable.ts +88 -0
  81. package/client/src/hooks/useExecution.ts +313 -0
  82. package/client/src/hooks/useParameterPanel.ts +176 -0
  83. package/client/src/hooks/useReactFlowNodes.ts +189 -0
  84. package/client/src/hooks/useToolSchema.ts +209 -0
  85. package/client/src/hooks/useWhatsApp.ts +196 -0
  86. package/client/src/hooks/useWorkflowManagement.ts +46 -0
  87. package/client/src/index.css +315 -0
  88. package/client/src/main.tsx +19 -0
  89. package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
  90. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
  91. package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
  92. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
  93. package/client/src/nodeDefinitions/chatNodes.ts +135 -0
  94. package/client/src/nodeDefinitions/codeNodes.ts +54 -0
  95. package/client/src/nodeDefinitions/documentNodes.ts +379 -0
  96. package/client/src/nodeDefinitions/index.ts +15 -0
  97. package/client/src/nodeDefinitions/locationNodes.ts +463 -0
  98. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
  99. package/client/src/nodeDefinitions/skillNodes.ts +211 -0
  100. package/client/src/nodeDefinitions/toolNodes.ts +198 -0
  101. package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
  102. package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
  103. package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
  104. package/client/src/nodeDefinitions.ts +104 -0
  105. package/client/src/schemas/workflowSchema.ts +264 -0
  106. package/client/src/services/dynamicParameterService.ts +96 -0
  107. package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
  108. package/client/src/services/executionService.ts +232 -0
  109. package/client/src/services/workflowApi.ts +91 -0
  110. package/client/src/store/useAppStore.ts +582 -0
  111. package/client/src/styles/theme.ts +508 -0
  112. package/client/src/styles/zIndex.ts +17 -0
  113. package/client/src/types/ComponentTypes.ts +39 -0
  114. package/client/src/types/EdgeCondition.ts +231 -0
  115. package/client/src/types/INodeProperties.ts +288 -0
  116. package/client/src/types/NodeTypes.ts +28 -0
  117. package/client/src/utils/formatters.ts +33 -0
  118. package/client/src/utils/googleMapsLoader.ts +140 -0
  119. package/client/src/utils/locationUtils.ts +85 -0
  120. package/client/src/utils/nodeUtils.ts +31 -0
  121. package/client/src/utils/workflow.ts +30 -0
  122. package/client/src/utils/workflowExport.ts +120 -0
  123. package/client/src/vite-env.d.ts +12 -0
  124. package/client/tailwind.config.js +60 -0
  125. package/client/tsconfig.json +25 -0
  126. package/client/tsconfig.node.json +11 -0
  127. package/client/vite.config.js +35 -0
  128. package/docker-compose.prod.yml +107 -0
  129. package/docker-compose.yml +104 -0
  130. package/docs-MachinaOs/README.md +85 -0
  131. package/docs-MachinaOs/deployment/docker.mdx +228 -0
  132. package/docs-MachinaOs/deployment/production.mdx +345 -0
  133. package/docs-MachinaOs/docs.json +75 -0
  134. package/docs-MachinaOs/faq.mdx +309 -0
  135. package/docs-MachinaOs/favicon.svg +5 -0
  136. package/docs-MachinaOs/installation.mdx +160 -0
  137. package/docs-MachinaOs/introduction.mdx +114 -0
  138. package/docs-MachinaOs/logo/dark.svg +6 -0
  139. package/docs-MachinaOs/logo/light.svg +6 -0
  140. package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
  141. package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
  142. package/docs-MachinaOs/nodes/android.mdx +411 -0
  143. package/docs-MachinaOs/nodes/overview.mdx +181 -0
  144. package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
  145. package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
  146. package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
  147. package/docs-MachinaOs/quickstart.mdx +119 -0
  148. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
  149. package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
  150. package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
  151. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
  152. package/nul +0 -0
  153. package/package.json +70 -0
  154. package/scripts/build.js +158 -0
  155. package/scripts/check-ports.ps1 +33 -0
  156. package/scripts/clean.js +40 -0
  157. package/scripts/docker.js +93 -0
  158. package/scripts/kill-port.ps1 +154 -0
  159. package/scripts/start.js +210 -0
  160. package/scripts/stop.js +325 -0
  161. package/server/.dockerignore +44 -0
  162. package/server/Dockerfile +45 -0
  163. package/server/constants.py +249 -0
  164. package/server/core/__init__.py +1 -0
  165. package/server/core/cache.py +461 -0
  166. package/server/core/config.py +128 -0
  167. package/server/core/container.py +99 -0
  168. package/server/core/database.py +1211 -0
  169. package/server/core/logging.py +314 -0
  170. package/server/main.py +289 -0
  171. package/server/middleware/__init__.py +5 -0
  172. package/server/middleware/auth.py +89 -0
  173. package/server/models/__init__.py +1 -0
  174. package/server/models/auth.py +52 -0
  175. package/server/models/cache.py +24 -0
  176. package/server/models/database.py +211 -0
  177. package/server/models/nodes.py +455 -0
  178. package/server/package.json +9 -0
  179. package/server/pyproject.toml +72 -0
  180. package/server/requirements.txt +83 -0
  181. package/server/routers/__init__.py +1 -0
  182. package/server/routers/android.py +294 -0
  183. package/server/routers/auth.py +203 -0
  184. package/server/routers/database.py +151 -0
  185. package/server/routers/maps.py +142 -0
  186. package/server/routers/nodejs_compat.py +289 -0
  187. package/server/routers/webhook.py +90 -0
  188. package/server/routers/websocket.py +2127 -0
  189. package/server/routers/whatsapp.py +761 -0
  190. package/server/routers/workflow.py +200 -0
  191. package/server/services/__init__.py +1 -0
  192. package/server/services/ai.py +2415 -0
  193. package/server/services/android/__init__.py +27 -0
  194. package/server/services/android/broadcaster.py +114 -0
  195. package/server/services/android/client.py +608 -0
  196. package/server/services/android/manager.py +78 -0
  197. package/server/services/android/protocol.py +165 -0
  198. package/server/services/android_service.py +588 -0
  199. package/server/services/auth.py +131 -0
  200. package/server/services/chat_client.py +160 -0
  201. package/server/services/deployment/__init__.py +12 -0
  202. package/server/services/deployment/manager.py +706 -0
  203. package/server/services/deployment/state.py +47 -0
  204. package/server/services/deployment/triggers.py +275 -0
  205. package/server/services/event_waiter.py +785 -0
  206. package/server/services/execution/__init__.py +77 -0
  207. package/server/services/execution/cache.py +769 -0
  208. package/server/services/execution/conditions.py +373 -0
  209. package/server/services/execution/dlq.py +132 -0
  210. package/server/services/execution/executor.py +1351 -0
  211. package/server/services/execution/models.py +531 -0
  212. package/server/services/execution/recovery.py +235 -0
  213. package/server/services/handlers/__init__.py +126 -0
  214. package/server/services/handlers/ai.py +355 -0
  215. package/server/services/handlers/android.py +260 -0
  216. package/server/services/handlers/code.py +278 -0
  217. package/server/services/handlers/document.py +598 -0
  218. package/server/services/handlers/http.py +193 -0
  219. package/server/services/handlers/polyglot.py +105 -0
  220. package/server/services/handlers/tools.py +845 -0
  221. package/server/services/handlers/triggers.py +107 -0
  222. package/server/services/handlers/utility.py +822 -0
  223. package/server/services/handlers/whatsapp.py +476 -0
  224. package/server/services/maps.py +289 -0
  225. package/server/services/memory_store.py +103 -0
  226. package/server/services/node_executor.py +375 -0
  227. package/server/services/parameter_resolver.py +218 -0
  228. package/server/services/polyglot_client.py +169 -0
  229. package/server/services/scheduler.py +155 -0
  230. package/server/services/skill_loader.py +417 -0
  231. package/server/services/status_broadcaster.py +826 -0
  232. package/server/services/temporal/__init__.py +23 -0
  233. package/server/services/temporal/activities.py +344 -0
  234. package/server/services/temporal/client.py +76 -0
  235. package/server/services/temporal/executor.py +147 -0
  236. package/server/services/temporal/worker.py +251 -0
  237. package/server/services/temporal/workflow.py +355 -0
  238. package/server/services/temporal/ws_client.py +236 -0
  239. package/server/services/text.py +111 -0
  240. package/server/services/user_auth.py +172 -0
  241. package/server/services/websocket_client.py +29 -0
  242. package/server/services/workflow.py +597 -0
  243. package/server/skills/android-skill/SKILL.md +82 -0
  244. package/server/skills/assistant-personality/SKILL.md +45 -0
  245. package/server/skills/code-skill/SKILL.md +140 -0
  246. package/server/skills/http-skill/SKILL.md +161 -0
  247. package/server/skills/maps-skill/SKILL.md +170 -0
  248. package/server/skills/memory-skill/SKILL.md +154 -0
  249. package/server/skills/scheduler-skill/SKILL.md +84 -0
  250. package/server/skills/whatsapp-skill/SKILL.md +283 -0
  251. package/server/uv.lock +2916 -0
  252. package/server/whatsapp-rpc/.dockerignore +30 -0
  253. package/server/whatsapp-rpc/Dockerfile +44 -0
  254. package/server/whatsapp-rpc/Dockerfile.web +17 -0
  255. package/server/whatsapp-rpc/README.md +139 -0
  256. package/server/whatsapp-rpc/cli.js +95 -0
  257. package/server/whatsapp-rpc/configs/config.yaml +7 -0
  258. package/server/whatsapp-rpc/docker-compose.yml +35 -0
  259. package/server/whatsapp-rpc/docs/API.md +410 -0
  260. package/server/whatsapp-rpc/go.mod +67 -0
  261. package/server/whatsapp-rpc/go.sum +203 -0
  262. package/server/whatsapp-rpc/package.json +30 -0
  263. package/server/whatsapp-rpc/schema.json +1294 -0
  264. package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
  265. package/server/whatsapp-rpc/scripts/cli.js +162 -0
  266. package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
  267. package/server/whatsapp-rpc/src/go/config/config.go +49 -0
  268. package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
  269. package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
  270. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
  271. package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
  272. package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
  273. package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
  274. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
  275. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
  276. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
  277. package/server/whatsapp-rpc/web/app.py +609 -0
  278. package/server/whatsapp-rpc/web/requirements.txt +6 -0
  279. package/server/whatsapp-rpc/web/rpc_client.py +427 -0
  280. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
  281. package/server/whatsapp-rpc/web/templates/base.html +150 -0
  282. package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
  283. package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
  284. package/server/whatsapp-rpc/web/templates/groups.html +328 -0
  285. package/server/whatsapp-rpc/web/templates/messages.html +465 -0
  286. package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
  287. package/server/whatsapp-rpc/web/templates/send.html +259 -0
  288. package/server/whatsapp-rpc/web/templates/settings.html +459 -0
@@ -0,0 +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)}