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