loudmouth-ai 0.1.0 → 0.2.0

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 (593) hide show
  1. package/README.md +148 -77
  2. package/dist/build-info.json +3 -3
  3. package/extensions/package.json +6 -0
  4. package/package.json +1 -1
  5. package/skills/autopilot/SKILL.md +179 -0
  6. package/skills/goals/SKILL.md +189 -0
  7. package/skills/wordpress/SKILL.md +232 -0
  8. package/extensions/bluebubbles/clawdbot.plugin.json +0 -11
  9. package/extensions/bluebubbles/index.ts +0 -20
  10. package/extensions/bluebubbles/package.json +0 -33
  11. package/extensions/bluebubbles/src/accounts.ts +0 -80
  12. package/extensions/bluebubbles/src/actions.test.ts +0 -651
  13. package/extensions/bluebubbles/src/actions.ts +0 -403
  14. package/extensions/bluebubbles/src/attachments.test.ts +0 -346
  15. package/extensions/bluebubbles/src/attachments.ts +0 -282
  16. package/extensions/bluebubbles/src/channel.ts +0 -399
  17. package/extensions/bluebubbles/src/chat.test.ts +0 -462
  18. package/extensions/bluebubbles/src/chat.ts +0 -354
  19. package/extensions/bluebubbles/src/config-schema.ts +0 -51
  20. package/extensions/bluebubbles/src/media-send.ts +0 -168
  21. package/extensions/bluebubbles/src/monitor.test.ts +0 -2140
  22. package/extensions/bluebubbles/src/monitor.ts +0 -2101
  23. package/extensions/bluebubbles/src/onboarding.ts +0 -340
  24. package/extensions/bluebubbles/src/probe.ts +0 -127
  25. package/extensions/bluebubbles/src/reactions.test.ts +0 -393
  26. package/extensions/bluebubbles/src/reactions.ts +0 -183
  27. package/extensions/bluebubbles/src/runtime.ts +0 -14
  28. package/extensions/bluebubbles/src/send.test.ts +0 -809
  29. package/extensions/bluebubbles/src/send.ts +0 -418
  30. package/extensions/bluebubbles/src/targets.test.ts +0 -184
  31. package/extensions/bluebubbles/src/targets.ts +0 -323
  32. package/extensions/bluebubbles/src/types.ts +0 -127
  33. package/extensions/copilot-proxy/README.md +0 -24
  34. package/extensions/copilot-proxy/clawdbot.plugin.json +0 -11
  35. package/extensions/copilot-proxy/index.ts +0 -142
  36. package/extensions/copilot-proxy/package.json +0 -11
  37. package/extensions/google-antigravity-auth/README.md +0 -24
  38. package/extensions/google-antigravity-auth/clawdbot.plugin.json +0 -11
  39. package/extensions/google-antigravity-auth/index.ts +0 -437
  40. package/extensions/google-antigravity-auth/package.json +0 -11
  41. package/extensions/google-gemini-cli-auth/README.md +0 -35
  42. package/extensions/google-gemini-cli-auth/clawdbot.plugin.json +0 -11
  43. package/extensions/google-gemini-cli-auth/index.ts +0 -91
  44. package/extensions/google-gemini-cli-auth/oauth.test.ts +0 -228
  45. package/extensions/google-gemini-cli-auth/oauth.ts +0 -580
  46. package/extensions/google-gemini-cli-auth/package.json +0 -11
  47. package/extensions/googlechat/clawdbot.plugin.json +0 -11
  48. package/extensions/googlechat/index.ts +0 -20
  49. package/extensions/googlechat/package.json +0 -39
  50. package/extensions/googlechat/src/accounts.ts +0 -133
  51. package/extensions/googlechat/src/actions.ts +0 -162
  52. package/extensions/googlechat/src/api.test.ts +0 -62
  53. package/extensions/googlechat/src/api.ts +0 -259
  54. package/extensions/googlechat/src/auth.ts +0 -113
  55. package/extensions/googlechat/src/channel.ts +0 -580
  56. package/extensions/googlechat/src/monitor.test.ts +0 -27
  57. package/extensions/googlechat/src/monitor.ts +0 -900
  58. package/extensions/googlechat/src/onboarding.ts +0 -278
  59. package/extensions/googlechat/src/runtime.ts +0 -14
  60. package/extensions/googlechat/src/targets.test.ts +0 -35
  61. package/extensions/googlechat/src/targets.ts +0 -55
  62. package/extensions/googlechat/src/types.config.ts +0 -3
  63. package/extensions/googlechat/src/types.ts +0 -73
  64. package/extensions/imessage/clawdbot.plugin.json +0 -11
  65. package/extensions/imessage/index.ts +0 -18
  66. package/extensions/imessage/package.json +0 -11
  67. package/extensions/imessage/src/channel.ts +0 -294
  68. package/extensions/imessage/src/runtime.ts +0 -14
  69. package/extensions/line/clawdbot.plugin.json +0 -11
  70. package/extensions/line/index.ts +0 -20
  71. package/extensions/line/package.json +0 -29
  72. package/extensions/line/src/card-command.ts +0 -338
  73. package/extensions/line/src/channel.logout.test.ts +0 -96
  74. package/extensions/line/src/channel.sendPayload.test.ts +0 -308
  75. package/extensions/line/src/channel.ts +0 -773
  76. package/extensions/line/src/runtime.ts +0 -14
  77. package/extensions/matrix/CHANGELOG.md +0 -54
  78. package/extensions/matrix/clawdbot.plugin.json +0 -11
  79. package/extensions/matrix/index.ts +0 -18
  80. package/extensions/matrix/package.json +0 -36
  81. package/extensions/matrix/src/actions.ts +0 -185
  82. package/extensions/matrix/src/channel.directory.test.ts +0 -56
  83. package/extensions/matrix/src/channel.ts +0 -417
  84. package/extensions/matrix/src/config-schema.ts +0 -62
  85. package/extensions/matrix/src/directory-live.ts +0 -175
  86. package/extensions/matrix/src/group-mentions.ts +0 -61
  87. package/extensions/matrix/src/matrix/accounts.test.ts +0 -83
  88. package/extensions/matrix/src/matrix/accounts.ts +0 -63
  89. package/extensions/matrix/src/matrix/actions/client.ts +0 -53
  90. package/extensions/matrix/src/matrix/actions/messages.ts +0 -120
  91. package/extensions/matrix/src/matrix/actions/pins.ts +0 -70
  92. package/extensions/matrix/src/matrix/actions/reactions.ts +0 -84
  93. package/extensions/matrix/src/matrix/actions/room.ts +0 -88
  94. package/extensions/matrix/src/matrix/actions/summary.ts +0 -77
  95. package/extensions/matrix/src/matrix/actions/types.ts +0 -84
  96. package/extensions/matrix/src/matrix/actions.ts +0 -15
  97. package/extensions/matrix/src/matrix/active-client.ts +0 -11
  98. package/extensions/matrix/src/matrix/client/config.ts +0 -165
  99. package/extensions/matrix/src/matrix/client/create-client.ts +0 -127
  100. package/extensions/matrix/src/matrix/client/logging.ts +0 -35
  101. package/extensions/matrix/src/matrix/client/runtime.ts +0 -4
  102. package/extensions/matrix/src/matrix/client/shared.ts +0 -169
  103. package/extensions/matrix/src/matrix/client/storage.ts +0 -131
  104. package/extensions/matrix/src/matrix/client/types.ts +0 -34
  105. package/extensions/matrix/src/matrix/client.test.ts +0 -57
  106. package/extensions/matrix/src/matrix/client.ts +0 -9
  107. package/extensions/matrix/src/matrix/credentials.ts +0 -103
  108. package/extensions/matrix/src/matrix/deps.ts +0 -57
  109. package/extensions/matrix/src/matrix/format.test.ts +0 -34
  110. package/extensions/matrix/src/matrix/format.ts +0 -22
  111. package/extensions/matrix/src/matrix/index.ts +0 -11
  112. package/extensions/matrix/src/matrix/monitor/allowlist.ts +0 -58
  113. package/extensions/matrix/src/matrix/monitor/auto-join.ts +0 -68
  114. package/extensions/matrix/src/matrix/monitor/direct.ts +0 -105
  115. package/extensions/matrix/src/matrix/monitor/events.ts +0 -103
  116. package/extensions/matrix/src/matrix/monitor/handler.ts +0 -645
  117. package/extensions/matrix/src/matrix/monitor/index.ts +0 -279
  118. package/extensions/matrix/src/matrix/monitor/location.ts +0 -83
  119. package/extensions/matrix/src/matrix/monitor/media.test.ts +0 -103
  120. package/extensions/matrix/src/matrix/monitor/media.ts +0 -113
  121. package/extensions/matrix/src/matrix/monitor/mentions.ts +0 -31
  122. package/extensions/matrix/src/matrix/monitor/replies.ts +0 -96
  123. package/extensions/matrix/src/matrix/monitor/room-info.ts +0 -58
  124. package/extensions/matrix/src/matrix/monitor/rooms.ts +0 -43
  125. package/extensions/matrix/src/matrix/monitor/threads.ts +0 -64
  126. package/extensions/matrix/src/matrix/monitor/types.ts +0 -39
  127. package/extensions/matrix/src/matrix/poll-types.test.ts +0 -22
  128. package/extensions/matrix/src/matrix/poll-types.ts +0 -157
  129. package/extensions/matrix/src/matrix/probe.ts +0 -70
  130. package/extensions/matrix/src/matrix/send/client.ts +0 -63
  131. package/extensions/matrix/src/matrix/send/formatting.ts +0 -92
  132. package/extensions/matrix/src/matrix/send/media.ts +0 -220
  133. package/extensions/matrix/src/matrix/send/targets.test.ts +0 -102
  134. package/extensions/matrix/src/matrix/send/targets.ts +0 -144
  135. package/extensions/matrix/src/matrix/send/types.ts +0 -109
  136. package/extensions/matrix/src/matrix/send.test.ts +0 -172
  137. package/extensions/matrix/src/matrix/send.ts +0 -255
  138. package/extensions/matrix/src/onboarding.ts +0 -432
  139. package/extensions/matrix/src/outbound.ts +0 -53
  140. package/extensions/matrix/src/resolve-targets.ts +0 -89
  141. package/extensions/matrix/src/runtime.ts +0 -14
  142. package/extensions/matrix/src/tool-actions.ts +0 -160
  143. package/extensions/matrix/src/types.ts +0 -95
  144. package/extensions/mattermost/clawdbot.plugin.json +0 -11
  145. package/extensions/mattermost/index.ts +0 -18
  146. package/extensions/mattermost/package.json +0 -25
  147. package/extensions/mattermost/src/channel.test.ts +0 -43
  148. package/extensions/mattermost/src/channel.ts +0 -339
  149. package/extensions/mattermost/src/config-schema.ts +0 -56
  150. package/extensions/mattermost/src/group-mentions.ts +0 -14
  151. package/extensions/mattermost/src/mattermost/accounts.ts +0 -115
  152. package/extensions/mattermost/src/mattermost/client.ts +0 -208
  153. package/extensions/mattermost/src/mattermost/index.ts +0 -9
  154. package/extensions/mattermost/src/mattermost/monitor-helpers.ts +0 -150
  155. package/extensions/mattermost/src/mattermost/monitor.ts +0 -921
  156. package/extensions/mattermost/src/mattermost/probe.ts +0 -70
  157. package/extensions/mattermost/src/mattermost/send.ts +0 -217
  158. package/extensions/mattermost/src/normalize.ts +0 -38
  159. package/extensions/mattermost/src/onboarding-helpers.ts +0 -42
  160. package/extensions/mattermost/src/onboarding.ts +0 -187
  161. package/extensions/mattermost/src/runtime.ts +0 -14
  162. package/extensions/mattermost/src/types.ts +0 -50
  163. package/extensions/msteams/CHANGELOG.md +0 -51
  164. package/extensions/msteams/clawdbot.plugin.json +0 -11
  165. package/extensions/msteams/index.ts +0 -18
  166. package/extensions/msteams/package.json +0 -36
  167. package/extensions/msteams/src/attachments/download.ts +0 -206
  168. package/extensions/msteams/src/attachments/graph.ts +0 -319
  169. package/extensions/msteams/src/attachments/html.ts +0 -76
  170. package/extensions/msteams/src/attachments/payload.ts +0 -22
  171. package/extensions/msteams/src/attachments/shared.ts +0 -235
  172. package/extensions/msteams/src/attachments/types.ts +0 -37
  173. package/extensions/msteams/src/attachments.test.ts +0 -424
  174. package/extensions/msteams/src/attachments.ts +0 -18
  175. package/extensions/msteams/src/channel.directory.test.ts +0 -46
  176. package/extensions/msteams/src/channel.ts +0 -436
  177. package/extensions/msteams/src/conversation-store-fs.test.ts +0 -88
  178. package/extensions/msteams/src/conversation-store-fs.ts +0 -155
  179. package/extensions/msteams/src/conversation-store-memory.ts +0 -45
  180. package/extensions/msteams/src/conversation-store.ts +0 -41
  181. package/extensions/msteams/src/directory-live.ts +0 -179
  182. package/extensions/msteams/src/errors.test.ts +0 -46
  183. package/extensions/msteams/src/errors.ts +0 -158
  184. package/extensions/msteams/src/file-consent-helpers.test.ts +0 -234
  185. package/extensions/msteams/src/file-consent-helpers.ts +0 -73
  186. package/extensions/msteams/src/file-consent.ts +0 -122
  187. package/extensions/msteams/src/graph-chat.ts +0 -52
  188. package/extensions/msteams/src/graph-upload.ts +0 -445
  189. package/extensions/msteams/src/inbound.test.ts +0 -67
  190. package/extensions/msteams/src/inbound.ts +0 -38
  191. package/extensions/msteams/src/index.ts +0 -4
  192. package/extensions/msteams/src/media-helpers.test.ts +0 -186
  193. package/extensions/msteams/src/media-helpers.ts +0 -77
  194. package/extensions/msteams/src/messenger.test.ts +0 -245
  195. package/extensions/msteams/src/messenger.ts +0 -460
  196. package/extensions/msteams/src/monitor-handler/inbound-media.ts +0 -123
  197. package/extensions/msteams/src/monitor-handler/message-handler.ts +0 -629
  198. package/extensions/msteams/src/monitor-handler.ts +0 -166
  199. package/extensions/msteams/src/monitor-types.ts +0 -5
  200. package/extensions/msteams/src/monitor.ts +0 -290
  201. package/extensions/msteams/src/onboarding.ts +0 -432
  202. package/extensions/msteams/src/outbound.ts +0 -47
  203. package/extensions/msteams/src/pending-uploads.ts +0 -87
  204. package/extensions/msteams/src/policy.test.ts +0 -210
  205. package/extensions/msteams/src/policy.ts +0 -202
  206. package/extensions/msteams/src/polls-store-memory.ts +0 -30
  207. package/extensions/msteams/src/polls-store.test.ts +0 -40
  208. package/extensions/msteams/src/polls.test.ts +0 -72
  209. package/extensions/msteams/src/polls.ts +0 -299
  210. package/extensions/msteams/src/probe.test.ts +0 -57
  211. package/extensions/msteams/src/probe.ts +0 -99
  212. package/extensions/msteams/src/reply-dispatcher.ts +0 -128
  213. package/extensions/msteams/src/resolve-allowlist.ts +0 -277
  214. package/extensions/msteams/src/runtime.ts +0 -14
  215. package/extensions/msteams/src/sdk-types.ts +0 -19
  216. package/extensions/msteams/src/sdk.ts +0 -33
  217. package/extensions/msteams/src/send-context.ts +0 -156
  218. package/extensions/msteams/src/send.ts +0 -489
  219. package/extensions/msteams/src/sent-message-cache.test.ts +0 -16
  220. package/extensions/msteams/src/sent-message-cache.ts +0 -41
  221. package/extensions/msteams/src/storage.ts +0 -22
  222. package/extensions/msteams/src/store-fs.ts +0 -80
  223. package/extensions/msteams/src/token.ts +0 -19
  224. package/extensions/nextcloud-talk/clawdbot.plugin.json +0 -11
  225. package/extensions/nextcloud-talk/index.ts +0 -18
  226. package/extensions/nextcloud-talk/package.json +0 -30
  227. package/extensions/nextcloud-talk/src/accounts.ts +0 -154
  228. package/extensions/nextcloud-talk/src/channel.ts +0 -404
  229. package/extensions/nextcloud-talk/src/config-schema.ts +0 -78
  230. package/extensions/nextcloud-talk/src/format.ts +0 -79
  231. package/extensions/nextcloud-talk/src/inbound.ts +0 -336
  232. package/extensions/nextcloud-talk/src/monitor.ts +0 -246
  233. package/extensions/nextcloud-talk/src/normalize.ts +0 -31
  234. package/extensions/nextcloud-talk/src/onboarding.ts +0 -341
  235. package/extensions/nextcloud-talk/src/policy.ts +0 -175
  236. package/extensions/nextcloud-talk/src/room-info.ts +0 -111
  237. package/extensions/nextcloud-talk/src/runtime.ts +0 -14
  238. package/extensions/nextcloud-talk/src/send.ts +0 -206
  239. package/extensions/nextcloud-talk/src/signature.ts +0 -67
  240. package/extensions/nextcloud-talk/src/types.ts +0 -179
  241. package/extensions/nostr/CHANGELOG.md +0 -46
  242. package/extensions/nostr/README.md +0 -136
  243. package/extensions/nostr/clawdbot.plugin.json +0 -11
  244. package/extensions/nostr/index.ts +0 -69
  245. package/extensions/nostr/package.json +0 -31
  246. package/extensions/nostr/src/channel.test.ts +0 -141
  247. package/extensions/nostr/src/channel.ts +0 -342
  248. package/extensions/nostr/src/config-schema.ts +0 -90
  249. package/extensions/nostr/src/metrics.ts +0 -464
  250. package/extensions/nostr/src/nostr-bus.fuzz.test.ts +0 -544
  251. package/extensions/nostr/src/nostr-bus.integration.test.ts +0 -452
  252. package/extensions/nostr/src/nostr-bus.test.ts +0 -199
  253. package/extensions/nostr/src/nostr-bus.ts +0 -741
  254. package/extensions/nostr/src/nostr-profile-http.test.ts +0 -378
  255. package/extensions/nostr/src/nostr-profile-http.ts +0 -500
  256. package/extensions/nostr/src/nostr-profile-import.test.ts +0 -120
  257. package/extensions/nostr/src/nostr-profile-import.ts +0 -259
  258. package/extensions/nostr/src/nostr-profile.fuzz.test.ts +0 -479
  259. package/extensions/nostr/src/nostr-profile.test.ts +0 -410
  260. package/extensions/nostr/src/nostr-profile.ts +0 -242
  261. package/extensions/nostr/src/nostr-state-store.test.ts +0 -128
  262. package/extensions/nostr/src/nostr-state-store.ts +0 -226
  263. package/extensions/nostr/src/runtime.ts +0 -14
  264. package/extensions/nostr/src/seen-tracker.ts +0 -271
  265. package/extensions/nostr/src/types.test.ts +0 -161
  266. package/extensions/nostr/src/types.ts +0 -99
  267. package/extensions/nostr/test/setup.ts +0 -5
  268. package/extensions/open-prose/README.md +0 -25
  269. package/extensions/open-prose/clawdbot.plugin.json +0 -11
  270. package/extensions/open-prose/index.ts +0 -5
  271. package/extensions/open-prose/package.json +0 -11
  272. package/extensions/open-prose/skills/prose/LICENSE +0 -21
  273. package/extensions/open-prose/skills/prose/SKILL.md +0 -318
  274. package/extensions/open-prose/skills/prose/alt-borges.md +0 -141
  275. package/extensions/open-prose/skills/prose/alts/arabian-nights.md +0 -358
  276. package/extensions/open-prose/skills/prose/alts/borges.md +0 -360
  277. package/extensions/open-prose/skills/prose/alts/folk.md +0 -322
  278. package/extensions/open-prose/skills/prose/alts/homer.md +0 -346
  279. package/extensions/open-prose/skills/prose/alts/kafka.md +0 -373
  280. package/extensions/open-prose/skills/prose/compiler.md +0 -2967
  281. package/extensions/open-prose/skills/prose/examples/01-hello-world.prose +0 -4
  282. package/extensions/open-prose/skills/prose/examples/02-research-and-summarize.prose +0 -6
  283. package/extensions/open-prose/skills/prose/examples/03-code-review.prose +0 -17
  284. package/extensions/open-prose/skills/prose/examples/04-write-and-refine.prose +0 -14
  285. package/extensions/open-prose/skills/prose/examples/05-debug-issue.prose +0 -20
  286. package/extensions/open-prose/skills/prose/examples/06-explain-codebase.prose +0 -17
  287. package/extensions/open-prose/skills/prose/examples/07-refactor.prose +0 -20
  288. package/extensions/open-prose/skills/prose/examples/08-blog-post.prose +0 -20
  289. package/extensions/open-prose/skills/prose/examples/09-research-with-agents.prose +0 -25
  290. package/extensions/open-prose/skills/prose/examples/10-code-review-agents.prose +0 -32
  291. package/extensions/open-prose/skills/prose/examples/11-skills-and-imports.prose +0 -27
  292. package/extensions/open-prose/skills/prose/examples/12-secure-agent-permissions.prose +0 -43
  293. package/extensions/open-prose/skills/prose/examples/13-variables-and-context.prose +0 -51
  294. package/extensions/open-prose/skills/prose/examples/14-composition-blocks.prose +0 -48
  295. package/extensions/open-prose/skills/prose/examples/15-inline-sequences.prose +0 -23
  296. package/extensions/open-prose/skills/prose/examples/16-parallel-reviews.prose +0 -19
  297. package/extensions/open-prose/skills/prose/examples/17-parallel-research.prose +0 -19
  298. package/extensions/open-prose/skills/prose/examples/18-mixed-parallel-sequential.prose +0 -36
  299. package/extensions/open-prose/skills/prose/examples/19-advanced-parallel.prose +0 -71
  300. package/extensions/open-prose/skills/prose/examples/20-fixed-loops.prose +0 -20
  301. package/extensions/open-prose/skills/prose/examples/21-pipeline-operations.prose +0 -35
  302. package/extensions/open-prose/skills/prose/examples/22-error-handling.prose +0 -51
  303. package/extensions/open-prose/skills/prose/examples/23-retry-with-backoff.prose +0 -63
  304. package/extensions/open-prose/skills/prose/examples/24-choice-blocks.prose +0 -86
  305. package/extensions/open-prose/skills/prose/examples/25-conditionals.prose +0 -114
  306. package/extensions/open-prose/skills/prose/examples/26-parameterized-blocks.prose +0 -100
  307. package/extensions/open-prose/skills/prose/examples/27-string-interpolation.prose +0 -105
  308. package/extensions/open-prose/skills/prose/examples/28-automated-pr-review.prose +0 -37
  309. package/extensions/open-prose/skills/prose/examples/28-gas-town.prose +0 -1572
  310. package/extensions/open-prose/skills/prose/examples/29-captains-chair.prose +0 -218
  311. package/extensions/open-prose/skills/prose/examples/30-captains-chair-simple.prose +0 -42
  312. package/extensions/open-prose/skills/prose/examples/31-captains-chair-with-memory.prose +0 -145
  313. package/extensions/open-prose/skills/prose/examples/33-pr-review-autofix.prose +0 -168
  314. package/extensions/open-prose/skills/prose/examples/34-content-pipeline.prose +0 -204
  315. package/extensions/open-prose/skills/prose/examples/35-feature-factory.prose +0 -296
  316. package/extensions/open-prose/skills/prose/examples/36-bug-hunter.prose +0 -237
  317. package/extensions/open-prose/skills/prose/examples/37-the-forge.prose +0 -1474
  318. package/extensions/open-prose/skills/prose/examples/38-skill-scan.prose +0 -455
  319. package/extensions/open-prose/skills/prose/examples/39-architect-by-simulation.prose +0 -277
  320. package/extensions/open-prose/skills/prose/examples/40-rlm-self-refine.prose +0 -32
  321. package/extensions/open-prose/skills/prose/examples/41-rlm-divide-conquer.prose +0 -38
  322. package/extensions/open-prose/skills/prose/examples/42-rlm-filter-recurse.prose +0 -46
  323. package/extensions/open-prose/skills/prose/examples/43-rlm-pairwise.prose +0 -50
  324. package/extensions/open-prose/skills/prose/examples/44-run-endpoint-ux-test.prose +0 -261
  325. package/extensions/open-prose/skills/prose/examples/45-plugin-release.prose +0 -159
  326. package/extensions/open-prose/skills/prose/examples/45-run-endpoint-ux-test-with-remediation.prose +0 -637
  327. package/extensions/open-prose/skills/prose/examples/46-run-endpoint-ux-test-fast.prose +0 -148
  328. package/extensions/open-prose/skills/prose/examples/46-workflow-crystallizer.prose +0 -225
  329. package/extensions/open-prose/skills/prose/examples/47-language-self-improvement.prose +0 -356
  330. package/extensions/open-prose/skills/prose/examples/48-habit-miner.prose +0 -445
  331. package/extensions/open-prose/skills/prose/examples/49-prose-run-retrospective.prose +0 -210
  332. package/extensions/open-prose/skills/prose/examples/README.md +0 -391
  333. package/extensions/open-prose/skills/prose/examples/roadmap/README.md +0 -22
  334. package/extensions/open-prose/skills/prose/examples/roadmap/iterative-refinement.prose +0 -20
  335. package/extensions/open-prose/skills/prose/examples/roadmap/parallel-review.prose +0 -18
  336. package/extensions/open-prose/skills/prose/examples/roadmap/simple-pipeline.prose +0 -17
  337. package/extensions/open-prose/skills/prose/examples/roadmap/syntax/open-prose-syntax.prose +0 -223
  338. package/extensions/open-prose/skills/prose/guidance/antipatterns.md +0 -951
  339. package/extensions/open-prose/skills/prose/guidance/patterns.md +0 -700
  340. package/extensions/open-prose/skills/prose/guidance/system-prompt.md +0 -180
  341. package/extensions/open-prose/skills/prose/help.md +0 -143
  342. package/extensions/open-prose/skills/prose/lib/README.md +0 -105
  343. package/extensions/open-prose/skills/prose/lib/calibrator.prose +0 -215
  344. package/extensions/open-prose/skills/prose/lib/cost-analyzer.prose +0 -174
  345. package/extensions/open-prose/skills/prose/lib/error-forensics.prose +0 -250
  346. package/extensions/open-prose/skills/prose/lib/inspector.prose +0 -196
  347. package/extensions/open-prose/skills/prose/lib/profiler.prose +0 -460
  348. package/extensions/open-prose/skills/prose/lib/program-improver.prose +0 -275
  349. package/extensions/open-prose/skills/prose/lib/project-memory.prose +0 -118
  350. package/extensions/open-prose/skills/prose/lib/user-memory.prose +0 -93
  351. package/extensions/open-prose/skills/prose/lib/vm-improver.prose +0 -243
  352. package/extensions/open-prose/skills/prose/primitives/session.md +0 -587
  353. package/extensions/open-prose/skills/prose/prose.md +0 -1235
  354. package/extensions/open-prose/skills/prose/state/filesystem.md +0 -478
  355. package/extensions/open-prose/skills/prose/state/in-context.md +0 -380
  356. package/extensions/open-prose/skills/prose/state/postgres.md +0 -875
  357. package/extensions/open-prose/skills/prose/state/sqlite.md +0 -572
  358. package/extensions/qwen-portal-auth/README.md +0 -24
  359. package/extensions/qwen-portal-auth/clawdbot.plugin.json +0 -11
  360. package/extensions/qwen-portal-auth/index.ts +0 -127
  361. package/extensions/qwen-portal-auth/oauth.ts +0 -190
  362. package/extensions/signal/clawdbot.plugin.json +0 -11
  363. package/extensions/signal/index.ts +0 -18
  364. package/extensions/signal/package.json +0 -11
  365. package/extensions/signal/src/channel.ts +0 -312
  366. package/extensions/signal/src/runtime.ts +0 -14
  367. package/extensions/telegram/clawdbot.plugin.json +0 -11
  368. package/extensions/telegram/index.ts +0 -18
  369. package/extensions/telegram/package.json +0 -11
  370. package/extensions/telegram/src/channel.ts +0 -478
  371. package/extensions/telegram/src/runtime.ts +0 -14
  372. package/extensions/tlon/README.md +0 -5
  373. package/extensions/tlon/clawdbot.plugin.json +0 -11
  374. package/extensions/tlon/index.ts +0 -18
  375. package/extensions/tlon/package.json +0 -30
  376. package/extensions/tlon/src/channel.ts +0 -379
  377. package/extensions/tlon/src/config-schema.test.ts +0 -32
  378. package/extensions/tlon/src/config-schema.ts +0 -43
  379. package/extensions/tlon/src/monitor/discovery.ts +0 -71
  380. package/extensions/tlon/src/monitor/history.ts +0 -87
  381. package/extensions/tlon/src/monitor/index.ts +0 -501
  382. package/extensions/tlon/src/monitor/processed-messages.test.ts +0 -24
  383. package/extensions/tlon/src/monitor/processed-messages.ts +0 -38
  384. package/extensions/tlon/src/monitor/utils.ts +0 -83
  385. package/extensions/tlon/src/onboarding.ts +0 -213
  386. package/extensions/tlon/src/runtime.ts +0 -14
  387. package/extensions/tlon/src/targets.ts +0 -79
  388. package/extensions/tlon/src/types.ts +0 -85
  389. package/extensions/tlon/src/urbit/auth.ts +0 -18
  390. package/extensions/tlon/src/urbit/http-api.ts +0 -36
  391. package/extensions/tlon/src/urbit/send.test.ts +0 -38
  392. package/extensions/tlon/src/urbit/send.ts +0 -127
  393. package/extensions/tlon/src/urbit/sse-client.test.ts +0 -41
  394. package/extensions/tlon/src/urbit/sse-client.ts +0 -367
  395. package/extensions/twitch/CHANGELOG.md +0 -21
  396. package/extensions/twitch/README.md +0 -89
  397. package/extensions/twitch/clawdbot.plugin.json +0 -9
  398. package/extensions/twitch/index.ts +0 -20
  399. package/extensions/twitch/package.json +0 -20
  400. package/extensions/twitch/src/access-control.test.ts +0 -489
  401. package/extensions/twitch/src/access-control.ts +0 -154
  402. package/extensions/twitch/src/actions.ts +0 -173
  403. package/extensions/twitch/src/client-manager-registry.ts +0 -115
  404. package/extensions/twitch/src/config-schema.ts +0 -82
  405. package/extensions/twitch/src/config.test.ts +0 -88
  406. package/extensions/twitch/src/config.ts +0 -116
  407. package/extensions/twitch/src/monitor.ts +0 -257
  408. package/extensions/twitch/src/onboarding.test.ts +0 -311
  409. package/extensions/twitch/src/onboarding.ts +0 -411
  410. package/extensions/twitch/src/outbound.test.ts +0 -373
  411. package/extensions/twitch/src/outbound.ts +0 -186
  412. package/extensions/twitch/src/plugin.test.ts +0 -39
  413. package/extensions/twitch/src/plugin.ts +0 -274
  414. package/extensions/twitch/src/probe.test.ts +0 -198
  415. package/extensions/twitch/src/probe.ts +0 -118
  416. package/extensions/twitch/src/resolver.ts +0 -137
  417. package/extensions/twitch/src/runtime.ts +0 -14
  418. package/extensions/twitch/src/send.test.ts +0 -289
  419. package/extensions/twitch/src/send.ts +0 -136
  420. package/extensions/twitch/src/status.test.ts +0 -270
  421. package/extensions/twitch/src/status.ts +0 -176
  422. package/extensions/twitch/src/token.test.ts +0 -171
  423. package/extensions/twitch/src/token.ts +0 -87
  424. package/extensions/twitch/src/twitch-client.test.ts +0 -574
  425. package/extensions/twitch/src/twitch-client.ts +0 -277
  426. package/extensions/twitch/src/types.ts +0 -141
  427. package/extensions/twitch/src/utils/markdown.ts +0 -92
  428. package/extensions/twitch/src/utils/twitch.ts +0 -78
  429. package/extensions/twitch/test/setup.ts +0 -7
  430. package/extensions/voice-call/CHANGELOG.md +0 -72
  431. package/extensions/voice-call/README.md +0 -134
  432. package/extensions/voice-call/clawdbot.plugin.json +0 -601
  433. package/extensions/voice-call/index.ts +0 -497
  434. package/extensions/voice-call/package.json +0 -16
  435. package/extensions/voice-call/src/cli.ts +0 -300
  436. package/extensions/voice-call/src/config.test.ts +0 -204
  437. package/extensions/voice-call/src/config.ts +0 -493
  438. package/extensions/voice-call/src/core-bridge.ts +0 -196
  439. package/extensions/voice-call/src/manager/context.ts +0 -21
  440. package/extensions/voice-call/src/manager/events.ts +0 -177
  441. package/extensions/voice-call/src/manager/lookup.ts +0 -33
  442. package/extensions/voice-call/src/manager/outbound.ts +0 -248
  443. package/extensions/voice-call/src/manager/state.ts +0 -50
  444. package/extensions/voice-call/src/manager/store.ts +0 -88
  445. package/extensions/voice-call/src/manager/timers.ts +0 -86
  446. package/extensions/voice-call/src/manager/twiml.ts +0 -9
  447. package/extensions/voice-call/src/manager.test.ts +0 -108
  448. package/extensions/voice-call/src/manager.ts +0 -876
  449. package/extensions/voice-call/src/media-stream.test.ts +0 -97
  450. package/extensions/voice-call/src/media-stream.ts +0 -393
  451. package/extensions/voice-call/src/providers/base.ts +0 -67
  452. package/extensions/voice-call/src/providers/index.ts +0 -10
  453. package/extensions/voice-call/src/providers/mock.ts +0 -168
  454. package/extensions/voice-call/src/providers/plivo.test.ts +0 -28
  455. package/extensions/voice-call/src/providers/plivo.ts +0 -504
  456. package/extensions/voice-call/src/providers/stt-openai-realtime.ts +0 -311
  457. package/extensions/voice-call/src/providers/telnyx.ts +0 -364
  458. package/extensions/voice-call/src/providers/tts-openai.ts +0 -264
  459. package/extensions/voice-call/src/providers/twilio/api.ts +0 -45
  460. package/extensions/voice-call/src/providers/twilio/webhook.ts +0 -29
  461. package/extensions/voice-call/src/providers/twilio.test.ts +0 -64
  462. package/extensions/voice-call/src/providers/twilio.ts +0 -595
  463. package/extensions/voice-call/src/response-generator.ts +0 -171
  464. package/extensions/voice-call/src/runtime.ts +0 -205
  465. package/extensions/voice-call/src/telephony-audio.ts +0 -88
  466. package/extensions/voice-call/src/telephony-tts.ts +0 -95
  467. package/extensions/voice-call/src/tunnel.ts +0 -331
  468. package/extensions/voice-call/src/types.ts +0 -272
  469. package/extensions/voice-call/src/utils.ts +0 -12
  470. package/extensions/voice-call/src/voice-mapping.ts +0 -65
  471. package/extensions/voice-call/src/webhook-security.test.ts +0 -233
  472. package/extensions/voice-call/src/webhook-security.ts +0 -446
  473. package/extensions/voice-call/src/webhook.ts +0 -490
  474. package/extensions/whatsapp/clawdbot.plugin.json +0 -11
  475. package/extensions/whatsapp/index.ts +0 -18
  476. package/extensions/whatsapp/package.json +0 -11
  477. package/extensions/whatsapp/src/channel.ts +0 -500
  478. package/extensions/whatsapp/src/runtime.ts +0 -14
  479. package/extensions/zalo/CHANGELOG.md +0 -55
  480. package/extensions/zalo/README.md +0 -50
  481. package/extensions/zalo/clawdbot.plugin.json +0 -11
  482. package/extensions/zalo/index.ts +0 -20
  483. package/extensions/zalo/package.json +0 -33
  484. package/extensions/zalo/src/accounts.ts +0 -71
  485. package/extensions/zalo/src/actions.ts +0 -62
  486. package/extensions/zalo/src/api.ts +0 -206
  487. package/extensions/zalo/src/channel.directory.test.ts +0 -35
  488. package/extensions/zalo/src/channel.ts +0 -394
  489. package/extensions/zalo/src/config-schema.ts +0 -24
  490. package/extensions/zalo/src/monitor.ts +0 -760
  491. package/extensions/zalo/src/monitor.webhook.test.ts +0 -70
  492. package/extensions/zalo/src/onboarding.ts +0 -405
  493. package/extensions/zalo/src/probe.ts +0 -46
  494. package/extensions/zalo/src/proxy.ts +0 -18
  495. package/extensions/zalo/src/runtime.ts +0 -14
  496. package/extensions/zalo/src/send.ts +0 -117
  497. package/extensions/zalo/src/status-issues.ts +0 -50
  498. package/extensions/zalo/src/token.ts +0 -55
  499. package/extensions/zalo/src/types.ts +0 -42
  500. package/extensions/zalouser/CHANGELOG.md +0 -33
  501. package/extensions/zalouser/README.md +0 -221
  502. package/extensions/zalouser/clawdbot.plugin.json +0 -11
  503. package/extensions/zalouser/index.ts +0 -32
  504. package/extensions/zalouser/package.json +0 -33
  505. package/extensions/zalouser/src/accounts.ts +0 -117
  506. package/extensions/zalouser/src/channel.test.ts +0 -17
  507. package/extensions/zalouser/src/channel.ts +0 -641
  508. package/extensions/zalouser/src/config-schema.ts +0 -27
  509. package/extensions/zalouser/src/monitor.ts +0 -574
  510. package/extensions/zalouser/src/onboarding.ts +0 -488
  511. package/extensions/zalouser/src/probe.ts +0 -28
  512. package/extensions/zalouser/src/runtime.ts +0 -14
  513. package/extensions/zalouser/src/send.ts +0 -150
  514. package/extensions/zalouser/src/status-issues.test.ts +0 -58
  515. package/extensions/zalouser/src/status-issues.ts +0 -81
  516. package/extensions/zalouser/src/tool.ts +0 -156
  517. package/extensions/zalouser/src/types.ts +0 -102
  518. package/extensions/zalouser/src/zca.ts +0 -208
  519. package/skills/1password/SKILL.md +0 -53
  520. package/skills/1password/references/cli-examples.md +0 -29
  521. package/skills/1password/references/get-started.md +0 -17
  522. package/skills/apple-notes/SKILL.md +0 -50
  523. package/skills/apple-reminders/SKILL.md +0 -67
  524. package/skills/bear-notes/SKILL.md +0 -79
  525. package/skills/bird/SKILL.md +0 -197
  526. package/skills/blogwatcher/SKILL.md +0 -46
  527. package/skills/blucli/SKILL.md +0 -27
  528. package/skills/bluebubbles/SKILL.md +0 -39
  529. package/skills/camsnap/SKILL.md +0 -25
  530. package/skills/canvas/SKILL.md +0 -189
  531. package/skills/clawdhub/SKILL.md +0 -53
  532. package/skills/coding-agent/SKILL.md +0 -278
  533. package/skills/discord/SKILL.md +0 -475
  534. package/skills/eightctl/SKILL.md +0 -29
  535. package/skills/food-order/SKILL.md +0 -41
  536. package/skills/gemini/SKILL.md +0 -23
  537. package/skills/gifgrep/SKILL.md +0 -47
  538. package/skills/github/SKILL.md +0 -48
  539. package/skills/gog/SKILL.md +0 -92
  540. package/skills/goplaces/SKILL.md +0 -30
  541. package/skills/himalaya/SKILL.md +0 -217
  542. package/skills/himalaya/references/configuration.md +0 -174
  543. package/skills/himalaya/references/message-composition.md +0 -182
  544. package/skills/imsg/SKILL.md +0 -25
  545. package/skills/local-places/SERVER_README.md +0 -101
  546. package/skills/local-places/SKILL.md +0 -91
  547. package/skills/local-places/pyproject.toml +0 -27
  548. package/skills/local-places/src/local_places/__init__.py +0 -2
  549. package/skills/local-places/src/local_places/google_places.py +0 -314
  550. package/skills/local-places/src/local_places/main.py +0 -65
  551. package/skills/local-places/src/local_places/schemas.py +0 -107
  552. package/skills/mcporter/SKILL.md +0 -38
  553. package/skills/model-usage/SKILL.md +0 -45
  554. package/skills/model-usage/references/codexbar-cli.md +0 -28
  555. package/skills/model-usage/scripts/model_usage.py +0 -310
  556. package/skills/nano-banana-pro/SKILL.md +0 -30
  557. package/skills/nano-banana-pro/scripts/generate_image.py +0 -169
  558. package/skills/nano-pdf/SKILL.md +0 -20
  559. package/skills/notion/SKILL.md +0 -156
  560. package/skills/obsidian/SKILL.md +0 -55
  561. package/skills/openai-image-gen/SKILL.md +0 -71
  562. package/skills/openai-image-gen/scripts/gen.py +0 -240
  563. package/skills/openai-whisper/SKILL.md +0 -19
  564. package/skills/openai-whisper-api/SKILL.md +0 -43
  565. package/skills/openai-whisper-api/scripts/transcribe.sh +0 -85
  566. package/skills/openhue/SKILL.md +0 -30
  567. package/skills/oracle/SKILL.md +0 -105
  568. package/skills/ordercli/SKILL.md +0 -47
  569. package/skills/peekaboo/SKILL.md +0 -153
  570. package/skills/sag/SKILL.md +0 -62
  571. package/skills/session-logs/SKILL.md +0 -105
  572. package/skills/sherpa-onnx-tts/SKILL.md +0 -49
  573. package/skills/sherpa-onnx-tts/bin/sherpa-onnx-tts +0 -178
  574. package/skills/skill-creator/SKILL.md +0 -371
  575. package/skills/skill-creator/license.txt +0 -202
  576. package/skills/skill-creator/scripts/init_skill.py +0 -378
  577. package/skills/skill-creator/scripts/package_skill.py +0 -111
  578. package/skills/skill-creator/scripts/quick_validate.py +0 -101
  579. package/skills/slack/SKILL.md +0 -144
  580. package/skills/songsee/SKILL.md +0 -29
  581. package/skills/sonoscli/SKILL.md +0 -26
  582. package/skills/spotify-player/SKILL.md +0 -34
  583. package/skills/summarize/SKILL.md +0 -67
  584. package/skills/things-mac/SKILL.md +0 -61
  585. package/skills/tmux/SKILL.md +0 -121
  586. package/skills/tmux/scripts/find-sessions.sh +0 -112
  587. package/skills/tmux/scripts/wait-for-text.sh +0 -83
  588. package/skills/trello/SKILL.md +0 -84
  589. package/skills/video-frames/SKILL.md +0 -29
  590. package/skills/video-frames/scripts/frame.sh +0 -81
  591. package/skills/voice-call/SKILL.md +0 -35
  592. package/skills/wacli/SKILL.md +0 -42
  593. package/skills/weather/SKILL.md +0 -49
@@ -1,2101 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
-
3
- import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
4
- import {
5
- logAckFailure,
6
- logInboundDrop,
7
- logTypingFailure,
8
- resolveAckReaction,
9
- resolveControlCommandGate,
10
- } from "clawdbot/plugin-sdk";
11
- import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js";
12
- import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
13
- import { downloadBlueBubblesAttachment } from "./attachments.js";
14
- import { formatBlueBubblesChatTarget, isAllowedBlueBubblesSender, normalizeBlueBubblesHandle } from "./targets.js";
15
- import { sendBlueBubblesMedia } from "./media-send.js";
16
- import type { BlueBubblesAccountConfig, BlueBubblesAttachment } from "./types.js";
17
- import type { ResolvedBlueBubblesAccount } from "./accounts.js";
18
- import { getBlueBubblesRuntime } from "./runtime.js";
19
- import { normalizeBlueBubblesReactionInput, sendBlueBubblesReaction } from "./reactions.js";
20
- import { fetchBlueBubblesServerInfo } from "./probe.js";
21
-
22
- export type BlueBubblesRuntimeEnv = {
23
- log?: (message: string) => void;
24
- error?: (message: string) => void;
25
- };
26
-
27
- export type BlueBubblesMonitorOptions = {
28
- account: ResolvedBlueBubblesAccount;
29
- config: ClawdbotConfig;
30
- runtime: BlueBubblesRuntimeEnv;
31
- abortSignal: AbortSignal;
32
- statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
33
- webhookPath?: string;
34
- };
35
-
36
- const DEFAULT_WEBHOOK_PATH = "/bluebubbles-webhook";
37
- const DEFAULT_TEXT_LIMIT = 4000;
38
- const invalidAckReactions = new Set<string>();
39
-
40
- const REPLY_CACHE_MAX = 2000;
41
- const REPLY_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
42
-
43
- type BlueBubblesReplyCacheEntry = {
44
- accountId: string;
45
- messageId: string;
46
- shortId: string;
47
- chatGuid?: string;
48
- chatIdentifier?: string;
49
- chatId?: number;
50
- senderLabel?: string;
51
- body?: string;
52
- timestamp: number;
53
- };
54
-
55
- // Best-effort cache for resolving reply context when BlueBubbles webhooks omit sender/body.
56
- const blueBubblesReplyCacheByMessageId = new Map<string, BlueBubblesReplyCacheEntry>();
57
-
58
- // Bidirectional maps for short ID ↔ message GUID resolution (token savings optimization)
59
- const blueBubblesShortIdToUuid = new Map<string, string>();
60
- const blueBubblesUuidToShortId = new Map<string, string>();
61
- let blueBubblesShortIdCounter = 0;
62
-
63
- function trimOrUndefined(value?: string | null): string | undefined {
64
- const trimmed = value?.trim();
65
- return trimmed ? trimmed : undefined;
66
- }
67
-
68
- function generateShortId(): string {
69
- blueBubblesShortIdCounter += 1;
70
- return String(blueBubblesShortIdCounter);
71
- }
72
-
73
- function rememberBlueBubblesReplyCache(
74
- entry: Omit<BlueBubblesReplyCacheEntry, "shortId">,
75
- ): BlueBubblesReplyCacheEntry {
76
- const messageId = entry.messageId.trim();
77
- if (!messageId) {
78
- return { ...entry, shortId: "" };
79
- }
80
-
81
- // Check if we already have a short ID for this GUID
82
- let shortId = blueBubblesUuidToShortId.get(messageId);
83
- if (!shortId) {
84
- shortId = generateShortId();
85
- blueBubblesShortIdToUuid.set(shortId, messageId);
86
- blueBubblesUuidToShortId.set(messageId, shortId);
87
- }
88
-
89
- const fullEntry: BlueBubblesReplyCacheEntry = { ...entry, messageId, shortId };
90
-
91
- // Refresh insertion order.
92
- blueBubblesReplyCacheByMessageId.delete(messageId);
93
- blueBubblesReplyCacheByMessageId.set(messageId, fullEntry);
94
-
95
- // Opportunistic prune.
96
- const cutoff = Date.now() - REPLY_CACHE_TTL_MS;
97
- for (const [key, value] of blueBubblesReplyCacheByMessageId) {
98
- if (value.timestamp < cutoff) {
99
- blueBubblesReplyCacheByMessageId.delete(key);
100
- // Clean up short ID mappings for expired entries
101
- if (value.shortId) {
102
- blueBubblesShortIdToUuid.delete(value.shortId);
103
- blueBubblesUuidToShortId.delete(key);
104
- }
105
- continue;
106
- }
107
- break;
108
- }
109
- while (blueBubblesReplyCacheByMessageId.size > REPLY_CACHE_MAX) {
110
- const oldest = blueBubblesReplyCacheByMessageId.keys().next().value as string | undefined;
111
- if (!oldest) break;
112
- const oldEntry = blueBubblesReplyCacheByMessageId.get(oldest);
113
- blueBubblesReplyCacheByMessageId.delete(oldest);
114
- // Clean up short ID mappings for evicted entries
115
- if (oldEntry?.shortId) {
116
- blueBubblesShortIdToUuid.delete(oldEntry.shortId);
117
- blueBubblesUuidToShortId.delete(oldest);
118
- }
119
- }
120
-
121
- return fullEntry;
122
- }
123
-
124
- /**
125
- * Resolves a short message ID (e.g., "1", "2") to a full BlueBubbles GUID.
126
- * Returns the input unchanged if it's already a GUID or not found in the mapping.
127
- */
128
- export function resolveBlueBubblesMessageId(
129
- shortOrUuid: string,
130
- opts?: { requireKnownShortId?: boolean },
131
- ): string {
132
- const trimmed = shortOrUuid.trim();
133
- if (!trimmed) return trimmed;
134
-
135
- // If it looks like a short ID (numeric), try to resolve it
136
- if (/^\d+$/.test(trimmed)) {
137
- const uuid = blueBubblesShortIdToUuid.get(trimmed);
138
- if (uuid) return uuid;
139
- if (opts?.requireKnownShortId) {
140
- throw new Error(
141
- `BlueBubbles short message id "${trimmed}" is no longer available. Use MessageSidFull.`,
142
- );
143
- }
144
- }
145
-
146
- // Return as-is (either already a UUID or not found)
147
- return trimmed;
148
- }
149
-
150
- /**
151
- * Resets the short ID state. Only use in tests.
152
- * @internal
153
- */
154
- export function _resetBlueBubblesShortIdState(): void {
155
- blueBubblesShortIdToUuid.clear();
156
- blueBubblesUuidToShortId.clear();
157
- blueBubblesReplyCacheByMessageId.clear();
158
- blueBubblesShortIdCounter = 0;
159
- }
160
-
161
- /**
162
- * Gets the short ID for a message GUID, if one exists.
163
- */
164
- function getShortIdForUuid(uuid: string): string | undefined {
165
- return blueBubblesUuidToShortId.get(uuid.trim());
166
- }
167
-
168
- function resolveReplyContextFromCache(params: {
169
- accountId: string;
170
- replyToId: string;
171
- chatGuid?: string;
172
- chatIdentifier?: string;
173
- chatId?: number;
174
- }): BlueBubblesReplyCacheEntry | null {
175
- const replyToId = params.replyToId.trim();
176
- if (!replyToId) return null;
177
-
178
- const cached = blueBubblesReplyCacheByMessageId.get(replyToId);
179
- if (!cached) return null;
180
- if (cached.accountId !== params.accountId) return null;
181
-
182
- const cutoff = Date.now() - REPLY_CACHE_TTL_MS;
183
- if (cached.timestamp < cutoff) {
184
- blueBubblesReplyCacheByMessageId.delete(replyToId);
185
- return null;
186
- }
187
-
188
- const chatGuid = trimOrUndefined(params.chatGuid);
189
- const chatIdentifier = trimOrUndefined(params.chatIdentifier);
190
- const cachedChatGuid = trimOrUndefined(cached.chatGuid);
191
- const cachedChatIdentifier = trimOrUndefined(cached.chatIdentifier);
192
- const chatId = typeof params.chatId === "number" ? params.chatId : undefined;
193
- const cachedChatId = typeof cached.chatId === "number" ? cached.chatId : undefined;
194
-
195
- // Avoid cross-chat collisions if we have identifiers.
196
- if (chatGuid && cachedChatGuid && chatGuid !== cachedChatGuid) return null;
197
- if (!chatGuid && chatIdentifier && cachedChatIdentifier && chatIdentifier !== cachedChatIdentifier) {
198
- return null;
199
- }
200
- if (!chatGuid && !chatIdentifier && chatId && cachedChatId && chatId !== cachedChatId) {
201
- return null;
202
- }
203
-
204
- return cached;
205
- }
206
-
207
- type BlueBubblesCoreRuntime = ReturnType<typeof getBlueBubblesRuntime>;
208
-
209
- function logVerbose(core: BlueBubblesCoreRuntime, runtime: BlueBubblesRuntimeEnv, message: string): void {
210
- if (core.logging.shouldLogVerbose()) {
211
- runtime.log?.(`[bluebubbles] ${message}`);
212
- }
213
- }
214
-
215
- function logGroupAllowlistHint(params: {
216
- runtime: BlueBubblesRuntimeEnv;
217
- reason: string;
218
- entry: string | null;
219
- chatName?: string;
220
- accountId?: string;
221
- }): void {
222
- const log = params.runtime.log ?? console.log;
223
- const nameHint = params.chatName ? ` (group name: ${params.chatName})` : "";
224
- const accountHint = params.accountId
225
- ? ` (or channels.bluebubbles.accounts.${params.accountId}.groupAllowFrom)`
226
- : "";
227
- if (params.entry) {
228
- log(
229
- `[bluebubbles] group message blocked (${params.reason}). Allow this group by adding ` +
230
- `"${params.entry}" to channels.bluebubbles.groupAllowFrom${nameHint}.`,
231
- );
232
- log(
233
- `[bluebubbles] add to config: channels.bluebubbles.groupAllowFrom=["${params.entry}"]${accountHint}.`,
234
- );
235
- return;
236
- }
237
- log(
238
- `[bluebubbles] group message blocked (${params.reason}). Allow groups by setting ` +
239
- `channels.bluebubbles.groupPolicy="open" or adding a group id to ` +
240
- `channels.bluebubbles.groupAllowFrom${accountHint}${nameHint}.`,
241
- );
242
- }
243
-
244
- type WebhookTarget = {
245
- account: ResolvedBlueBubblesAccount;
246
- config: ClawdbotConfig;
247
- runtime: BlueBubblesRuntimeEnv;
248
- core: BlueBubblesCoreRuntime;
249
- path: string;
250
- statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
251
- };
252
-
253
- const webhookTargets = new Map<string, WebhookTarget[]>();
254
-
255
- function normalizeWebhookPath(raw: string): string {
256
- const trimmed = raw.trim();
257
- if (!trimmed) return "/";
258
- const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
259
- if (withSlash.length > 1 && withSlash.endsWith("/")) {
260
- return withSlash.slice(0, -1);
261
- }
262
- return withSlash;
263
- }
264
-
265
- export function registerBlueBubblesWebhookTarget(target: WebhookTarget): () => void {
266
- const key = normalizeWebhookPath(target.path);
267
- const normalizedTarget = { ...target, path: key };
268
- const existing = webhookTargets.get(key) ?? [];
269
- const next = [...existing, normalizedTarget];
270
- webhookTargets.set(key, next);
271
- return () => {
272
- const updated = (webhookTargets.get(key) ?? []).filter((entry) => entry !== normalizedTarget);
273
- if (updated.length > 0) {
274
- webhookTargets.set(key, updated);
275
- } else {
276
- webhookTargets.delete(key);
277
- }
278
- };
279
- }
280
-
281
- async function readJsonBody(req: IncomingMessage, maxBytes: number) {
282
- const chunks: Buffer[] = [];
283
- let total = 0;
284
- return await new Promise<{ ok: boolean; value?: unknown; error?: string }>((resolve) => {
285
- req.on("data", (chunk: Buffer) => {
286
- total += chunk.length;
287
- if (total > maxBytes) {
288
- resolve({ ok: false, error: "payload too large" });
289
- req.destroy();
290
- return;
291
- }
292
- chunks.push(chunk);
293
- });
294
- req.on("end", () => {
295
- try {
296
- const raw = Buffer.concat(chunks).toString("utf8");
297
- if (!raw.trim()) {
298
- resolve({ ok: false, error: "empty payload" });
299
- return;
300
- }
301
- try {
302
- resolve({ ok: true, value: JSON.parse(raw) as unknown });
303
- return;
304
- } catch {
305
- const params = new URLSearchParams(raw);
306
- const payload = params.get("payload") ?? params.get("data") ?? params.get("message");
307
- if (payload) {
308
- resolve({ ok: true, value: JSON.parse(payload) as unknown });
309
- return;
310
- }
311
- throw new Error("invalid json");
312
- }
313
- } catch (err) {
314
- resolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
315
- }
316
- });
317
- req.on("error", (err) => {
318
- resolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
319
- });
320
- });
321
- }
322
-
323
- function asRecord(value: unknown): Record<string, unknown> | null {
324
- return value && typeof value === "object" && !Array.isArray(value)
325
- ? (value as Record<string, unknown>)
326
- : null;
327
- }
328
-
329
- function readString(record: Record<string, unknown> | null, key: string): string | undefined {
330
- if (!record) return undefined;
331
- const value = record[key];
332
- return typeof value === "string" ? value : undefined;
333
- }
334
-
335
- function readNumber(record: Record<string, unknown> | null, key: string): number | undefined {
336
- if (!record) return undefined;
337
- const value = record[key];
338
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
339
- }
340
-
341
- function readBoolean(record: Record<string, unknown> | null, key: string): boolean | undefined {
342
- if (!record) return undefined;
343
- const value = record[key];
344
- return typeof value === "boolean" ? value : undefined;
345
- }
346
-
347
- function extractAttachments(message: Record<string, unknown>): BlueBubblesAttachment[] {
348
- const raw = message["attachments"];
349
- if (!Array.isArray(raw)) return [];
350
- const out: BlueBubblesAttachment[] = [];
351
- for (const entry of raw) {
352
- const record = asRecord(entry);
353
- if (!record) continue;
354
- out.push({
355
- guid: readString(record, "guid"),
356
- uti: readString(record, "uti"),
357
- mimeType: readString(record, "mimeType") ?? readString(record, "mime_type"),
358
- transferName: readString(record, "transferName") ?? readString(record, "transfer_name"),
359
- totalBytes: readNumberLike(record, "totalBytes") ?? readNumberLike(record, "total_bytes"),
360
- height: readNumberLike(record, "height"),
361
- width: readNumberLike(record, "width"),
362
- originalROWID: readNumberLike(record, "originalROWID") ?? readNumberLike(record, "rowid"),
363
- });
364
- }
365
- return out;
366
- }
367
-
368
- function buildAttachmentPlaceholder(attachments: BlueBubblesAttachment[]): string {
369
- if (attachments.length === 0) return "";
370
- const mimeTypes = attachments.map((entry) => entry.mimeType ?? "");
371
- const allImages = mimeTypes.every((entry) => entry.startsWith("image/"));
372
- const allVideos = mimeTypes.every((entry) => entry.startsWith("video/"));
373
- const allAudio = mimeTypes.every((entry) => entry.startsWith("audio/"));
374
- const tag = allImages
375
- ? "<media:image>"
376
- : allVideos
377
- ? "<media:video>"
378
- : allAudio
379
- ? "<media:audio>"
380
- : "<media:attachment>";
381
- const label = allImages ? "image" : allVideos ? "video" : allAudio ? "audio" : "file";
382
- const suffix = attachments.length === 1 ? label : `${label}s`;
383
- return `${tag} (${attachments.length} ${suffix})`;
384
- }
385
-
386
- function buildMessagePlaceholder(message: NormalizedWebhookMessage): string {
387
- const attachmentPlaceholder = buildAttachmentPlaceholder(message.attachments ?? []);
388
- if (attachmentPlaceholder) return attachmentPlaceholder;
389
- if (message.balloonBundleId) return "<media:sticker>";
390
- return "";
391
- }
392
-
393
- // Returns inline reply tag like "[[reply_to:4]]" for prepending to message body
394
- function formatReplyTag(message: {
395
- replyToId?: string;
396
- replyToShortId?: string;
397
- }): string | null {
398
- // Prefer short ID
399
- const rawId = message.replyToShortId || message.replyToId;
400
- if (!rawId) return null;
401
- return `[[reply_to:${rawId}]]`;
402
- }
403
-
404
- function readNumberLike(record: Record<string, unknown> | null, key: string): number | undefined {
405
- if (!record) return undefined;
406
- const value = record[key];
407
- if (typeof value === "number" && Number.isFinite(value)) return value;
408
- if (typeof value === "string") {
409
- const parsed = Number.parseFloat(value);
410
- if (Number.isFinite(parsed)) return parsed;
411
- }
412
- return undefined;
413
- }
414
-
415
- function extractReplyMetadata(message: Record<string, unknown>): {
416
- replyToId?: string;
417
- replyToBody?: string;
418
- replyToSender?: string;
419
- } {
420
- const replyRaw =
421
- message["replyTo"] ??
422
- message["reply_to"] ??
423
- message["replyToMessage"] ??
424
- message["reply_to_message"] ??
425
- message["repliedMessage"] ??
426
- message["quotedMessage"] ??
427
- message["associatedMessage"] ??
428
- message["reply"];
429
- const replyRecord = asRecord(replyRaw);
430
- const replyHandle = asRecord(replyRecord?.["handle"]) ?? asRecord(replyRecord?.["sender"]) ?? null;
431
- const replySenderRaw =
432
- readString(replyHandle, "address") ??
433
- readString(replyHandle, "handle") ??
434
- readString(replyHandle, "id") ??
435
- readString(replyRecord, "senderId") ??
436
- readString(replyRecord, "sender") ??
437
- readString(replyRecord, "from");
438
- const normalizedSender = replySenderRaw
439
- ? normalizeBlueBubblesHandle(replySenderRaw) || replySenderRaw.trim()
440
- : undefined;
441
-
442
- const replyToBody =
443
- readString(replyRecord, "text") ??
444
- readString(replyRecord, "body") ??
445
- readString(replyRecord, "message") ??
446
- readString(replyRecord, "subject") ??
447
- undefined;
448
-
449
- const directReplyId =
450
- readString(message, "replyToMessageGuid") ??
451
- readString(message, "replyToGuid") ??
452
- readString(message, "replyGuid") ??
453
- readString(message, "selectedMessageGuid") ??
454
- readString(message, "selectedMessageId") ??
455
- readString(message, "replyToMessageId") ??
456
- readString(message, "replyId") ??
457
- readString(replyRecord, "guid") ??
458
- readString(replyRecord, "id") ??
459
- readString(replyRecord, "messageId");
460
-
461
- const associatedType =
462
- readNumberLike(message, "associatedMessageType") ??
463
- readNumberLike(message, "associated_message_type");
464
- const associatedGuid =
465
- readString(message, "associatedMessageGuid") ??
466
- readString(message, "associated_message_guid") ??
467
- readString(message, "associatedMessageId");
468
- const isReactionAssociation =
469
- typeof associatedType === "number" && REACTION_TYPE_MAP.has(associatedType);
470
-
471
- const replyToId = directReplyId ?? (!isReactionAssociation ? associatedGuid : undefined);
472
- const threadOriginatorGuid = readString(message, "threadOriginatorGuid");
473
- const messageGuid = readString(message, "guid");
474
- const fallbackReplyId =
475
- !replyToId && threadOriginatorGuid && threadOriginatorGuid !== messageGuid
476
- ? threadOriginatorGuid
477
- : undefined;
478
-
479
- return {
480
- replyToId: (replyToId ?? fallbackReplyId)?.trim() || undefined,
481
- replyToBody: replyToBody?.trim() || undefined,
482
- replyToSender: normalizedSender || undefined,
483
- };
484
- }
485
-
486
- function readFirstChatRecord(message: Record<string, unknown>): Record<string, unknown> | null {
487
- const chats = message["chats"];
488
- if (!Array.isArray(chats) || chats.length === 0) return null;
489
- const first = chats[0];
490
- return asRecord(first);
491
- }
492
-
493
- function normalizeParticipantEntry(entry: unknown): BlueBubblesParticipant | null {
494
- if (typeof entry === "string" || typeof entry === "number") {
495
- const raw = String(entry).trim();
496
- if (!raw) return null;
497
- const normalized = normalizeBlueBubblesHandle(raw) || raw;
498
- return normalized ? { id: normalized } : null;
499
- }
500
- const record = asRecord(entry);
501
- if (!record) return null;
502
- const nestedHandle =
503
- asRecord(record["handle"]) ?? asRecord(record["sender"]) ?? asRecord(record["contact"]) ?? null;
504
- const idRaw =
505
- readString(record, "address") ??
506
- readString(record, "handle") ??
507
- readString(record, "id") ??
508
- readString(record, "phoneNumber") ??
509
- readString(record, "phone_number") ??
510
- readString(record, "email") ??
511
- readString(nestedHandle, "address") ??
512
- readString(nestedHandle, "handle") ??
513
- readString(nestedHandle, "id");
514
- const nameRaw =
515
- readString(record, "displayName") ??
516
- readString(record, "name") ??
517
- readString(record, "title") ??
518
- readString(nestedHandle, "displayName") ??
519
- readString(nestedHandle, "name");
520
- const normalizedId = idRaw ? normalizeBlueBubblesHandle(idRaw) || idRaw.trim() : "";
521
- if (!normalizedId) return null;
522
- const name = nameRaw?.trim() || undefined;
523
- return { id: normalizedId, name };
524
- }
525
-
526
- function normalizeParticipantList(raw: unknown): BlueBubblesParticipant[] {
527
- if (!Array.isArray(raw) || raw.length === 0) return [];
528
- const seen = new Set<string>();
529
- const output: BlueBubblesParticipant[] = [];
530
- for (const entry of raw) {
531
- const normalized = normalizeParticipantEntry(entry);
532
- if (!normalized?.id) continue;
533
- const key = normalized.id.toLowerCase();
534
- if (seen.has(key)) continue;
535
- seen.add(key);
536
- output.push(normalized);
537
- }
538
- return output;
539
- }
540
-
541
- function formatGroupMembers(params: {
542
- participants?: BlueBubblesParticipant[];
543
- fallback?: BlueBubblesParticipant;
544
- }): string | undefined {
545
- const seen = new Set<string>();
546
- const ordered: BlueBubblesParticipant[] = [];
547
- for (const entry of params.participants ?? []) {
548
- if (!entry?.id) continue;
549
- const key = entry.id.toLowerCase();
550
- if (seen.has(key)) continue;
551
- seen.add(key);
552
- ordered.push(entry);
553
- }
554
- if (ordered.length === 0 && params.fallback?.id) {
555
- ordered.push(params.fallback);
556
- }
557
- if (ordered.length === 0) return undefined;
558
- return ordered
559
- .map((entry) => (entry.name ? `${entry.name} (${entry.id})` : entry.id))
560
- .join(", ");
561
- }
562
-
563
- function resolveGroupFlagFromChatGuid(chatGuid?: string | null): boolean | undefined {
564
- const guid = chatGuid?.trim();
565
- if (!guid) return undefined;
566
- const parts = guid.split(";");
567
- if (parts.length >= 3) {
568
- if (parts[1] === "+") return true;
569
- if (parts[1] === "-") return false;
570
- }
571
- if (guid.includes(";+;")) return true;
572
- if (guid.includes(";-;")) return false;
573
- return undefined;
574
- }
575
-
576
- function extractChatIdentifierFromChatGuid(chatGuid?: string | null): string | undefined {
577
- const guid = chatGuid?.trim();
578
- if (!guid) return undefined;
579
- const parts = guid.split(";");
580
- if (parts.length < 3) return undefined;
581
- const identifier = parts[2]?.trim();
582
- return identifier || undefined;
583
- }
584
-
585
- function formatGroupAllowlistEntry(params: {
586
- chatGuid?: string;
587
- chatId?: number;
588
- chatIdentifier?: string;
589
- }): string | null {
590
- const guid = params.chatGuid?.trim();
591
- if (guid) return `chat_guid:${guid}`;
592
- const chatId = params.chatId;
593
- if (typeof chatId === "number" && Number.isFinite(chatId)) return `chat_id:${chatId}`;
594
- const identifier = params.chatIdentifier?.trim();
595
- if (identifier) return `chat_identifier:${identifier}`;
596
- return null;
597
- }
598
-
599
- type BlueBubblesParticipant = {
600
- id: string;
601
- name?: string;
602
- };
603
-
604
- type NormalizedWebhookMessage = {
605
- text: string;
606
- senderId: string;
607
- senderName?: string;
608
- messageId?: string;
609
- timestamp?: number;
610
- isGroup: boolean;
611
- chatId?: number;
612
- chatGuid?: string;
613
- chatIdentifier?: string;
614
- chatName?: string;
615
- fromMe?: boolean;
616
- attachments?: BlueBubblesAttachment[];
617
- balloonBundleId?: string;
618
- associatedMessageGuid?: string;
619
- associatedMessageType?: number;
620
- associatedMessageEmoji?: string;
621
- isTapback?: boolean;
622
- participants?: BlueBubblesParticipant[];
623
- replyToId?: string;
624
- replyToBody?: string;
625
- replyToSender?: string;
626
- };
627
-
628
- type NormalizedWebhookReaction = {
629
- action: "added" | "removed";
630
- emoji: string;
631
- senderId: string;
632
- senderName?: string;
633
- messageId: string;
634
- timestamp?: number;
635
- isGroup: boolean;
636
- chatId?: number;
637
- chatGuid?: string;
638
- chatIdentifier?: string;
639
- chatName?: string;
640
- fromMe?: boolean;
641
- };
642
-
643
- const REACTION_TYPE_MAP = new Map<number, { emoji: string; action: "added" | "removed" }>([
644
- [2000, { emoji: "❤️", action: "added" }],
645
- [2001, { emoji: "👍", action: "added" }],
646
- [2002, { emoji: "👎", action: "added" }],
647
- [2003, { emoji: "😂", action: "added" }],
648
- [2004, { emoji: "‼️", action: "added" }],
649
- [2005, { emoji: "❓", action: "added" }],
650
- [3000, { emoji: "❤️", action: "removed" }],
651
- [3001, { emoji: "👍", action: "removed" }],
652
- [3002, { emoji: "👎", action: "removed" }],
653
- [3003, { emoji: "😂", action: "removed" }],
654
- [3004, { emoji: "‼️", action: "removed" }],
655
- [3005, { emoji: "❓", action: "removed" }],
656
- ]);
657
-
658
- // Maps tapback text patterns (e.g., "Loved", "Liked") to emoji + action
659
- const TAPBACK_TEXT_MAP = new Map<string, { emoji: string; action: "added" | "removed" }>([
660
- ["loved", { emoji: "❤️", action: "added" }],
661
- ["liked", { emoji: "👍", action: "added" }],
662
- ["disliked", { emoji: "👎", action: "added" }],
663
- ["laughed at", { emoji: "😂", action: "added" }],
664
- ["emphasized", { emoji: "‼️", action: "added" }],
665
- ["questioned", { emoji: "❓", action: "added" }],
666
- // Removal patterns (e.g., "Removed a heart from")
667
- ["removed a heart from", { emoji: "❤️", action: "removed" }],
668
- ["removed a like from", { emoji: "👍", action: "removed" }],
669
- ["removed a dislike from", { emoji: "👎", action: "removed" }],
670
- ["removed a laugh from", { emoji: "😂", action: "removed" }],
671
- ["removed an emphasis from", { emoji: "‼️", action: "removed" }],
672
- ["removed a question from", { emoji: "❓", action: "removed" }],
673
- ]);
674
-
675
- const TAPBACK_EMOJI_REGEX =
676
- /(?:\p{Regional_Indicator}{2})|(?:[0-9#*]\uFE0F?\u20E3)|(?:\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?(?:\p{Emoji_Modifier})?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?(?:\p{Emoji_Modifier})?)*)/u;
677
-
678
- function extractFirstEmoji(text: string): string | null {
679
- const match = text.match(TAPBACK_EMOJI_REGEX);
680
- return match ? match[0] : null;
681
- }
682
-
683
- function extractQuotedTapbackText(text: string): string | null {
684
- const match = text.match(/[“"]([^”"]+)[”"]/s);
685
- return match ? match[1] : null;
686
- }
687
-
688
- function isTapbackAssociatedType(type: number | undefined): boolean {
689
- return typeof type === "number" && Number.isFinite(type) && type >= 2000 && type < 4000;
690
- }
691
-
692
- function resolveTapbackActionHint(type: number | undefined): "added" | "removed" | undefined {
693
- if (typeof type !== "number" || !Number.isFinite(type)) return undefined;
694
- if (type >= 3000 && type < 4000) return "removed";
695
- if (type >= 2000 && type < 3000) return "added";
696
- return undefined;
697
- }
698
-
699
- function resolveTapbackContext(message: NormalizedWebhookMessage): {
700
- emojiHint?: string;
701
- actionHint?: "added" | "removed";
702
- replyToId?: string;
703
- } | null {
704
- const associatedType = message.associatedMessageType;
705
- const hasTapbackType = isTapbackAssociatedType(associatedType);
706
- const hasTapbackMarker = Boolean(message.associatedMessageEmoji) || Boolean(message.isTapback);
707
- if (!hasTapbackType && !hasTapbackMarker) return null;
708
- const replyToId = message.associatedMessageGuid?.trim() || message.replyToId?.trim() || undefined;
709
- const actionHint = resolveTapbackActionHint(associatedType);
710
- const emojiHint =
711
- message.associatedMessageEmoji?.trim() || REACTION_TYPE_MAP.get(associatedType ?? -1)?.emoji;
712
- return { emojiHint, actionHint, replyToId };
713
- }
714
-
715
- // Detects tapback text patterns like 'Loved "message"' and converts to structured format
716
- function parseTapbackText(params: {
717
- text: string;
718
- emojiHint?: string;
719
- actionHint?: "added" | "removed";
720
- requireQuoted?: boolean;
721
- }): {
722
- emoji: string;
723
- action: "added" | "removed";
724
- quotedText: string;
725
- } | null {
726
- const trimmed = params.text.trim();
727
- const lower = trimmed.toLowerCase();
728
- if (!trimmed) return null;
729
-
730
- for (const [pattern, { emoji, action }] of TAPBACK_TEXT_MAP) {
731
- if (lower.startsWith(pattern)) {
732
- // Extract quoted text if present (e.g., 'Loved "hello"' -> "hello")
733
- const afterPattern = trimmed.slice(pattern.length).trim();
734
- if (params.requireQuoted) {
735
- const strictMatch = afterPattern.match(/^[“"](.+)[”"]$/s);
736
- if (!strictMatch) return null;
737
- return { emoji, action, quotedText: strictMatch[1] };
738
- }
739
- const quotedText =
740
- extractQuotedTapbackText(afterPattern) ?? extractQuotedTapbackText(trimmed) ?? afterPattern;
741
- return { emoji, action, quotedText };
742
- }
743
- }
744
-
745
- if (lower.startsWith("reacted")) {
746
- const emoji = extractFirstEmoji(trimmed) ?? params.emojiHint;
747
- if (!emoji) return null;
748
- const quotedText = extractQuotedTapbackText(trimmed);
749
- if (params.requireQuoted && !quotedText) return null;
750
- const fallback = trimmed.slice("reacted".length).trim();
751
- return { emoji, action: params.actionHint ?? "added", quotedText: quotedText ?? fallback };
752
- }
753
-
754
- if (lower.startsWith("removed")) {
755
- const emoji = extractFirstEmoji(trimmed) ?? params.emojiHint;
756
- if (!emoji) return null;
757
- const quotedText = extractQuotedTapbackText(trimmed);
758
- if (params.requireQuoted && !quotedText) return null;
759
- const fallback = trimmed.slice("removed".length).trim();
760
- return { emoji, action: params.actionHint ?? "removed", quotedText: quotedText ?? fallback };
761
- }
762
- return null;
763
- }
764
-
765
- function maskSecret(value: string): string {
766
- if (value.length <= 6) return "***";
767
- return `${value.slice(0, 2)}***${value.slice(-2)}`;
768
- }
769
-
770
- function resolveBlueBubblesAckReaction(params: {
771
- cfg: ClawdbotConfig;
772
- agentId: string;
773
- core: BlueBubblesCoreRuntime;
774
- runtime: BlueBubblesRuntimeEnv;
775
- }): string | null {
776
- const raw = resolveAckReaction(params.cfg, params.agentId).trim();
777
- if (!raw) return null;
778
- try {
779
- normalizeBlueBubblesReactionInput(raw);
780
- return raw;
781
- } catch {
782
- const key = raw.toLowerCase();
783
- if (!invalidAckReactions.has(key)) {
784
- invalidAckReactions.add(key);
785
- logVerbose(
786
- params.core,
787
- params.runtime,
788
- `ack reaction skipped (unsupported for BlueBubbles): ${raw}`,
789
- );
790
- }
791
- return null;
792
- }
793
- }
794
-
795
- function extractMessagePayload(payload: Record<string, unknown>): Record<string, unknown> | null {
796
- const dataRaw = payload.data ?? payload.payload ?? payload.event;
797
- const data =
798
- asRecord(dataRaw) ??
799
- (typeof dataRaw === "string" ? (asRecord(JSON.parse(dataRaw)) ?? null) : null);
800
- const messageRaw = payload.message ?? data?.message ?? data;
801
- const message =
802
- asRecord(messageRaw) ??
803
- (typeof messageRaw === "string" ? (asRecord(JSON.parse(messageRaw)) ?? null) : null);
804
- if (!message) return null;
805
- return message;
806
- }
807
-
808
- function normalizeWebhookMessage(payload: Record<string, unknown>): NormalizedWebhookMessage | null {
809
- const message = extractMessagePayload(payload);
810
- if (!message) return null;
811
-
812
- const text =
813
- readString(message, "text") ??
814
- readString(message, "body") ??
815
- readString(message, "subject") ??
816
- "";
817
-
818
- const handleValue = message.handle ?? message.sender;
819
- const handle =
820
- asRecord(handleValue) ??
821
- (typeof handleValue === "string" ? { address: handleValue } : null);
822
- const senderId =
823
- readString(handle, "address") ??
824
- readString(handle, "handle") ??
825
- readString(handle, "id") ??
826
- readString(message, "senderId") ??
827
- readString(message, "sender") ??
828
- readString(message, "from") ??
829
- "";
830
-
831
- const senderName =
832
- readString(handle, "displayName") ??
833
- readString(handle, "name") ??
834
- readString(message, "senderName") ??
835
- undefined;
836
-
837
- const chat = asRecord(message.chat) ?? asRecord(message.conversation) ?? null;
838
- const chatFromList = readFirstChatRecord(message);
839
- const chatGuid =
840
- readString(message, "chatGuid") ??
841
- readString(message, "chat_guid") ??
842
- readString(chat, "chatGuid") ??
843
- readString(chat, "chat_guid") ??
844
- readString(chat, "guid") ??
845
- readString(chatFromList, "chatGuid") ??
846
- readString(chatFromList, "chat_guid") ??
847
- readString(chatFromList, "guid");
848
- const chatIdentifier =
849
- readString(message, "chatIdentifier") ??
850
- readString(message, "chat_identifier") ??
851
- readString(chat, "chatIdentifier") ??
852
- readString(chat, "chat_identifier") ??
853
- readString(chat, "identifier") ??
854
- readString(chatFromList, "chatIdentifier") ??
855
- readString(chatFromList, "chat_identifier") ??
856
- readString(chatFromList, "identifier") ??
857
- extractChatIdentifierFromChatGuid(chatGuid);
858
- const chatId =
859
- readNumberLike(message, "chatId") ??
860
- readNumberLike(message, "chat_id") ??
861
- readNumberLike(chat, "chatId") ??
862
- readNumberLike(chat, "chat_id") ??
863
- readNumberLike(chat, "id") ??
864
- readNumberLike(chatFromList, "chatId") ??
865
- readNumberLike(chatFromList, "chat_id") ??
866
- readNumberLike(chatFromList, "id");
867
- const chatName =
868
- readString(message, "chatName") ??
869
- readString(chat, "displayName") ??
870
- readString(chat, "name") ??
871
- readString(chatFromList, "displayName") ??
872
- readString(chatFromList, "name") ??
873
- undefined;
874
-
875
- const chatParticipants = chat ? chat["participants"] : undefined;
876
- const messageParticipants = message["participants"];
877
- const chatsParticipants = chatFromList ? chatFromList["participants"] : undefined;
878
- const participants = Array.isArray(chatParticipants)
879
- ? chatParticipants
880
- : Array.isArray(messageParticipants)
881
- ? messageParticipants
882
- : Array.isArray(chatsParticipants)
883
- ? chatsParticipants
884
- : [];
885
- const normalizedParticipants = normalizeParticipantList(participants);
886
- const participantsCount = participants.length;
887
- const groupFromChatGuid = resolveGroupFlagFromChatGuid(chatGuid);
888
- const explicitIsGroup =
889
- readBoolean(message, "isGroup") ??
890
- readBoolean(message, "is_group") ??
891
- readBoolean(chat, "isGroup") ??
892
- readBoolean(message, "group");
893
- const isGroup =
894
- typeof groupFromChatGuid === "boolean"
895
- ? groupFromChatGuid
896
- : explicitIsGroup ?? (participantsCount > 2 ? true : false);
897
-
898
- const fromMe = readBoolean(message, "isFromMe") ?? readBoolean(message, "is_from_me");
899
- const messageId =
900
- readString(message, "guid") ??
901
- readString(message, "id") ??
902
- readString(message, "messageId") ??
903
- undefined;
904
- const balloonBundleId = readString(message, "balloonBundleId");
905
- const associatedMessageGuid =
906
- readString(message, "associatedMessageGuid") ??
907
- readString(message, "associated_message_guid") ??
908
- readString(message, "associatedMessageId") ??
909
- undefined;
910
- const associatedMessageType =
911
- readNumberLike(message, "associatedMessageType") ??
912
- readNumberLike(message, "associated_message_type");
913
- const associatedMessageEmoji =
914
- readString(message, "associatedMessageEmoji") ??
915
- readString(message, "associated_message_emoji") ??
916
- readString(message, "reactionEmoji") ??
917
- readString(message, "reaction_emoji") ??
918
- undefined;
919
- const isTapback =
920
- readBoolean(message, "isTapback") ??
921
- readBoolean(message, "is_tapback") ??
922
- readBoolean(message, "tapback") ??
923
- undefined;
924
-
925
- const timestampRaw =
926
- readNumber(message, "date") ??
927
- readNumber(message, "dateCreated") ??
928
- readNumber(message, "timestamp");
929
- const timestamp =
930
- typeof timestampRaw === "number"
931
- ? timestampRaw > 1_000_000_000_000
932
- ? timestampRaw
933
- : timestampRaw * 1000
934
- : undefined;
935
-
936
- const normalizedSender = normalizeBlueBubblesHandle(senderId);
937
- if (!normalizedSender) return null;
938
- const replyMetadata = extractReplyMetadata(message);
939
-
940
- return {
941
- text,
942
- senderId: normalizedSender,
943
- senderName,
944
- messageId,
945
- timestamp,
946
- isGroup,
947
- chatId,
948
- chatGuid,
949
- chatIdentifier,
950
- chatName,
951
- fromMe,
952
- attachments: extractAttachments(message),
953
- balloonBundleId,
954
- associatedMessageGuid,
955
- associatedMessageType,
956
- associatedMessageEmoji,
957
- isTapback,
958
- participants: normalizedParticipants,
959
- replyToId: replyMetadata.replyToId,
960
- replyToBody: replyMetadata.replyToBody,
961
- replyToSender: replyMetadata.replyToSender,
962
- };
963
- }
964
-
965
- function normalizeWebhookReaction(payload: Record<string, unknown>): NormalizedWebhookReaction | null {
966
- const message = extractMessagePayload(payload);
967
- if (!message) return null;
968
-
969
- const associatedGuid =
970
- readString(message, "associatedMessageGuid") ??
971
- readString(message, "associated_message_guid") ??
972
- readString(message, "associatedMessageId");
973
- const associatedType =
974
- readNumberLike(message, "associatedMessageType") ??
975
- readNumberLike(message, "associated_message_type");
976
- if (!associatedGuid || associatedType === undefined) return null;
977
-
978
- const mapping = REACTION_TYPE_MAP.get(associatedType);
979
- const associatedEmoji =
980
- readString(message, "associatedMessageEmoji") ??
981
- readString(message, "associated_message_emoji") ??
982
- readString(message, "reactionEmoji") ??
983
- readString(message, "reaction_emoji");
984
- const emoji = (associatedEmoji?.trim() || mapping?.emoji) ?? `reaction:${associatedType}`;
985
- const action = mapping?.action ?? resolveTapbackActionHint(associatedType) ?? "added";
986
-
987
- const handleValue = message.handle ?? message.sender;
988
- const handle =
989
- asRecord(handleValue) ??
990
- (typeof handleValue === "string" ? { address: handleValue } : null);
991
- const senderId =
992
- readString(handle, "address") ??
993
- readString(handle, "handle") ??
994
- readString(handle, "id") ??
995
- readString(message, "senderId") ??
996
- readString(message, "sender") ??
997
- readString(message, "from") ??
998
- "";
999
- const senderName =
1000
- readString(handle, "displayName") ??
1001
- readString(handle, "name") ??
1002
- readString(message, "senderName") ??
1003
- undefined;
1004
-
1005
- const chat = asRecord(message.chat) ?? asRecord(message.conversation) ?? null;
1006
- const chatFromList = readFirstChatRecord(message);
1007
- const chatGuid =
1008
- readString(message, "chatGuid") ??
1009
- readString(message, "chat_guid") ??
1010
- readString(chat, "chatGuid") ??
1011
- readString(chat, "chat_guid") ??
1012
- readString(chat, "guid") ??
1013
- readString(chatFromList, "chatGuid") ??
1014
- readString(chatFromList, "chat_guid") ??
1015
- readString(chatFromList, "guid");
1016
- const chatIdentifier =
1017
- readString(message, "chatIdentifier") ??
1018
- readString(message, "chat_identifier") ??
1019
- readString(chat, "chatIdentifier") ??
1020
- readString(chat, "chat_identifier") ??
1021
- readString(chat, "identifier") ??
1022
- readString(chatFromList, "chatIdentifier") ??
1023
- readString(chatFromList, "chat_identifier") ??
1024
- readString(chatFromList, "identifier") ??
1025
- extractChatIdentifierFromChatGuid(chatGuid);
1026
- const chatId =
1027
- readNumberLike(message, "chatId") ??
1028
- readNumberLike(message, "chat_id") ??
1029
- readNumberLike(chat, "chatId") ??
1030
- readNumberLike(chat, "chat_id") ??
1031
- readNumberLike(chat, "id") ??
1032
- readNumberLike(chatFromList, "chatId") ??
1033
- readNumberLike(chatFromList, "chat_id") ??
1034
- readNumberLike(chatFromList, "id");
1035
- const chatName =
1036
- readString(message, "chatName") ??
1037
- readString(chat, "displayName") ??
1038
- readString(chat, "name") ??
1039
- readString(chatFromList, "displayName") ??
1040
- readString(chatFromList, "name") ??
1041
- undefined;
1042
-
1043
- const chatParticipants = chat ? chat["participants"] : undefined;
1044
- const messageParticipants = message["participants"];
1045
- const chatsParticipants = chatFromList ? chatFromList["participants"] : undefined;
1046
- const participants = Array.isArray(chatParticipants)
1047
- ? chatParticipants
1048
- : Array.isArray(messageParticipants)
1049
- ? messageParticipants
1050
- : Array.isArray(chatsParticipants)
1051
- ? chatsParticipants
1052
- : [];
1053
- const participantsCount = participants.length;
1054
- const groupFromChatGuid = resolveGroupFlagFromChatGuid(chatGuid);
1055
- const explicitIsGroup =
1056
- readBoolean(message, "isGroup") ??
1057
- readBoolean(message, "is_group") ??
1058
- readBoolean(chat, "isGroup") ??
1059
- readBoolean(message, "group");
1060
- const isGroup =
1061
- typeof groupFromChatGuid === "boolean"
1062
- ? groupFromChatGuid
1063
- : explicitIsGroup ?? (participantsCount > 2 ? true : false);
1064
-
1065
- const fromMe = readBoolean(message, "isFromMe") ?? readBoolean(message, "is_from_me");
1066
- const timestampRaw =
1067
- readNumberLike(message, "date") ??
1068
- readNumberLike(message, "dateCreated") ??
1069
- readNumberLike(message, "timestamp");
1070
- const timestamp =
1071
- typeof timestampRaw === "number"
1072
- ? timestampRaw > 1_000_000_000_000
1073
- ? timestampRaw
1074
- : timestampRaw * 1000
1075
- : undefined;
1076
-
1077
- const normalizedSender = normalizeBlueBubblesHandle(senderId);
1078
- if (!normalizedSender) return null;
1079
-
1080
- return {
1081
- action,
1082
- emoji,
1083
- senderId: normalizedSender,
1084
- senderName,
1085
- messageId: associatedGuid,
1086
- timestamp,
1087
- isGroup,
1088
- chatId,
1089
- chatGuid,
1090
- chatIdentifier,
1091
- chatName,
1092
- fromMe,
1093
- };
1094
- }
1095
-
1096
- export async function handleBlueBubblesWebhookRequest(
1097
- req: IncomingMessage,
1098
- res: ServerResponse,
1099
- ): Promise<boolean> {
1100
- const url = new URL(req.url ?? "/", "http://localhost");
1101
- const path = normalizeWebhookPath(url.pathname);
1102
- const targets = webhookTargets.get(path);
1103
- if (!targets || targets.length === 0) return false;
1104
-
1105
- if (req.method !== "POST") {
1106
- res.statusCode = 405;
1107
- res.setHeader("Allow", "POST");
1108
- res.end("Method Not Allowed");
1109
- return true;
1110
- }
1111
-
1112
- const body = await readJsonBody(req, 1024 * 1024);
1113
- if (!body.ok) {
1114
- res.statusCode = body.error === "payload too large" ? 413 : 400;
1115
- res.end(body.error ?? "invalid payload");
1116
- console.warn(`[bluebubbles] webhook rejected: ${body.error ?? "invalid payload"}`);
1117
- return true;
1118
- }
1119
-
1120
- const payload = asRecord(body.value) ?? {};
1121
- const firstTarget = targets[0];
1122
- if (firstTarget) {
1123
- logVerbose(
1124
- firstTarget.core,
1125
- firstTarget.runtime,
1126
- `webhook received path=${path} keys=${Object.keys(payload).join(",") || "none"}`,
1127
- );
1128
- }
1129
- const eventTypeRaw = payload.type;
1130
- const eventType = typeof eventTypeRaw === "string" ? eventTypeRaw.trim() : "";
1131
- const allowedEventTypes = new Set([
1132
- "new-message",
1133
- "updated-message",
1134
- "message-reaction",
1135
- "reaction",
1136
- ]);
1137
- if (eventType && !allowedEventTypes.has(eventType)) {
1138
- res.statusCode = 200;
1139
- res.end("ok");
1140
- if (firstTarget) {
1141
- logVerbose(firstTarget.core, firstTarget.runtime, `webhook ignored type=${eventType}`);
1142
- }
1143
- return true;
1144
- }
1145
- const reaction = normalizeWebhookReaction(payload);
1146
- if (
1147
- (eventType === "updated-message" ||
1148
- eventType === "message-reaction" ||
1149
- eventType === "reaction") &&
1150
- !reaction
1151
- ) {
1152
- res.statusCode = 200;
1153
- res.end("ok");
1154
- if (firstTarget) {
1155
- logVerbose(
1156
- firstTarget.core,
1157
- firstTarget.runtime,
1158
- `webhook ignored ${eventType || "event"} without reaction`,
1159
- );
1160
- }
1161
- return true;
1162
- }
1163
- const message = reaction ? null : normalizeWebhookMessage(payload);
1164
- if (!message && !reaction) {
1165
- res.statusCode = 400;
1166
- res.end("invalid payload");
1167
- console.warn("[bluebubbles] webhook rejected: unable to parse message payload");
1168
- return true;
1169
- }
1170
-
1171
- const matching = targets.filter((target) => {
1172
- const token = target.account.config.password?.trim();
1173
- if (!token) return true;
1174
- const guidParam = url.searchParams.get("guid") ?? url.searchParams.get("password");
1175
- const headerToken =
1176
- req.headers["x-guid"] ??
1177
- req.headers["x-password"] ??
1178
- req.headers["x-bluebubbles-guid"] ??
1179
- req.headers["authorization"];
1180
- const guid =
1181
- (Array.isArray(headerToken) ? headerToken[0] : headerToken) ?? guidParam ?? "";
1182
- if (guid && guid.trim() === token) return true;
1183
- const remote = req.socket?.remoteAddress ?? "";
1184
- if (remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1") {
1185
- return true;
1186
- }
1187
- return false;
1188
- });
1189
-
1190
- if (matching.length === 0) {
1191
- res.statusCode = 401;
1192
- res.end("unauthorized");
1193
- console.warn(
1194
- `[bluebubbles] webhook rejected: unauthorized guid=${maskSecret(url.searchParams.get("guid") ?? url.searchParams.get("password") ?? "")}`,
1195
- );
1196
- return true;
1197
- }
1198
-
1199
- for (const target of matching) {
1200
- target.statusSink?.({ lastInboundAt: Date.now() });
1201
- if (reaction) {
1202
- processReaction(reaction, target).catch((err) => {
1203
- target.runtime.error?.(
1204
- `[${target.account.accountId}] BlueBubbles reaction failed: ${String(err)}`,
1205
- );
1206
- });
1207
- } else if (message) {
1208
- processMessage(message, target).catch((err) => {
1209
- target.runtime.error?.(
1210
- `[${target.account.accountId}] BlueBubbles webhook failed: ${String(err)}`,
1211
- );
1212
- });
1213
- }
1214
- }
1215
-
1216
- res.statusCode = 200;
1217
- res.end("ok");
1218
- if (reaction) {
1219
- if (firstTarget) {
1220
- logVerbose(
1221
- firstTarget.core,
1222
- firstTarget.runtime,
1223
- `webhook accepted reaction sender=${reaction.senderId} msg=${reaction.messageId} action=${reaction.action}`,
1224
- );
1225
- }
1226
- } else if (message) {
1227
- if (firstTarget) {
1228
- logVerbose(
1229
- firstTarget.core,
1230
- firstTarget.runtime,
1231
- `webhook accepted sender=${message.senderId} group=${message.isGroup} chatGuid=${message.chatGuid ?? ""} chatId=${message.chatId ?? ""}`,
1232
- );
1233
- }
1234
- }
1235
- return true;
1236
- }
1237
-
1238
- async function processMessage(
1239
- message: NormalizedWebhookMessage,
1240
- target: WebhookTarget,
1241
- ): Promise<void> {
1242
- const { account, config, runtime, core, statusSink } = target;
1243
-
1244
- const groupFlag = resolveGroupFlagFromChatGuid(message.chatGuid);
1245
- const isGroup = typeof groupFlag === "boolean" ? groupFlag : message.isGroup;
1246
-
1247
- const text = message.text.trim();
1248
- const attachments = message.attachments ?? [];
1249
- const placeholder = buildMessagePlaceholder(message);
1250
- // Check if text is a tapback pattern (e.g., 'Loved "hello"') and transform to emoji format
1251
- // For tapbacks, we'll append [[reply_to:N]] at the end; for regular messages, prepend it
1252
- const tapbackContext = resolveTapbackContext(message);
1253
- const tapbackParsed = parseTapbackText({
1254
- text,
1255
- emojiHint: tapbackContext?.emojiHint,
1256
- actionHint: tapbackContext?.actionHint,
1257
- requireQuoted: !tapbackContext,
1258
- });
1259
- const isTapbackMessage = Boolean(tapbackParsed);
1260
- const rawBody = tapbackParsed
1261
- ? tapbackParsed.action === "removed"
1262
- ? `removed ${tapbackParsed.emoji} reaction`
1263
- : `reacted with ${tapbackParsed.emoji}`
1264
- : text || placeholder;
1265
-
1266
- const cacheMessageId = message.messageId?.trim();
1267
- let messageShortId: string | undefined;
1268
- const cacheInboundMessage = () => {
1269
- if (!cacheMessageId) return;
1270
- const cacheEntry = rememberBlueBubblesReplyCache({
1271
- accountId: account.accountId,
1272
- messageId: cacheMessageId,
1273
- chatGuid: message.chatGuid,
1274
- chatIdentifier: message.chatIdentifier,
1275
- chatId: message.chatId,
1276
- senderLabel: message.fromMe ? "me" : message.senderId,
1277
- body: rawBody,
1278
- timestamp: message.timestamp ?? Date.now(),
1279
- });
1280
- messageShortId = cacheEntry.shortId;
1281
- };
1282
-
1283
- if (message.fromMe) {
1284
- // Cache from-me messages so reply context can resolve sender/body.
1285
- cacheInboundMessage();
1286
- return;
1287
- }
1288
-
1289
- if (!rawBody) {
1290
- logVerbose(core, runtime, `drop: empty text sender=${message.senderId}`);
1291
- return;
1292
- }
1293
- logVerbose(
1294
- core,
1295
- runtime,
1296
- `msg sender=${message.senderId} group=${isGroup} textLen=${text.length} attachments=${attachments.length} chatGuid=${message.chatGuid ?? ""} chatId=${message.chatId ?? ""}`,
1297
- );
1298
-
1299
- const dmPolicy = account.config.dmPolicy ?? "pairing";
1300
- const groupPolicy = account.config.groupPolicy ?? "allowlist";
1301
- const configAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry));
1302
- const configGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((entry) => String(entry));
1303
- const storeAllowFrom = await core.channel.pairing
1304
- .readAllowFromStore("bluebubbles")
1305
- .catch(() => []);
1306
- const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]
1307
- .map((entry) => String(entry).trim())
1308
- .filter(Boolean);
1309
- const effectiveGroupAllowFrom = [
1310
- ...(configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom),
1311
- ...storeAllowFrom,
1312
- ]
1313
- .map((entry) => String(entry).trim())
1314
- .filter(Boolean);
1315
- const groupAllowEntry = formatGroupAllowlistEntry({
1316
- chatGuid: message.chatGuid,
1317
- chatId: message.chatId ?? undefined,
1318
- chatIdentifier: message.chatIdentifier ?? undefined,
1319
- });
1320
- const groupName = message.chatName?.trim() || undefined;
1321
-
1322
- if (isGroup) {
1323
- if (groupPolicy === "disabled") {
1324
- logVerbose(core, runtime, "Blocked BlueBubbles group message (groupPolicy=disabled)");
1325
- logGroupAllowlistHint({
1326
- runtime,
1327
- reason: "groupPolicy=disabled",
1328
- entry: groupAllowEntry,
1329
- chatName: groupName,
1330
- accountId: account.accountId,
1331
- });
1332
- return;
1333
- }
1334
- if (groupPolicy === "allowlist") {
1335
- if (effectiveGroupAllowFrom.length === 0) {
1336
- logVerbose(core, runtime, "Blocked BlueBubbles group message (no allowlist)");
1337
- logGroupAllowlistHint({
1338
- runtime,
1339
- reason: "groupPolicy=allowlist (empty allowlist)",
1340
- entry: groupAllowEntry,
1341
- chatName: groupName,
1342
- accountId: account.accountId,
1343
- });
1344
- return;
1345
- }
1346
- const allowed = isAllowedBlueBubblesSender({
1347
- allowFrom: effectiveGroupAllowFrom,
1348
- sender: message.senderId,
1349
- chatId: message.chatId ?? undefined,
1350
- chatGuid: message.chatGuid ?? undefined,
1351
- chatIdentifier: message.chatIdentifier ?? undefined,
1352
- });
1353
- if (!allowed) {
1354
- logVerbose(
1355
- core,
1356
- runtime,
1357
- `Blocked BlueBubbles sender ${message.senderId} (not in groupAllowFrom)`,
1358
- );
1359
- logVerbose(
1360
- core,
1361
- runtime,
1362
- `drop: group sender not allowed sender=${message.senderId} allowFrom=${effectiveGroupAllowFrom.join(",")}`,
1363
- );
1364
- logGroupAllowlistHint({
1365
- runtime,
1366
- reason: "groupPolicy=allowlist (not allowlisted)",
1367
- entry: groupAllowEntry,
1368
- chatName: groupName,
1369
- accountId: account.accountId,
1370
- });
1371
- return;
1372
- }
1373
- }
1374
- } else {
1375
- if (dmPolicy === "disabled") {
1376
- logVerbose(core, runtime, `Blocked BlueBubbles DM from ${message.senderId}`);
1377
- logVerbose(core, runtime, `drop: dmPolicy disabled sender=${message.senderId}`);
1378
- return;
1379
- }
1380
- if (dmPolicy !== "open") {
1381
- const allowed = isAllowedBlueBubblesSender({
1382
- allowFrom: effectiveAllowFrom,
1383
- sender: message.senderId,
1384
- chatId: message.chatId ?? undefined,
1385
- chatGuid: message.chatGuid ?? undefined,
1386
- chatIdentifier: message.chatIdentifier ?? undefined,
1387
- });
1388
- if (!allowed) {
1389
- if (dmPolicy === "pairing") {
1390
- const { code, created } = await core.channel.pairing.upsertPairingRequest({
1391
- channel: "bluebubbles",
1392
- id: message.senderId,
1393
- meta: { name: message.senderName },
1394
- });
1395
- runtime.log?.(
1396
- `[bluebubbles] pairing request sender=${message.senderId} created=${created}`,
1397
- );
1398
- if (created) {
1399
- logVerbose(core, runtime, `bluebubbles pairing request sender=${message.senderId}`);
1400
- try {
1401
- await sendMessageBlueBubbles(
1402
- message.senderId,
1403
- core.channel.pairing.buildPairingReply({
1404
- channel: "bluebubbles",
1405
- idLine: `Your BlueBubbles sender id: ${message.senderId}`,
1406
- code,
1407
- }),
1408
- { cfg: config, accountId: account.accountId },
1409
- );
1410
- statusSink?.({ lastOutboundAt: Date.now() });
1411
- } catch (err) {
1412
- logVerbose(
1413
- core,
1414
- runtime,
1415
- `bluebubbles pairing reply failed for ${message.senderId}: ${String(err)}`,
1416
- );
1417
- runtime.error?.(
1418
- `[bluebubbles] pairing reply failed sender=${message.senderId}: ${String(err)}`,
1419
- );
1420
- }
1421
- }
1422
- } else {
1423
- logVerbose(
1424
- core,
1425
- runtime,
1426
- `Blocked unauthorized BlueBubbles sender ${message.senderId} (dmPolicy=${dmPolicy})`,
1427
- );
1428
- logVerbose(
1429
- core,
1430
- runtime,
1431
- `drop: dm sender not allowed sender=${message.senderId} allowFrom=${effectiveAllowFrom.join(",")}`,
1432
- );
1433
- }
1434
- return;
1435
- }
1436
- }
1437
- }
1438
-
1439
- const chatId = message.chatId ?? undefined;
1440
- const chatGuid = message.chatGuid ?? undefined;
1441
- const chatIdentifier = message.chatIdentifier ?? undefined;
1442
- const peerId = isGroup
1443
- ? chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group")
1444
- : message.senderId;
1445
-
1446
- const route = core.channel.routing.resolveAgentRoute({
1447
- cfg: config,
1448
- channel: "bluebubbles",
1449
- accountId: account.accountId,
1450
- peer: {
1451
- kind: isGroup ? "group" : "dm",
1452
- id: peerId,
1453
- },
1454
- });
1455
-
1456
- // Mention gating for group chats (parity with iMessage/WhatsApp)
1457
- const messageText = text;
1458
- const mentionRegexes = core.channel.mentions.buildMentionRegexes(config, route.agentId);
1459
- const wasMentioned = isGroup
1460
- ? core.channel.mentions.matchesMentionPatterns(messageText, mentionRegexes)
1461
- : true;
1462
- const canDetectMention = mentionRegexes.length > 0;
1463
- const requireMention = core.channel.groups.resolveRequireMention({
1464
- cfg: config,
1465
- channel: "bluebubbles",
1466
- groupId: peerId,
1467
- accountId: account.accountId,
1468
- });
1469
-
1470
- // Command gating (parity with iMessage/WhatsApp)
1471
- const useAccessGroups = config.commands?.useAccessGroups !== false;
1472
- const hasControlCmd = core.channel.text.hasControlCommand(messageText, config);
1473
- const ownerAllowedForCommands =
1474
- effectiveAllowFrom.length > 0
1475
- ? isAllowedBlueBubblesSender({
1476
- allowFrom: effectiveAllowFrom,
1477
- sender: message.senderId,
1478
- chatId: message.chatId ?? undefined,
1479
- chatGuid: message.chatGuid ?? undefined,
1480
- chatIdentifier: message.chatIdentifier ?? undefined,
1481
- })
1482
- : false;
1483
- const groupAllowedForCommands =
1484
- effectiveGroupAllowFrom.length > 0
1485
- ? isAllowedBlueBubblesSender({
1486
- allowFrom: effectiveGroupAllowFrom,
1487
- sender: message.senderId,
1488
- chatId: message.chatId ?? undefined,
1489
- chatGuid: message.chatGuid ?? undefined,
1490
- chatIdentifier: message.chatIdentifier ?? undefined,
1491
- })
1492
- : false;
1493
- const dmAuthorized = dmPolicy === "open" || ownerAllowedForCommands;
1494
- const commandGate = resolveControlCommandGate({
1495
- useAccessGroups,
1496
- authorizers: [
1497
- { configured: effectiveAllowFrom.length > 0, allowed: ownerAllowedForCommands },
1498
- { configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands },
1499
- ],
1500
- allowTextCommands: true,
1501
- hasControlCommand: hasControlCmd,
1502
- });
1503
- const commandAuthorized = isGroup ? commandGate.commandAuthorized : dmAuthorized;
1504
-
1505
- // Block control commands from unauthorized senders in groups
1506
- if (isGroup && commandGate.shouldBlock) {
1507
- logInboundDrop({
1508
- log: (msg) => logVerbose(core, runtime, msg),
1509
- channel: "bluebubbles",
1510
- reason: "control command (unauthorized)",
1511
- target: message.senderId,
1512
- });
1513
- return;
1514
- }
1515
-
1516
- // Allow control commands to bypass mention gating when authorized (parity with iMessage)
1517
- const shouldBypassMention =
1518
- isGroup &&
1519
- requireMention &&
1520
- !wasMentioned &&
1521
- commandAuthorized &&
1522
- hasControlCmd;
1523
- const effectiveWasMentioned = wasMentioned || shouldBypassMention;
1524
-
1525
- // Skip group messages that require mention but weren't mentioned
1526
- if (isGroup && requireMention && canDetectMention && !wasMentioned && !shouldBypassMention) {
1527
- logVerbose(core, runtime, `bluebubbles: skipping group message (no mention)`);
1528
- return;
1529
- }
1530
-
1531
- // Cache allowed inbound messages so later replies can resolve sender/body without
1532
- // surfacing dropped content (allowlist/mention/command gating).
1533
- cacheInboundMessage();
1534
-
1535
- const baseUrl = account.config.serverUrl?.trim();
1536
- const password = account.config.password?.trim();
1537
- const maxBytes =
1538
- account.config.mediaMaxMb && account.config.mediaMaxMb > 0
1539
- ? account.config.mediaMaxMb * 1024 * 1024
1540
- : 8 * 1024 * 1024;
1541
-
1542
- let mediaUrls: string[] = [];
1543
- let mediaPaths: string[] = [];
1544
- let mediaTypes: string[] = [];
1545
- if (attachments.length > 0) {
1546
- if (!baseUrl || !password) {
1547
- logVerbose(core, runtime, "attachment download skipped (missing serverUrl/password)");
1548
- } else {
1549
- for (const attachment of attachments) {
1550
- if (!attachment.guid) continue;
1551
- if (attachment.totalBytes && attachment.totalBytes > maxBytes) {
1552
- logVerbose(
1553
- core,
1554
- runtime,
1555
- `attachment too large guid=${attachment.guid} bytes=${attachment.totalBytes}`,
1556
- );
1557
- continue;
1558
- }
1559
- try {
1560
- const downloaded = await downloadBlueBubblesAttachment(attachment, {
1561
- cfg: config,
1562
- accountId: account.accountId,
1563
- maxBytes,
1564
- });
1565
- const saved = await core.channel.media.saveMediaBuffer(
1566
- downloaded.buffer,
1567
- downloaded.contentType,
1568
- "inbound",
1569
- maxBytes,
1570
- );
1571
- mediaPaths.push(saved.path);
1572
- mediaUrls.push(saved.path);
1573
- if (saved.contentType) {
1574
- mediaTypes.push(saved.contentType);
1575
- }
1576
- } catch (err) {
1577
- logVerbose(
1578
- core,
1579
- runtime,
1580
- `attachment download failed guid=${attachment.guid} err=${String(err)}`,
1581
- );
1582
- }
1583
- }
1584
- }
1585
- }
1586
- let replyToId = message.replyToId;
1587
- let replyToBody = message.replyToBody;
1588
- let replyToSender = message.replyToSender;
1589
- let replyToShortId: string | undefined;
1590
-
1591
- if (isTapbackMessage && tapbackContext?.replyToId) {
1592
- replyToId = tapbackContext.replyToId;
1593
- }
1594
-
1595
- if (replyToId) {
1596
- const cached = resolveReplyContextFromCache({
1597
- accountId: account.accountId,
1598
- replyToId,
1599
- chatGuid: message.chatGuid,
1600
- chatIdentifier: message.chatIdentifier,
1601
- chatId: message.chatId,
1602
- });
1603
- if (cached) {
1604
- if (!replyToBody && cached.body) replyToBody = cached.body;
1605
- if (!replyToSender && cached.senderLabel) replyToSender = cached.senderLabel;
1606
- replyToShortId = cached.shortId;
1607
- if (core.logging.shouldLogVerbose()) {
1608
- const preview = (cached.body ?? "").replace(/\s+/g, " ").slice(0, 120);
1609
- logVerbose(
1610
- core,
1611
- runtime,
1612
- `reply-context cache hit replyToId=${replyToId} sender=${replyToSender ?? ""} body="${preview}"`,
1613
- );
1614
- }
1615
- }
1616
- }
1617
-
1618
- // If no cached short ID, try to get one from the UUID directly
1619
- if (replyToId && !replyToShortId) {
1620
- replyToShortId = getShortIdForUuid(replyToId);
1621
- }
1622
-
1623
- // Use inline [[reply_to:N]] tag format
1624
- // For tapbacks/reactions: append at end (e.g., "reacted with ❤️ [[reply_to:4]]")
1625
- // For regular replies: prepend at start (e.g., "[[reply_to:4]] Awesome")
1626
- const replyTag = formatReplyTag({ replyToId, replyToShortId });
1627
- const baseBody = replyTag
1628
- ? isTapbackMessage
1629
- ? `${rawBody} ${replyTag}`
1630
- : `${replyTag} ${rawBody}`
1631
- : rawBody;
1632
- const fromLabel = isGroup ? undefined : message.senderName || `user:${message.senderId}`;
1633
- const groupSubject = isGroup ? message.chatName?.trim() || undefined : undefined;
1634
- const groupMembers = isGroup
1635
- ? formatGroupMembers({
1636
- participants: message.participants,
1637
- fallback: message.senderId ? { id: message.senderId, name: message.senderName } : undefined,
1638
- })
1639
- : undefined;
1640
- const storePath = core.channel.session.resolveStorePath(config.session?.store, {
1641
- agentId: route.agentId,
1642
- });
1643
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
1644
- const previousTimestamp = core.channel.session.readSessionUpdatedAt({
1645
- storePath,
1646
- sessionKey: route.sessionKey,
1647
- });
1648
- const body = core.channel.reply.formatAgentEnvelope({
1649
- channel: "BlueBubbles",
1650
- from: fromLabel,
1651
- timestamp: message.timestamp,
1652
- previousTimestamp,
1653
- envelope: envelopeOptions,
1654
- body: baseBody,
1655
- });
1656
- let chatGuidForActions = chatGuid;
1657
- if (!chatGuidForActions && baseUrl && password) {
1658
- const target =
1659
- isGroup && (chatId || chatIdentifier)
1660
- ? chatId
1661
- ? ({ kind: "chat_id", chatId } as const)
1662
- : ({ kind: "chat_identifier", chatIdentifier: chatIdentifier ?? "" } as const)
1663
- : ({ kind: "handle", address: message.senderId } as const);
1664
- if (target.kind !== "chat_identifier" || target.chatIdentifier) {
1665
- chatGuidForActions =
1666
- (await resolveChatGuidForTarget({
1667
- baseUrl,
1668
- password,
1669
- target,
1670
- })) ?? undefined;
1671
- }
1672
- }
1673
-
1674
- const ackReactionScope = config.messages?.ackReactionScope ?? "group-mentions";
1675
- const removeAckAfterReply = config.messages?.removeAckAfterReply ?? false;
1676
- const ackReactionValue = resolveBlueBubblesAckReaction({
1677
- cfg: config,
1678
- agentId: route.agentId,
1679
- core,
1680
- runtime,
1681
- });
1682
- const shouldAckReaction = () =>
1683
- Boolean(
1684
- ackReactionValue &&
1685
- core.channel.reactions.shouldAckReaction({
1686
- scope: ackReactionScope,
1687
- isDirect: !isGroup,
1688
- isGroup,
1689
- isMentionableGroup: isGroup,
1690
- requireMention: Boolean(requireMention),
1691
- canDetectMention,
1692
- effectiveWasMentioned,
1693
- shouldBypassMention,
1694
- }),
1695
- );
1696
- const ackMessageId = message.messageId?.trim() || "";
1697
- const ackReactionPromise =
1698
- shouldAckReaction() && ackMessageId && chatGuidForActions && ackReactionValue
1699
- ? sendBlueBubblesReaction({
1700
- chatGuid: chatGuidForActions,
1701
- messageGuid: ackMessageId,
1702
- emoji: ackReactionValue,
1703
- opts: { cfg: config, accountId: account.accountId },
1704
- }).then(
1705
- () => true,
1706
- (err) => {
1707
- logVerbose(
1708
- core,
1709
- runtime,
1710
- `ack reaction failed chatGuid=${chatGuidForActions} msg=${ackMessageId}: ${String(err)}`,
1711
- );
1712
- return false;
1713
- },
1714
- )
1715
- : null;
1716
-
1717
- // Respect sendReadReceipts config (parity with WhatsApp)
1718
- const sendReadReceipts = account.config.sendReadReceipts !== false;
1719
- if (chatGuidForActions && baseUrl && password && sendReadReceipts) {
1720
- try {
1721
- await markBlueBubblesChatRead(chatGuidForActions, {
1722
- cfg: config,
1723
- accountId: account.accountId,
1724
- });
1725
- logVerbose(core, runtime, `marked read chatGuid=${chatGuidForActions}`);
1726
- } catch (err) {
1727
- runtime.error?.(`[bluebubbles] mark read failed: ${String(err)}`);
1728
- }
1729
- } else if (!sendReadReceipts) {
1730
- logVerbose(core, runtime, "mark read skipped (sendReadReceipts=false)");
1731
- } else {
1732
- logVerbose(core, runtime, "mark read skipped (missing chatGuid or credentials)");
1733
- }
1734
-
1735
- const outboundTarget = isGroup
1736
- ? formatBlueBubblesChatTarget({
1737
- chatId,
1738
- chatGuid: chatGuidForActions ?? chatGuid,
1739
- chatIdentifier,
1740
- }) || peerId
1741
- : chatGuidForActions
1742
- ? formatBlueBubblesChatTarget({ chatGuid: chatGuidForActions })
1743
- : message.senderId;
1744
-
1745
- const maybeEnqueueOutboundMessageId = (messageId?: string, snippet?: string) => {
1746
- const trimmed = messageId?.trim();
1747
- if (!trimmed || trimmed === "ok" || trimmed === "unknown") return;
1748
- // Cache outbound message to get short ID
1749
- const cacheEntry = rememberBlueBubblesReplyCache({
1750
- accountId: account.accountId,
1751
- messageId: trimmed,
1752
- chatGuid: chatGuidForActions ?? chatGuid,
1753
- chatIdentifier,
1754
- chatId,
1755
- senderLabel: "me",
1756
- body: snippet ?? "",
1757
- timestamp: Date.now(),
1758
- });
1759
- const displayId = cacheEntry.shortId || trimmed;
1760
- const preview = snippet ? ` "${snippet.slice(0, 12)}${snippet.length > 12 ? "…" : ""}"` : "";
1761
- core.system.enqueueSystemEvent(`Assistant sent${preview} [message_id:${displayId}]`, {
1762
- sessionKey: route.sessionKey,
1763
- contextKey: `bluebubbles:outbound:${outboundTarget}:${trimmed}`,
1764
- });
1765
- };
1766
-
1767
- const ctxPayload = {
1768
- Body: body,
1769
- BodyForAgent: body,
1770
- RawBody: rawBody,
1771
- CommandBody: rawBody,
1772
- BodyForCommands: rawBody,
1773
- MediaUrl: mediaUrls[0],
1774
- MediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined,
1775
- MediaPath: mediaPaths[0],
1776
- MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
1777
- MediaType: mediaTypes[0],
1778
- MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
1779
- From: isGroup ? `group:${peerId}` : `bluebubbles:${message.senderId}`,
1780
- To: `bluebubbles:${outboundTarget}`,
1781
- SessionKey: route.sessionKey,
1782
- AccountId: route.accountId,
1783
- ChatType: isGroup ? "group" : "direct",
1784
- ConversationLabel: fromLabel,
1785
- // Use short ID for token savings (agent can use this to reference the message)
1786
- ReplyToId: replyToShortId || replyToId,
1787
- ReplyToIdFull: replyToId,
1788
- ReplyToBody: replyToBody,
1789
- ReplyToSender: replyToSender,
1790
- GroupSubject: groupSubject,
1791
- GroupMembers: groupMembers,
1792
- SenderName: message.senderName || undefined,
1793
- SenderId: message.senderId,
1794
- Provider: "bluebubbles",
1795
- Surface: "bluebubbles",
1796
- // Use short ID for token savings (agent can use this to reference the message)
1797
- MessageSid: messageShortId || message.messageId,
1798
- MessageSidFull: message.messageId,
1799
- Timestamp: message.timestamp,
1800
- OriginatingChannel: "bluebubbles",
1801
- OriginatingTo: `bluebubbles:${outboundTarget}`,
1802
- WasMentioned: effectiveWasMentioned,
1803
- CommandAuthorized: commandAuthorized,
1804
- };
1805
-
1806
- let sentMessage = false;
1807
- try {
1808
- await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1809
- ctx: ctxPayload,
1810
- cfg: config,
1811
- dispatcherOptions: {
1812
- deliver: async (payload) => {
1813
- const rawReplyToId = typeof payload.replyToId === "string" ? payload.replyToId.trim() : "";
1814
- // Resolve short ID (e.g., "5") to full UUID
1815
- const replyToMessageGuid = rawReplyToId
1816
- ? resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
1817
- : "";
1818
- const mediaList = payload.mediaUrls?.length
1819
- ? payload.mediaUrls
1820
- : payload.mediaUrl
1821
- ? [payload.mediaUrl]
1822
- : [];
1823
- if (mediaList.length > 0) {
1824
- const tableMode = core.channel.text.resolveMarkdownTableMode({
1825
- cfg: config,
1826
- channel: "bluebubbles",
1827
- accountId: account.accountId,
1828
- });
1829
- const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
1830
- let first = true;
1831
- for (const mediaUrl of mediaList) {
1832
- const caption = first ? text : undefined;
1833
- first = false;
1834
- const result = await sendBlueBubblesMedia({
1835
- cfg: config,
1836
- to: outboundTarget,
1837
- mediaUrl,
1838
- caption: caption ?? undefined,
1839
- replyToId: replyToMessageGuid || null,
1840
- accountId: account.accountId,
1841
- });
1842
- const cachedBody = (caption ?? "").trim() || "<media:attachment>";
1843
- maybeEnqueueOutboundMessageId(result.messageId, cachedBody);
1844
- sentMessage = true;
1845
- statusSink?.({ lastOutboundAt: Date.now() });
1846
- }
1847
- return;
1848
- }
1849
-
1850
- const textLimit =
1851
- account.config.textChunkLimit && account.config.textChunkLimit > 0
1852
- ? account.config.textChunkLimit
1853
- : DEFAULT_TEXT_LIMIT;
1854
- const chunkMode = account.config.chunkMode ?? "length";
1855
- const tableMode = core.channel.text.resolveMarkdownTableMode({
1856
- cfg: config,
1857
- channel: "bluebubbles",
1858
- accountId: account.accountId,
1859
- });
1860
- const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
1861
- const chunks =
1862
- chunkMode === "newline"
1863
- ? core.channel.text.chunkTextWithMode(text, textLimit, chunkMode)
1864
- : core.channel.text.chunkMarkdownText(text, textLimit);
1865
- if (!chunks.length && text) chunks.push(text);
1866
- if (!chunks.length) return;
1867
- for (let i = 0; i < chunks.length; i++) {
1868
- const chunk = chunks[i];
1869
- const result = await sendMessageBlueBubbles(outboundTarget, chunk, {
1870
- cfg: config,
1871
- accountId: account.accountId,
1872
- replyToMessageGuid: replyToMessageGuid || undefined,
1873
- });
1874
- maybeEnqueueOutboundMessageId(result.messageId, chunk);
1875
- sentMessage = true;
1876
- statusSink?.({ lastOutboundAt: Date.now() });
1877
- // In newline mode, restart typing after each chunk if more chunks remain
1878
- // Small delay allows the Apple API to finish clearing the typing state from message send
1879
- if (chunkMode === "newline" && i < chunks.length - 1 && chatGuidForActions) {
1880
- await new Promise((r) => setTimeout(r, 150));
1881
- sendBlueBubblesTyping(chatGuidForActions, true, {
1882
- cfg: config,
1883
- accountId: account.accountId,
1884
- }).catch(() => {
1885
- // Ignore typing errors
1886
- });
1887
- }
1888
- }
1889
- },
1890
- onReplyStart: async () => {
1891
- if (!chatGuidForActions) return;
1892
- if (!baseUrl || !password) return;
1893
- logVerbose(core, runtime, `typing start chatGuid=${chatGuidForActions}`);
1894
- try {
1895
- await sendBlueBubblesTyping(chatGuidForActions, true, {
1896
- cfg: config,
1897
- accountId: account.accountId,
1898
- });
1899
- } catch (err) {
1900
- runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`);
1901
- }
1902
- },
1903
- onIdle: async () => {
1904
- if (!chatGuidForActions) return;
1905
- if (!baseUrl || !password) return;
1906
- try {
1907
- await sendBlueBubblesTyping(chatGuidForActions, false, {
1908
- cfg: config,
1909
- accountId: account.accountId,
1910
- });
1911
- } catch (err) {
1912
- logVerbose(core, runtime, `typing stop failed: ${String(err)}`);
1913
- }
1914
- },
1915
- onError: (err, info) => {
1916
- runtime.error?.(`BlueBubbles ${info.kind} reply failed: ${String(err)}`);
1917
- },
1918
- },
1919
- replyOptions: {
1920
- disableBlockStreaming:
1921
- typeof account.config.blockStreaming === "boolean"
1922
- ? !account.config.blockStreaming
1923
- : undefined,
1924
- },
1925
- });
1926
- } finally {
1927
- if (sentMessage && chatGuidForActions && ackMessageId) {
1928
- core.channel.reactions.removeAckReactionAfterReply({
1929
- removeAfterReply: removeAckAfterReply,
1930
- ackReactionPromise,
1931
- ackReactionValue: ackReactionValue ?? null,
1932
- remove: () =>
1933
- sendBlueBubblesReaction({
1934
- chatGuid: chatGuidForActions,
1935
- messageGuid: ackMessageId,
1936
- emoji: ackReactionValue ?? "",
1937
- remove: true,
1938
- opts: { cfg: config, accountId: account.accountId },
1939
- }),
1940
- onError: (err) => {
1941
- logAckFailure({
1942
- log: (msg) => logVerbose(core, runtime, msg),
1943
- channel: "bluebubbles",
1944
- target: `${chatGuidForActions}/${ackMessageId}`,
1945
- error: err,
1946
- });
1947
- },
1948
- });
1949
- }
1950
- if (chatGuidForActions && baseUrl && password && !sentMessage) {
1951
- // Stop typing indicator when no message was sent (e.g., NO_REPLY)
1952
- sendBlueBubblesTyping(chatGuidForActions, false, {
1953
- cfg: config,
1954
- accountId: account.accountId,
1955
- }).catch((err) => {
1956
- logTypingFailure({
1957
- log: (msg) => logVerbose(core, runtime, msg),
1958
- channel: "bluebubbles",
1959
- action: "stop",
1960
- target: chatGuidForActions,
1961
- error: err,
1962
- });
1963
- });
1964
- }
1965
- }
1966
- }
1967
-
1968
- async function processReaction(
1969
- reaction: NormalizedWebhookReaction,
1970
- target: WebhookTarget,
1971
- ): Promise<void> {
1972
- const { account, config, runtime, core } = target;
1973
- if (reaction.fromMe) return;
1974
-
1975
- const dmPolicy = account.config.dmPolicy ?? "pairing";
1976
- const groupPolicy = account.config.groupPolicy ?? "allowlist";
1977
- const configAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry));
1978
- const configGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((entry) => String(entry));
1979
- const storeAllowFrom = await core.channel.pairing
1980
- .readAllowFromStore("bluebubbles")
1981
- .catch(() => []);
1982
- const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]
1983
- .map((entry) => String(entry).trim())
1984
- .filter(Boolean);
1985
- const effectiveGroupAllowFrom = [
1986
- ...(configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom),
1987
- ...storeAllowFrom,
1988
- ]
1989
- .map((entry) => String(entry).trim())
1990
- .filter(Boolean);
1991
-
1992
- if (reaction.isGroup) {
1993
- if (groupPolicy === "disabled") return;
1994
- if (groupPolicy === "allowlist") {
1995
- if (effectiveGroupAllowFrom.length === 0) return;
1996
- const allowed = isAllowedBlueBubblesSender({
1997
- allowFrom: effectiveGroupAllowFrom,
1998
- sender: reaction.senderId,
1999
- chatId: reaction.chatId ?? undefined,
2000
- chatGuid: reaction.chatGuid ?? undefined,
2001
- chatIdentifier: reaction.chatIdentifier ?? undefined,
2002
- });
2003
- if (!allowed) return;
2004
- }
2005
- } else {
2006
- if (dmPolicy === "disabled") return;
2007
- if (dmPolicy !== "open") {
2008
- const allowed = isAllowedBlueBubblesSender({
2009
- allowFrom: effectiveAllowFrom,
2010
- sender: reaction.senderId,
2011
- chatId: reaction.chatId ?? undefined,
2012
- chatGuid: reaction.chatGuid ?? undefined,
2013
- chatIdentifier: reaction.chatIdentifier ?? undefined,
2014
- });
2015
- if (!allowed) return;
2016
- }
2017
- }
2018
-
2019
- const chatId = reaction.chatId ?? undefined;
2020
- const chatGuid = reaction.chatGuid ?? undefined;
2021
- const chatIdentifier = reaction.chatIdentifier ?? undefined;
2022
- const peerId = reaction.isGroup
2023
- ? chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group")
2024
- : reaction.senderId;
2025
-
2026
- const route = core.channel.routing.resolveAgentRoute({
2027
- cfg: config,
2028
- channel: "bluebubbles",
2029
- accountId: account.accountId,
2030
- peer: {
2031
- kind: reaction.isGroup ? "group" : "dm",
2032
- id: peerId,
2033
- },
2034
- });
2035
-
2036
- const senderLabel = reaction.senderName || reaction.senderId;
2037
- const chatLabel = reaction.isGroup ? ` in group:${peerId}` : "";
2038
- // Use short ID for token savings
2039
- const messageDisplayId = getShortIdForUuid(reaction.messageId) || reaction.messageId;
2040
- // Format: "Tyler reacted with ❤️ [[reply_to:5]]" or "Tyler removed ❤️ reaction [[reply_to:5]]"
2041
- const text =
2042
- reaction.action === "removed"
2043
- ? `${senderLabel} removed ${reaction.emoji} reaction [[reply_to:${messageDisplayId}]]${chatLabel}`
2044
- : `${senderLabel} reacted with ${reaction.emoji} [[reply_to:${messageDisplayId}]]${chatLabel}`;
2045
- core.system.enqueueSystemEvent(text, {
2046
- sessionKey: route.sessionKey,
2047
- contextKey: `bluebubbles:reaction:${reaction.action}:${peerId}:${reaction.messageId}:${reaction.senderId}:${reaction.emoji}`,
2048
- });
2049
- logVerbose(core, runtime, `reaction event enqueued: ${text}`);
2050
- }
2051
-
2052
- export async function monitorBlueBubblesProvider(
2053
- options: BlueBubblesMonitorOptions,
2054
- ): Promise<void> {
2055
- const { account, config, runtime, abortSignal, statusSink } = options;
2056
- const core = getBlueBubblesRuntime();
2057
- const path = options.webhookPath?.trim() || DEFAULT_WEBHOOK_PATH;
2058
-
2059
- // Fetch and cache server info (for macOS version detection in action gating)
2060
- const serverInfo = await fetchBlueBubblesServerInfo({
2061
- baseUrl: account.baseUrl,
2062
- password: account.config.password,
2063
- accountId: account.accountId,
2064
- timeoutMs: 5000,
2065
- }).catch(() => null);
2066
- if (serverInfo?.os_version) {
2067
- runtime.log?.(`[${account.accountId}] BlueBubbles server macOS ${serverInfo.os_version}`);
2068
- }
2069
-
2070
- const unregister = registerBlueBubblesWebhookTarget({
2071
- account,
2072
- config,
2073
- runtime,
2074
- core,
2075
- path,
2076
- statusSink,
2077
- });
2078
-
2079
- return await new Promise((resolve) => {
2080
- const stop = () => {
2081
- unregister();
2082
- resolve();
2083
- };
2084
-
2085
- if (abortSignal?.aborted) {
2086
- stop();
2087
- return;
2088
- }
2089
-
2090
- abortSignal?.addEventListener("abort", stop, { once: true });
2091
- runtime.log?.(
2092
- `[${account.accountId}] BlueBubbles webhook listening on ${normalizeWebhookPath(path)}`,
2093
- );
2094
- });
2095
- }
2096
-
2097
- export function resolveWebhookPathFromConfig(config?: BlueBubblesAccountConfig): string {
2098
- const raw = config?.webhookPath?.trim();
2099
- if (raw) return normalizeWebhookPath(raw);
2100
- return DEFAULT_WEBHOOK_PATH;
2101
- }