cowork-os 0.3.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1638 -0
- package/bin/cowork.js +42 -0
- package/build/entitlements.mac.plist +16 -0
- package/build/icon.icns +0 -0
- package/build/icon.png +0 -0
- package/dist/electron/electron/activity/ActivityRepository.js +190 -0
- package/dist/electron/electron/agent/browser/browser-service.js +639 -0
- package/dist/electron/electron/agent/context-manager.js +225 -0
- package/dist/electron/electron/agent/custom-skill-loader.js +566 -0
- package/dist/electron/electron/agent/daemon.js +975 -0
- package/dist/electron/electron/agent/executor.js +3561 -0
- package/dist/electron/electron/agent/llm/anthropic-provider.js +155 -0
- package/dist/electron/electron/agent/llm/bedrock-provider.js +202 -0
- package/dist/electron/electron/agent/llm/gemini-provider.js +375 -0
- package/dist/electron/electron/agent/llm/index.js +34 -0
- package/dist/electron/electron/agent/llm/ollama-provider.js +263 -0
- package/dist/electron/electron/agent/llm/openai-oauth.js +101 -0
- package/dist/electron/electron/agent/llm/openai-provider.js +657 -0
- package/dist/electron/electron/agent/llm/openrouter-provider.js +232 -0
- package/dist/electron/electron/agent/llm/pricing.js +160 -0
- package/dist/electron/electron/agent/llm/provider-factory.js +880 -0
- package/dist/electron/electron/agent/llm/types.js +178 -0
- package/dist/electron/electron/agent/queue-manager.js +378 -0
- package/dist/electron/electron/agent/sandbox/docker-sandbox.js +402 -0
- package/dist/electron/electron/agent/sandbox/macos-sandbox.js +407 -0
- package/dist/electron/electron/agent/sandbox/runner.js +410 -0
- package/dist/electron/electron/agent/sandbox/sandbox-factory.js +228 -0
- package/dist/electron/electron/agent/sandbox/security-utils.js +258 -0
- package/dist/electron/electron/agent/search/brave-provider.js +119 -0
- package/dist/electron/electron/agent/search/google-provider.js +100 -0
- package/dist/electron/electron/agent/search/index.js +28 -0
- package/dist/electron/electron/agent/search/provider-factory.js +395 -0
- package/dist/electron/electron/agent/search/serpapi-provider.js +112 -0
- package/dist/electron/electron/agent/search/tavily-provider.js +90 -0
- package/dist/electron/electron/agent/search/types.js +40 -0
- package/dist/electron/electron/agent/security/index.js +12 -0
- package/dist/electron/electron/agent/security/input-sanitizer.js +303 -0
- package/dist/electron/electron/agent/security/output-filter.js +217 -0
- package/dist/electron/electron/agent/skill-eligibility.js +281 -0
- package/dist/electron/electron/agent/skill-registry.js +396 -0
- package/dist/electron/electron/agent/skills/document.js +878 -0
- package/dist/electron/electron/agent/skills/image-generator.js +225 -0
- package/dist/electron/electron/agent/skills/organizer.js +141 -0
- package/dist/electron/electron/agent/skills/presentation.js +367 -0
- package/dist/electron/electron/agent/skills/spreadsheet.js +165 -0
- package/dist/electron/electron/agent/tools/browser-tools.js +523 -0
- package/dist/electron/electron/agent/tools/builtin-settings.js +384 -0
- package/dist/electron/electron/agent/tools/canvas-tools.js +530 -0
- package/dist/electron/electron/agent/tools/cron-tools.js +577 -0
- package/dist/electron/electron/agent/tools/edit-tools.js +194 -0
- package/dist/electron/electron/agent/tools/file-tools.js +719 -0
- package/dist/electron/electron/agent/tools/glob-tools.js +283 -0
- package/dist/electron/electron/agent/tools/grep-tools.js +387 -0
- package/dist/electron/electron/agent/tools/image-tools.js +111 -0
- package/dist/electron/electron/agent/tools/mention-tools.js +282 -0
- package/dist/electron/electron/agent/tools/node-tools.js +476 -0
- package/dist/electron/electron/agent/tools/registry.js +2719 -0
- package/dist/electron/electron/agent/tools/search-tools.js +91 -0
- package/dist/electron/electron/agent/tools/shell-tools.js +574 -0
- package/dist/electron/electron/agent/tools/skill-tools.js +274 -0
- package/dist/electron/electron/agent/tools/system-tools.js +578 -0
- package/dist/electron/electron/agent/tools/web-fetch-tools.js +444 -0
- package/dist/electron/electron/agent/tools/x-tools.js +264 -0
- package/dist/electron/electron/agents/AgentRoleRepository.js +420 -0
- package/dist/electron/electron/agents/HeartbeatService.js +356 -0
- package/dist/electron/electron/agents/MentionRepository.js +197 -0
- package/dist/electron/electron/agents/TaskSubscriptionRepository.js +168 -0
- package/dist/electron/electron/agents/WorkingStateRepository.js +229 -0
- package/dist/electron/electron/canvas/canvas-manager.js +714 -0
- package/dist/electron/electron/canvas/canvas-preload.js +53 -0
- package/dist/electron/electron/canvas/canvas-protocol.js +195 -0
- package/dist/electron/electron/canvas/canvas-store.js +174 -0
- package/dist/electron/electron/canvas/index.js +13 -0
- package/dist/electron/electron/control-plane/client.js +364 -0
- package/dist/electron/electron/control-plane/handlers.js +572 -0
- package/dist/electron/electron/control-plane/index.js +41 -0
- package/dist/electron/electron/control-plane/node-manager.js +264 -0
- package/dist/electron/electron/control-plane/protocol.js +194 -0
- package/dist/electron/electron/control-plane/remote-client.js +437 -0
- package/dist/electron/electron/control-plane/server.js +640 -0
- package/dist/electron/electron/control-plane/settings.js +369 -0
- package/dist/electron/electron/control-plane/ssh-tunnel.js +549 -0
- package/dist/electron/electron/cron/index.js +30 -0
- package/dist/electron/electron/cron/schedule.js +190 -0
- package/dist/electron/electron/cron/service.js +614 -0
- package/dist/electron/electron/cron/store.js +155 -0
- package/dist/electron/electron/cron/types.js +82 -0
- package/dist/electron/electron/cron/webhook.js +258 -0
- package/dist/electron/electron/database/SecureSettingsRepository.js +444 -0
- package/dist/electron/electron/database/TaskLabelRepository.js +120 -0
- package/dist/electron/electron/database/repositories.js +1781 -0
- package/dist/electron/electron/database/schema.js +978 -0
- package/dist/electron/electron/extensions/index.js +33 -0
- package/dist/electron/electron/extensions/loader.js +313 -0
- package/dist/electron/electron/extensions/registry.js +485 -0
- package/dist/electron/electron/extensions/types.js +11 -0
- package/dist/electron/electron/gateway/channel-registry.js +1102 -0
- package/dist/electron/electron/gateway/channels/bluebubbles-client.js +479 -0
- package/dist/electron/electron/gateway/channels/bluebubbles.js +432 -0
- package/dist/electron/electron/gateway/channels/discord.js +975 -0
- package/dist/electron/electron/gateway/channels/email-client.js +593 -0
- package/dist/electron/electron/gateway/channels/email.js +443 -0
- package/dist/electron/electron/gateway/channels/google-chat.js +631 -0
- package/dist/electron/electron/gateway/channels/imessage-client.js +363 -0
- package/dist/electron/electron/gateway/channels/imessage.js +465 -0
- package/dist/electron/electron/gateway/channels/index.js +36 -0
- package/dist/electron/electron/gateway/channels/line-client.js +470 -0
- package/dist/electron/electron/gateway/channels/line.js +479 -0
- package/dist/electron/electron/gateway/channels/matrix-client.js +432 -0
- package/dist/electron/electron/gateway/channels/matrix.js +592 -0
- package/dist/electron/electron/gateway/channels/mattermost-client.js +394 -0
- package/dist/electron/electron/gateway/channels/mattermost.js +496 -0
- package/dist/electron/electron/gateway/channels/signal-client.js +500 -0
- package/dist/electron/electron/gateway/channels/signal.js +582 -0
- package/dist/electron/electron/gateway/channels/slack.js +415 -0
- package/dist/electron/electron/gateway/channels/teams.js +596 -0
- package/dist/electron/electron/gateway/channels/telegram.js +1390 -0
- package/dist/electron/electron/gateway/channels/twitch-client.js +502 -0
- package/dist/electron/electron/gateway/channels/twitch.js +396 -0
- package/dist/electron/electron/gateway/channels/types.js +8 -0
- package/dist/electron/electron/gateway/channels/whatsapp.js +953 -0
- package/dist/electron/electron/gateway/context-policy.js +268 -0
- package/dist/electron/electron/gateway/index.js +1063 -0
- package/dist/electron/electron/gateway/infrastructure.js +496 -0
- package/dist/electron/electron/gateway/router.js +2700 -0
- package/dist/electron/electron/gateway/security.js +375 -0
- package/dist/electron/electron/gateway/session.js +115 -0
- package/dist/electron/electron/gateway/tunnel.js +503 -0
- package/dist/electron/electron/guardrails/guardrail-manager.js +348 -0
- package/dist/electron/electron/hooks/gmail-watcher.js +300 -0
- package/dist/electron/electron/hooks/index.js +46 -0
- package/dist/electron/electron/hooks/mappings.js +381 -0
- package/dist/electron/electron/hooks/server.js +480 -0
- package/dist/electron/electron/hooks/settings.js +447 -0
- package/dist/electron/electron/hooks/types.js +41 -0
- package/dist/electron/electron/ipc/canvas-handlers.js +158 -0
- package/dist/electron/electron/ipc/handlers.js +3138 -0
- package/dist/electron/electron/ipc/mission-control-handlers.js +141 -0
- package/dist/electron/electron/main.js +448 -0
- package/dist/electron/electron/mcp/client/MCPClientManager.js +330 -0
- package/dist/electron/electron/mcp/client/MCPServerConnection.js +437 -0
- package/dist/electron/electron/mcp/client/transports/SSETransport.js +304 -0
- package/dist/electron/electron/mcp/client/transports/StdioTransport.js +307 -0
- package/dist/electron/electron/mcp/client/transports/WebSocketTransport.js +329 -0
- package/dist/electron/electron/mcp/host/MCPHostServer.js +354 -0
- package/dist/electron/electron/mcp/host/ToolAdapter.js +100 -0
- package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +497 -0
- package/dist/electron/electron/mcp/settings.js +446 -0
- package/dist/electron/electron/mcp/types.js +59 -0
- package/dist/electron/electron/memory/MemoryService.js +435 -0
- package/dist/electron/electron/notifications/index.js +17 -0
- package/dist/electron/electron/notifications/service.js +118 -0
- package/dist/electron/electron/notifications/store.js +144 -0
- package/dist/electron/electron/preload.js +842 -0
- package/dist/electron/electron/reports/StandupReportService.js +272 -0
- package/dist/electron/electron/security/concurrency.js +293 -0
- package/dist/electron/electron/security/index.js +15 -0
- package/dist/electron/electron/security/policy-manager.js +435 -0
- package/dist/electron/electron/settings/appearance-manager.js +193 -0
- package/dist/electron/electron/settings/personality-manager.js +724 -0
- package/dist/electron/electron/settings/x-manager.js +58 -0
- package/dist/electron/electron/tailscale/exposure.js +188 -0
- package/dist/electron/electron/tailscale/index.js +28 -0
- package/dist/electron/electron/tailscale/settings.js +205 -0
- package/dist/electron/electron/tailscale/tailscale.js +355 -0
- package/dist/electron/electron/tray/QuickInputWindow.js +568 -0
- package/dist/electron/electron/tray/TrayManager.js +895 -0
- package/dist/electron/electron/tray/index.js +9 -0
- package/dist/electron/electron/updater/index.js +6 -0
- package/dist/electron/electron/updater/update-manager.js +418 -0
- package/dist/electron/electron/utils/env-migration.js +209 -0
- package/dist/electron/electron/utils/process.js +102 -0
- package/dist/electron/electron/utils/rate-limiter.js +104 -0
- package/dist/electron/electron/utils/validation.js +419 -0
- package/dist/electron/electron/utils/x-cli.js +177 -0
- package/dist/electron/electron/voice/VoiceService.js +507 -0
- package/dist/electron/electron/voice/index.js +14 -0
- package/dist/electron/electron/voice/voice-settings-manager.js +359 -0
- package/dist/electron/shared/channelMessages.js +170 -0
- package/dist/electron/shared/types.js +1185 -0
- package/package.json +159 -0
- package/resources/skills/1password.json +10 -0
- package/resources/skills/add-documentation.json +31 -0
- package/resources/skills/analyze-csv.json +17 -0
- package/resources/skills/apple-notes.json +10 -0
- package/resources/skills/apple-reminders.json +10 -0
- package/resources/skills/auto-commenter.json +10 -0
- package/resources/skills/bear-notes.json +10 -0
- package/resources/skills/bird.json +35 -0
- package/resources/skills/blogwatcher.json +10 -0
- package/resources/skills/blucli.json +10 -0
- package/resources/skills/bluebubbles.json +10 -0
- package/resources/skills/camsnap.json +10 -0
- package/resources/skills/clean-imports.json +18 -0
- package/resources/skills/code-review.json +18 -0
- package/resources/skills/coding-agent.json +10 -0
- package/resources/skills/compare-files.json +23 -0
- package/resources/skills/convert-code.json +34 -0
- package/resources/skills/create-changelog.json +24 -0
- package/resources/skills/debug-error.json +17 -0
- package/resources/skills/dependency-check.json +10 -0
- package/resources/skills/discord.json +10 -0
- package/resources/skills/eightctl.json +10 -0
- package/resources/skills/explain-code.json +29 -0
- package/resources/skills/extract-todos.json +18 -0
- package/resources/skills/food-order.json +10 -0
- package/resources/skills/gemini.json +10 -0
- package/resources/skills/generate-readme.json +10 -0
- package/resources/skills/gifgrep.json +10 -0
- package/resources/skills/git-commit.json +10 -0
- package/resources/skills/github.json +10 -0
- package/resources/skills/gog.json +10 -0
- package/resources/skills/goplaces.json +10 -0
- package/resources/skills/himalaya.json +10 -0
- package/resources/skills/imsg.json +10 -0
- package/resources/skills/karpathy-guidelines.json +12 -0
- package/resources/skills/last30days.json +26 -0
- package/resources/skills/local-places.json +10 -0
- package/resources/skills/mcporter.json +10 -0
- package/resources/skills/model-usage.json +10 -0
- package/resources/skills/nano-banana-pro.json +10 -0
- package/resources/skills/nano-pdf.json +10 -0
- package/resources/skills/notion.json +10 -0
- package/resources/skills/obsidian.json +10 -0
- package/resources/skills/openai-image-gen.json +10 -0
- package/resources/skills/openai-whisper-api.json +10 -0
- package/resources/skills/openai-whisper.json +10 -0
- package/resources/skills/openhue.json +10 -0
- package/resources/skills/oracle.json +10 -0
- package/resources/skills/ordercli.json +10 -0
- package/resources/skills/peekaboo.json +10 -0
- package/resources/skills/project-structure.json +10 -0
- package/resources/skills/proofread.json +17 -0
- package/resources/skills/refactor-code.json +31 -0
- package/resources/skills/rename-symbol.json +23 -0
- package/resources/skills/sag.json +10 -0
- package/resources/skills/security-audit.json +18 -0
- package/resources/skills/session-logs.json +10 -0
- package/resources/skills/sherpa-onnx-tts.json +10 -0
- package/resources/skills/skill-creator.json +15 -0
- package/resources/skills/skill-hub.json +29 -0
- package/resources/skills/slack.json +10 -0
- package/resources/skills/songsee.json +10 -0
- package/resources/skills/sonoscli.json +10 -0
- package/resources/skills/spotify-player.json +10 -0
- package/resources/skills/startup-cfo.json +55 -0
- package/resources/skills/summarize-folder.json +18 -0
- package/resources/skills/summarize.json +10 -0
- package/resources/skills/things-mac.json +10 -0
- package/resources/skills/tmux.json +10 -0
- package/resources/skills/translate.json +36 -0
- package/resources/skills/trello.json +10 -0
- package/resources/skills/video-frames.json +10 -0
- package/resources/skills/voice-call.json +10 -0
- package/resources/skills/wacli.json +10 -0
- package/resources/skills/weather.json +10 -0
- package/resources/skills/write-tests.json +31 -0
- package/src/electron/activity/ActivityRepository.ts +238 -0
- package/src/electron/agent/browser/browser-service.ts +721 -0
- package/src/electron/agent/context-manager.ts +257 -0
- package/src/electron/agent/custom-skill-loader.ts +634 -0
- package/src/electron/agent/daemon.ts +1097 -0
- package/src/electron/agent/executor.ts +4017 -0
- package/src/electron/agent/llm/anthropic-provider.ts +175 -0
- package/src/electron/agent/llm/bedrock-provider.ts +236 -0
- package/src/electron/agent/llm/gemini-provider.ts +422 -0
- package/src/electron/agent/llm/index.ts +9 -0
- package/src/electron/agent/llm/ollama-provider.ts +347 -0
- package/src/electron/agent/llm/openai-oauth.ts +127 -0
- package/src/electron/agent/llm/openai-provider.ts +686 -0
- package/src/electron/agent/llm/openrouter-provider.ts +273 -0
- package/src/electron/agent/llm/pricing.ts +180 -0
- package/src/electron/agent/llm/provider-factory.ts +971 -0
- package/src/electron/agent/llm/types.ts +291 -0
- package/src/electron/agent/queue-manager.ts +408 -0
- package/src/electron/agent/sandbox/docker-sandbox.ts +453 -0
- package/src/electron/agent/sandbox/macos-sandbox.ts +426 -0
- package/src/electron/agent/sandbox/runner.ts +453 -0
- package/src/electron/agent/sandbox/sandbox-factory.ts +337 -0
- package/src/electron/agent/sandbox/security-utils.ts +251 -0
- package/src/electron/agent/search/brave-provider.ts +141 -0
- package/src/electron/agent/search/google-provider.ts +131 -0
- package/src/electron/agent/search/index.ts +6 -0
- package/src/electron/agent/search/provider-factory.ts +450 -0
- package/src/electron/agent/search/serpapi-provider.ts +138 -0
- package/src/electron/agent/search/tavily-provider.ts +108 -0
- package/src/electron/agent/search/types.ts +118 -0
- package/src/electron/agent/security/index.ts +20 -0
- package/src/electron/agent/security/input-sanitizer.ts +380 -0
- package/src/electron/agent/security/output-filter.ts +259 -0
- package/src/electron/agent/skill-eligibility.ts +334 -0
- package/src/electron/agent/skill-registry.ts +457 -0
- package/src/electron/agent/skills/document.ts +1070 -0
- package/src/electron/agent/skills/image-generator.ts +272 -0
- package/src/electron/agent/skills/organizer.ts +131 -0
- package/src/electron/agent/skills/presentation.ts +418 -0
- package/src/electron/agent/skills/spreadsheet.ts +166 -0
- package/src/electron/agent/tools/browser-tools.ts +546 -0
- package/src/electron/agent/tools/builtin-settings.ts +422 -0
- package/src/electron/agent/tools/canvas-tools.ts +572 -0
- package/src/electron/agent/tools/cron-tools.ts +723 -0
- package/src/electron/agent/tools/edit-tools.ts +196 -0
- package/src/electron/agent/tools/file-tools.ts +811 -0
- package/src/electron/agent/tools/glob-tools.ts +303 -0
- package/src/electron/agent/tools/grep-tools.ts +432 -0
- package/src/electron/agent/tools/image-tools.ts +126 -0
- package/src/electron/agent/tools/mention-tools.ts +371 -0
- package/src/electron/agent/tools/node-tools.ts +550 -0
- package/src/electron/agent/tools/registry.ts +3052 -0
- package/src/electron/agent/tools/search-tools.ts +111 -0
- package/src/electron/agent/tools/shell-tools.ts +651 -0
- package/src/electron/agent/tools/skill-tools.ts +340 -0
- package/src/electron/agent/tools/system-tools.ts +665 -0
- package/src/electron/agent/tools/web-fetch-tools.ts +528 -0
- package/src/electron/agent/tools/x-tools.ts +267 -0
- package/src/electron/agents/AgentRoleRepository.ts +557 -0
- package/src/electron/agents/HeartbeatService.ts +469 -0
- package/src/electron/agents/MentionRepository.ts +242 -0
- package/src/electron/agents/TaskSubscriptionRepository.ts +231 -0
- package/src/electron/agents/WorkingStateRepository.ts +278 -0
- package/src/electron/canvas/canvas-manager.ts +818 -0
- package/src/electron/canvas/canvas-preload.ts +102 -0
- package/src/electron/canvas/canvas-protocol.ts +174 -0
- package/src/electron/canvas/canvas-store.ts +200 -0
- package/src/electron/canvas/index.ts +8 -0
- package/src/electron/control-plane/client.ts +527 -0
- package/src/electron/control-plane/handlers.ts +723 -0
- package/src/electron/control-plane/index.ts +51 -0
- package/src/electron/control-plane/node-manager.ts +322 -0
- package/src/electron/control-plane/protocol.ts +269 -0
- package/src/electron/control-plane/remote-client.ts +517 -0
- package/src/electron/control-plane/server.ts +853 -0
- package/src/electron/control-plane/settings.ts +401 -0
- package/src/electron/control-plane/ssh-tunnel.ts +624 -0
- package/src/electron/cron/index.ts +9 -0
- package/src/electron/cron/schedule.ts +217 -0
- package/src/electron/cron/service.ts +743 -0
- package/src/electron/cron/store.ts +165 -0
- package/src/electron/cron/types.ts +291 -0
- package/src/electron/cron/webhook.ts +303 -0
- package/src/electron/database/SecureSettingsRepository.ts +514 -0
- package/src/electron/database/TaskLabelRepository.ts +148 -0
- package/src/electron/database/repositories.ts +2397 -0
- package/src/electron/database/schema.ts +1017 -0
- package/src/electron/extensions/index.ts +18 -0
- package/src/electron/extensions/loader.ts +336 -0
- package/src/electron/extensions/registry.ts +546 -0
- package/src/electron/extensions/types.ts +372 -0
- package/src/electron/gateway/channel-registry.ts +1267 -0
- package/src/electron/gateway/channels/bluebubbles-client.ts +641 -0
- package/src/electron/gateway/channels/bluebubbles.ts +509 -0
- package/src/electron/gateway/channels/discord.ts +1150 -0
- package/src/electron/gateway/channels/email-client.ts +708 -0
- package/src/electron/gateway/channels/email.ts +516 -0
- package/src/electron/gateway/channels/google-chat.ts +760 -0
- package/src/electron/gateway/channels/imessage-client.ts +473 -0
- package/src/electron/gateway/channels/imessage.ts +520 -0
- package/src/electron/gateway/channels/index.ts +21 -0
- package/src/electron/gateway/channels/line-client.ts +598 -0
- package/src/electron/gateway/channels/line.ts +559 -0
- package/src/electron/gateway/channels/matrix-client.ts +632 -0
- package/src/electron/gateway/channels/matrix.ts +655 -0
- package/src/electron/gateway/channels/mattermost-client.ts +526 -0
- package/src/electron/gateway/channels/mattermost.ts +550 -0
- package/src/electron/gateway/channels/signal-client.ts +722 -0
- package/src/electron/gateway/channels/signal.ts +666 -0
- package/src/electron/gateway/channels/slack.ts +458 -0
- package/src/electron/gateway/channels/teams.ts +681 -0
- package/src/electron/gateway/channels/telegram.ts +1727 -0
- package/src/electron/gateway/channels/twitch-client.ts +665 -0
- package/src/electron/gateway/channels/twitch.ts +468 -0
- package/src/electron/gateway/channels/types.ts +1002 -0
- package/src/electron/gateway/channels/whatsapp.ts +1101 -0
- package/src/electron/gateway/context-policy.ts +382 -0
- package/src/electron/gateway/index.ts +1274 -0
- package/src/electron/gateway/infrastructure.ts +645 -0
- package/src/electron/gateway/router.ts +3206 -0
- package/src/electron/gateway/security.ts +422 -0
- package/src/electron/gateway/session.ts +144 -0
- package/src/electron/gateway/tunnel.ts +626 -0
- package/src/electron/guardrails/guardrail-manager.ts +380 -0
- package/src/electron/hooks/gmail-watcher.ts +355 -0
- package/src/electron/hooks/index.ts +30 -0
- package/src/electron/hooks/mappings.ts +404 -0
- package/src/electron/hooks/server.ts +574 -0
- package/src/electron/hooks/settings.ts +466 -0
- package/src/electron/hooks/types.ts +245 -0
- package/src/electron/ipc/canvas-handlers.ts +223 -0
- package/src/electron/ipc/handlers.ts +3661 -0
- package/src/electron/ipc/mission-control-handlers.ts +182 -0
- package/src/electron/main.ts +496 -0
- package/src/electron/mcp/client/MCPClientManager.ts +406 -0
- package/src/electron/mcp/client/MCPServerConnection.ts +514 -0
- package/src/electron/mcp/client/transports/SSETransport.ts +360 -0
- package/src/electron/mcp/client/transports/StdioTransport.ts +355 -0
- package/src/electron/mcp/client/transports/WebSocketTransport.ts +384 -0
- package/src/electron/mcp/host/MCPHostServer.ts +388 -0
- package/src/electron/mcp/host/ToolAdapter.ts +140 -0
- package/src/electron/mcp/registry/MCPRegistryManager.ts +565 -0
- package/src/electron/mcp/settings.ts +468 -0
- package/src/electron/mcp/types.ts +371 -0
- package/src/electron/memory/MemoryService.ts +523 -0
- package/src/electron/notifications/index.ts +16 -0
- package/src/electron/notifications/service.ts +161 -0
- package/src/electron/notifications/store.ts +163 -0
- package/src/electron/preload.ts +2845 -0
- package/src/electron/reports/StandupReportService.ts +356 -0
- package/src/electron/security/concurrency.ts +333 -0
- package/src/electron/security/index.ts +17 -0
- package/src/electron/security/policy-manager.ts +539 -0
- package/src/electron/settings/appearance-manager.ts +182 -0
- package/src/electron/settings/personality-manager.ts +800 -0
- package/src/electron/settings/x-manager.ts +62 -0
- package/src/electron/tailscale/exposure.ts +262 -0
- package/src/electron/tailscale/index.ts +34 -0
- package/src/electron/tailscale/settings.ts +218 -0
- package/src/electron/tailscale/tailscale.ts +379 -0
- package/src/electron/tray/QuickInputWindow.ts +609 -0
- package/src/electron/tray/TrayManager.ts +1005 -0
- package/src/electron/tray/index.ts +6 -0
- package/src/electron/updater/index.ts +1 -0
- package/src/electron/updater/update-manager.ts +447 -0
- package/src/electron/utils/env-migration.ts +203 -0
- package/src/electron/utils/process.ts +124 -0
- package/src/electron/utils/rate-limiter.ts +130 -0
- package/src/electron/utils/validation.ts +493 -0
- package/src/electron/utils/x-cli.ts +198 -0
- package/src/electron/voice/VoiceService.ts +583 -0
- package/src/electron/voice/index.ts +9 -0
- package/src/electron/voice/voice-settings-manager.ts +403 -0
- package/src/renderer/App.tsx +775 -0
- package/src/renderer/components/ActivityFeed.tsx +407 -0
- package/src/renderer/components/ActivityFeedItem.tsx +285 -0
- package/src/renderer/components/AgentRoleCard.tsx +343 -0
- package/src/renderer/components/AgentRoleEditor.tsx +805 -0
- package/src/renderer/components/AgentSquadSettings.tsx +295 -0
- package/src/renderer/components/AgentWorkingStatePanel.tsx +411 -0
- package/src/renderer/components/AppearanceSettings.tsx +122 -0
- package/src/renderer/components/ApprovalDialog.tsx +100 -0
- package/src/renderer/components/BlueBubblesSettings.tsx +505 -0
- package/src/renderer/components/BuiltinToolsSettings.tsx +307 -0
- package/src/renderer/components/CanvasPreview.tsx +1189 -0
- package/src/renderer/components/CommandOutput.tsx +202 -0
- package/src/renderer/components/ContextPolicySettings.tsx +523 -0
- package/src/renderer/components/ControlPlaneSettings.tsx +1134 -0
- package/src/renderer/components/DisclaimerModal.tsx +124 -0
- package/src/renderer/components/DiscordSettings.tsx +436 -0
- package/src/renderer/components/EmailSettings.tsx +606 -0
- package/src/renderer/components/ExtensionsSettings.tsx +542 -0
- package/src/renderer/components/FileViewer.tsx +224 -0
- package/src/renderer/components/GoogleChatSettings.tsx +535 -0
- package/src/renderer/components/GuardrailSettings.tsx +487 -0
- package/src/renderer/components/HooksSettings.tsx +581 -0
- package/src/renderer/components/ImessageSettings.tsx +484 -0
- package/src/renderer/components/LineSettings.tsx +483 -0
- package/src/renderer/components/MCPRegistryBrowser.tsx +386 -0
- package/src/renderer/components/MCPSettings.tsx +943 -0
- package/src/renderer/components/MainContent.tsx +2433 -0
- package/src/renderer/components/MatrixSettings.tsx +510 -0
- package/src/renderer/components/MattermostSettings.tsx +473 -0
- package/src/renderer/components/MemorySettings.tsx +247 -0
- package/src/renderer/components/MentionBadge.tsx +87 -0
- package/src/renderer/components/MentionInput.tsx +409 -0
- package/src/renderer/components/MentionList.tsx +476 -0
- package/src/renderer/components/MissionControlPanel.tsx +1995 -0
- package/src/renderer/components/NodesSettings.tsx +316 -0
- package/src/renderer/components/NotificationPanel.tsx +481 -0
- package/src/renderer/components/Onboarding/AwakeningOrb.tsx +44 -0
- package/src/renderer/components/Onboarding/Onboarding.tsx +443 -0
- package/src/renderer/components/Onboarding/TypewriterText.tsx +102 -0
- package/src/renderer/components/Onboarding/index.ts +3 -0
- package/src/renderer/components/OnboardingModal.tsx +698 -0
- package/src/renderer/components/PairingCodeDisplay.tsx +324 -0
- package/src/renderer/components/PersonalitySettings.tsx +597 -0
- package/src/renderer/components/QueueSettings.tsx +119 -0
- package/src/renderer/components/QuickTaskFAB.tsx +71 -0
- package/src/renderer/components/RightPanel.tsx +413 -0
- package/src/renderer/components/ScheduledTasksSettings.tsx +1328 -0
- package/src/renderer/components/SearchSettings.tsx +328 -0
- package/src/renderer/components/Settings.tsx +1504 -0
- package/src/renderer/components/Sidebar.tsx +344 -0
- package/src/renderer/components/SignalSettings.tsx +673 -0
- package/src/renderer/components/SkillHubBrowser.tsx +458 -0
- package/src/renderer/components/SkillParameterModal.tsx +185 -0
- package/src/renderer/components/SkillsSettings.tsx +451 -0
- package/src/renderer/components/SlackSettings.tsx +442 -0
- package/src/renderer/components/StandupReportViewer.tsx +614 -0
- package/src/renderer/components/TaskBoard.tsx +498 -0
- package/src/renderer/components/TaskBoardCard.tsx +357 -0
- package/src/renderer/components/TaskBoardColumn.tsx +211 -0
- package/src/renderer/components/TaskLabelManager.tsx +472 -0
- package/src/renderer/components/TaskQueuePanel.tsx +144 -0
- package/src/renderer/components/TaskQuickActions.tsx +492 -0
- package/src/renderer/components/TaskTimeline.tsx +216 -0
- package/src/renderer/components/TaskView.tsx +162 -0
- package/src/renderer/components/TeamsSettings.tsx +518 -0
- package/src/renderer/components/TelegramSettings.tsx +421 -0
- package/src/renderer/components/Toast.tsx +76 -0
- package/src/renderer/components/TraySettings.tsx +189 -0
- package/src/renderer/components/TwitchSettings.tsx +511 -0
- package/src/renderer/components/UpdateSettings.tsx +295 -0
- package/src/renderer/components/VoiceIndicator.tsx +270 -0
- package/src/renderer/components/VoiceSettings.tsx +867 -0
- package/src/renderer/components/WhatsAppSettings.tsx +721 -0
- package/src/renderer/components/WorkingStateEditor.tsx +309 -0
- package/src/renderer/components/WorkingStateHistory.tsx +481 -0
- package/src/renderer/components/WorkspaceSelector.tsx +150 -0
- package/src/renderer/components/XSettings.tsx +311 -0
- package/src/renderer/global.d.ts +9 -0
- package/src/renderer/hooks/useAgentContext.ts +153 -0
- package/src/renderer/hooks/useOnboardingFlow.ts +548 -0
- package/src/renderer/hooks/useVoiceInput.ts +268 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +10 -0
- package/src/renderer/public/cowork-os-logo.png +0 -0
- package/src/renderer/quick-input.html +164 -0
- package/src/renderer/styles/index.css +14504 -0
- package/src/renderer/utils/agentMessages.ts +749 -0
- package/src/renderer/utils/voice-directives.ts +169 -0
- package/src/shared/channelMessages.ts +213 -0
- package/src/shared/types.ts +3608 -0
- package/tsconfig.electron.json +26 -0
- package/tsconfig.json +26 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +23 -0
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhatsApp Channel Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the ChannelAdapter interface using Baileys for WhatsApp Web API.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - QR code authentication for WhatsApp Web
|
|
8
|
+
* - Multi-file auth state persistence
|
|
9
|
+
* - Message deduplication
|
|
10
|
+
* - Group and DM message handling
|
|
11
|
+
* - Media message support (images, documents, audio, video)
|
|
12
|
+
* - Typing indicators (composing presence)
|
|
13
|
+
* - Message reactions
|
|
14
|
+
* - Auto-reconnection with exponential backoff
|
|
15
|
+
* - Read receipts
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
makeWASocket,
|
|
20
|
+
useMultiFileAuthState,
|
|
21
|
+
fetchLatestBaileysVersion,
|
|
22
|
+
makeCacheableSignalKeyStore,
|
|
23
|
+
DisconnectReason,
|
|
24
|
+
isJidGroup,
|
|
25
|
+
downloadContentFromMessage,
|
|
26
|
+
type WASocket,
|
|
27
|
+
type WAMessage,
|
|
28
|
+
type AnyMessageContent,
|
|
29
|
+
type ConnectionState,
|
|
30
|
+
type proto,
|
|
31
|
+
type DownloadableMessage,
|
|
32
|
+
} from '@whiskeysockets/baileys';
|
|
33
|
+
import * as fs from 'fs';
|
|
34
|
+
import * as path from 'path';
|
|
35
|
+
import { app } from 'electron';
|
|
36
|
+
import {
|
|
37
|
+
ChannelAdapter,
|
|
38
|
+
ChannelStatus,
|
|
39
|
+
IncomingMessage,
|
|
40
|
+
OutgoingMessage,
|
|
41
|
+
MessageHandler,
|
|
42
|
+
ErrorHandler,
|
|
43
|
+
StatusHandler,
|
|
44
|
+
ChannelInfo,
|
|
45
|
+
MessageAttachment,
|
|
46
|
+
CallbackQueryHandler,
|
|
47
|
+
WhatsAppConfig,
|
|
48
|
+
} from './types';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Exponential backoff configuration
|
|
52
|
+
*/
|
|
53
|
+
interface BackoffConfig {
|
|
54
|
+
initialDelay: number;
|
|
55
|
+
maxDelay: number;
|
|
56
|
+
multiplier: number;
|
|
57
|
+
jitter: number;
|
|
58
|
+
maxAttempts: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* QR code event handler
|
|
63
|
+
*/
|
|
64
|
+
export type QrCodeHandler = (qr: string) => void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* WhatsApp inbound message
|
|
68
|
+
*/
|
|
69
|
+
interface WhatsAppInboundMessage {
|
|
70
|
+
id?: string;
|
|
71
|
+
from: string;
|
|
72
|
+
to: string;
|
|
73
|
+
body: string;
|
|
74
|
+
timestamp?: number;
|
|
75
|
+
chatType: 'direct' | 'group';
|
|
76
|
+
chatId: string;
|
|
77
|
+
senderJid?: string;
|
|
78
|
+
senderE164?: string;
|
|
79
|
+
senderName?: string;
|
|
80
|
+
groupSubject?: string;
|
|
81
|
+
mediaPath?: string;
|
|
82
|
+
mediaType?: string;
|
|
83
|
+
replyToId?: string;
|
|
84
|
+
replyToBody?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class WhatsAppAdapter implements ChannelAdapter {
|
|
88
|
+
readonly type = 'whatsapp' as const;
|
|
89
|
+
|
|
90
|
+
private sock: WASocket | null = null;
|
|
91
|
+
private _status: ChannelStatus = 'disconnected';
|
|
92
|
+
private _selfJid?: string;
|
|
93
|
+
private _selfE164?: string;
|
|
94
|
+
private messageHandlers: MessageHandler[] = [];
|
|
95
|
+
private errorHandlers: ErrorHandler[] = [];
|
|
96
|
+
private statusHandlers: StatusHandler[] = [];
|
|
97
|
+
private qrCodeHandlers: QrCodeHandler[] = [];
|
|
98
|
+
private config: WhatsAppConfig;
|
|
99
|
+
private authDir: string;
|
|
100
|
+
|
|
101
|
+
// Message deduplication
|
|
102
|
+
private processedMessages: Map<string, number> = new Map();
|
|
103
|
+
private readonly DEDUP_CACHE_TTL = 60000; // 1 minute
|
|
104
|
+
private readonly DEDUP_CACHE_MAX_SIZE = 1000;
|
|
105
|
+
private dedupCleanupTimer?: ReturnType<typeof setTimeout>;
|
|
106
|
+
|
|
107
|
+
// Connection state
|
|
108
|
+
private connectedAtMs: number = 0;
|
|
109
|
+
private isReconnecting = false;
|
|
110
|
+
private backoffAttempt = 0;
|
|
111
|
+
private backoffTimer?: ReturnType<typeof setTimeout>;
|
|
112
|
+
private currentQr?: string;
|
|
113
|
+
|
|
114
|
+
private readonly DEFAULT_BACKOFF: BackoffConfig = {
|
|
115
|
+
initialDelay: 2000,
|
|
116
|
+
maxDelay: 30000,
|
|
117
|
+
multiplier: 1.8,
|
|
118
|
+
jitter: 0.25,
|
|
119
|
+
maxAttempts: 10,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Group metadata cache
|
|
123
|
+
private groupMetaCache: Map<string, { subject?: string; expires: number }> = new Map();
|
|
124
|
+
private readonly GROUP_META_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
125
|
+
|
|
126
|
+
constructor(config: WhatsAppConfig) {
|
|
127
|
+
this.config = {
|
|
128
|
+
deduplicationEnabled: true,
|
|
129
|
+
sendReadReceipts: true,
|
|
130
|
+
printQrToTerminal: false,
|
|
131
|
+
selfChatMode: true, // Default to self-chat mode since most users use their own number
|
|
132
|
+
responsePrefix: '🤖', // Default prefix for bot responses
|
|
133
|
+
...config,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// In self-chat mode, disable read receipts by default
|
|
137
|
+
if (this.config.selfChatMode && config.sendReadReceipts === undefined) {
|
|
138
|
+
this.config.sendReadReceipts = false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Set auth directory
|
|
142
|
+
this.authDir = config.authDir || path.join(app.getPath('userData'), 'whatsapp-auth');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if self-chat mode is enabled
|
|
147
|
+
*/
|
|
148
|
+
get isSelfChatMode(): boolean {
|
|
149
|
+
return this.config.selfChatMode === true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get the response prefix for bot messages
|
|
154
|
+
*/
|
|
155
|
+
get responsePrefix(): string {
|
|
156
|
+
return this.config.responsePrefix || '🤖';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get status(): ChannelStatus {
|
|
160
|
+
return this._status;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get botUsername(): string | undefined {
|
|
164
|
+
return this._selfE164;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get the current QR code (if in login state)
|
|
169
|
+
*/
|
|
170
|
+
get qrCode(): string | undefined {
|
|
171
|
+
return this.currentQr;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if WhatsApp auth credentials exist
|
|
176
|
+
*/
|
|
177
|
+
async hasCredentials(): Promise<boolean> {
|
|
178
|
+
const credsPath = path.join(this.authDir, 'creds.json');
|
|
179
|
+
return fs.existsSync(credsPath);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Connect to WhatsApp Web
|
|
184
|
+
*/
|
|
185
|
+
async connect(): Promise<void> {
|
|
186
|
+
if (this._status === 'connected' || this._status === 'connecting') {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.setStatus('connecting');
|
|
191
|
+
this.resetBackoff();
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Ensure auth directory exists
|
|
195
|
+
await this.ensureDir(this.authDir);
|
|
196
|
+
|
|
197
|
+
// Load auth state
|
|
198
|
+
const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
|
|
199
|
+
|
|
200
|
+
// Get latest Baileys version
|
|
201
|
+
const { version } = await fetchLatestBaileysVersion();
|
|
202
|
+
|
|
203
|
+
// Create silent logger to suppress Baileys logs
|
|
204
|
+
const logger = {
|
|
205
|
+
level: 'silent' as const,
|
|
206
|
+
trace: () => {},
|
|
207
|
+
debug: () => {},
|
|
208
|
+
info: () => {},
|
|
209
|
+
warn: () => {},
|
|
210
|
+
error: () => {},
|
|
211
|
+
fatal: () => {},
|
|
212
|
+
child: () => logger,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Create WhatsApp socket
|
|
216
|
+
this.sock = makeWASocket({
|
|
217
|
+
auth: {
|
|
218
|
+
creds: state.creds,
|
|
219
|
+
keys: makeCacheableSignalKeyStore(state.keys, logger as any),
|
|
220
|
+
},
|
|
221
|
+
version,
|
|
222
|
+
logger: logger as any,
|
|
223
|
+
// Note: printQRInTerminal is deprecated - QR codes are handled via connection.update event
|
|
224
|
+
browser: ['CoWork-OS', 'Desktop', '1.0.0'],
|
|
225
|
+
syncFullHistory: false,
|
|
226
|
+
markOnlineOnConnect: false,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Handle credential updates
|
|
230
|
+
this.sock.ev.on('creds.update', saveCreds);
|
|
231
|
+
|
|
232
|
+
// Handle connection updates
|
|
233
|
+
this.sock.ev.on('connection.update', (update) => {
|
|
234
|
+
this.handleConnectionUpdate(update);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Handle incoming messages
|
|
238
|
+
this.sock.ev.on('messages.upsert', (upsert) => {
|
|
239
|
+
this.handleMessagesUpsert(upsert);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Start deduplication cleanup
|
|
243
|
+
if (this.config.deduplicationEnabled) {
|
|
244
|
+
this.startDedupCleanup();
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
248
|
+
this.setStatus('error', err);
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Handle connection state updates
|
|
255
|
+
*/
|
|
256
|
+
private handleConnectionUpdate(update: Partial<ConnectionState>): void {
|
|
257
|
+
const { connection, lastDisconnect, qr } = update;
|
|
258
|
+
|
|
259
|
+
// Handle QR code for authentication
|
|
260
|
+
if (qr) {
|
|
261
|
+
this.currentQr = qr;
|
|
262
|
+
console.log('WhatsApp QR code received - scan with WhatsApp mobile app');
|
|
263
|
+
|
|
264
|
+
// Notify QR handlers
|
|
265
|
+
for (const handler of this.qrCodeHandlers) {
|
|
266
|
+
try {
|
|
267
|
+
handler(qr);
|
|
268
|
+
} catch (e) {
|
|
269
|
+
console.error('Error in QR code handler:', e);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Handle connection open
|
|
275
|
+
if (connection === 'open') {
|
|
276
|
+
this.currentQr = undefined;
|
|
277
|
+
this.connectedAtMs = Date.now();
|
|
278
|
+
this._selfJid = this.sock?.user?.id;
|
|
279
|
+
this._selfE164 = this._selfJid ? this.jidToE164(this._selfJid) ?? undefined : undefined;
|
|
280
|
+
|
|
281
|
+
console.log(`WhatsApp connected as ${this._selfE164 || this._selfJid}`);
|
|
282
|
+
this.setStatus('connected');
|
|
283
|
+
this.resetBackoff();
|
|
284
|
+
|
|
285
|
+
// Send available presence
|
|
286
|
+
this.sock?.sendPresenceUpdate('available').catch(() => {});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Handle connection close
|
|
290
|
+
if (connection === 'close') {
|
|
291
|
+
this.currentQr = undefined;
|
|
292
|
+
const statusCode = this.getStatusCode(lastDisconnect?.error);
|
|
293
|
+
|
|
294
|
+
if (statusCode === DisconnectReason.loggedOut) {
|
|
295
|
+
console.error('WhatsApp session logged out');
|
|
296
|
+
this.setStatus('error', new Error('WhatsApp session logged out. Please re-authenticate.'));
|
|
297
|
+
// Clear credentials on logout
|
|
298
|
+
this.clearCredentials().catch(() => {});
|
|
299
|
+
} else if (statusCode === DisconnectReason.restartRequired) {
|
|
300
|
+
console.log('WhatsApp restart required, reconnecting...');
|
|
301
|
+
this.attemptReconnection();
|
|
302
|
+
} else {
|
|
303
|
+
console.log(`WhatsApp connection closed (status: ${statusCode}), attempting reconnection...`);
|
|
304
|
+
this.attemptReconnection();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Handle incoming messages
|
|
311
|
+
*/
|
|
312
|
+
private async handleMessagesUpsert(upsert: { type?: string; messages?: WAMessage[] }): Promise<void> {
|
|
313
|
+
if (upsert.type !== 'notify' && upsert.type !== 'append') return;
|
|
314
|
+
|
|
315
|
+
for (const msg of upsert.messages ?? []) {
|
|
316
|
+
try {
|
|
317
|
+
await this.processInboundMessage(msg, upsert.type);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error('Error processing WhatsApp message:', error);
|
|
320
|
+
this.handleError(
|
|
321
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
322
|
+
'messageProcessing'
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Process a single inbound message
|
|
330
|
+
*/
|
|
331
|
+
private async processInboundMessage(msg: WAMessage, upsertType: string): Promise<void> {
|
|
332
|
+
const id = msg.key?.id;
|
|
333
|
+
const remoteJid = msg.key?.remoteJid;
|
|
334
|
+
if (!remoteJid) return;
|
|
335
|
+
|
|
336
|
+
// Skip status and broadcast messages
|
|
337
|
+
if (remoteJid.endsWith('@status') || remoteJid.endsWith('@broadcast')) return;
|
|
338
|
+
|
|
339
|
+
// CRITICAL: In self-chat mode, ONLY process messages from self-chat
|
|
340
|
+
// This prevents the bot from responding to messages sent to other people
|
|
341
|
+
if (this.isSelfChatMode && this._selfJid) {
|
|
342
|
+
// Normalize JIDs by removing device suffix (e.g., "123:5@s.whatsapp.net" -> "123@s.whatsapp.net")
|
|
343
|
+
const normalizeJid = (jid: string) => jid.replace(/:[\d]+@/, '@');
|
|
344
|
+
const selfJidNormalized = normalizeJid(this._selfJid);
|
|
345
|
+
const remoteJidNormalized = normalizeJid(remoteJid);
|
|
346
|
+
|
|
347
|
+
if (remoteJidNormalized !== selfJidNormalized) {
|
|
348
|
+
// Message is NOT in self-chat, silently ignore it
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Deduplication
|
|
354
|
+
if (id && this.config.deduplicationEnabled) {
|
|
355
|
+
const dedupeKey = `${remoteJid}:${id}`;
|
|
356
|
+
if (this.processedMessages.has(dedupeKey)) return;
|
|
357
|
+
this.processedMessages.set(dedupeKey, Date.now());
|
|
358
|
+
|
|
359
|
+
// Cleanup if cache is too large
|
|
360
|
+
if (this.processedMessages.size > this.DEDUP_CACHE_MAX_SIZE) {
|
|
361
|
+
this.cleanupDedupCache();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const isGroup = isJidGroup(remoteJid) === true;
|
|
366
|
+
const participantJid = msg.key?.participant;
|
|
367
|
+
const from = isGroup ? remoteJid : this.jidToE164(remoteJid) || remoteJid;
|
|
368
|
+
const senderE164 = isGroup
|
|
369
|
+
? participantJid ? this.jidToE164(participantJid) : null
|
|
370
|
+
: from;
|
|
371
|
+
|
|
372
|
+
// Check access control
|
|
373
|
+
if (this.config.allowedNumbers && this.config.allowedNumbers.length > 0) {
|
|
374
|
+
const senderNumber = senderE164?.replace(/[^0-9]/g, '');
|
|
375
|
+
if (senderNumber && !this.config.allowedNumbers.includes(senderNumber)) {
|
|
376
|
+
console.log(`WhatsApp: Ignoring message from unauthorized number: ${senderNumber}`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Get group metadata if applicable
|
|
382
|
+
let groupSubject: string | undefined;
|
|
383
|
+
if (isGroup && this.sock) {
|
|
384
|
+
const meta = await this.getGroupMeta(remoteJid);
|
|
385
|
+
groupSubject = meta.subject;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Extract message text
|
|
389
|
+
const body = this.extractText(msg.message);
|
|
390
|
+
if (!body) {
|
|
391
|
+
// Check for media placeholder
|
|
392
|
+
const mediaPlaceholder = this.extractMediaPlaceholder(msg.message);
|
|
393
|
+
if (!mediaPlaceholder) return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Send read receipt
|
|
397
|
+
if (id && this.config.sendReadReceipts && upsertType === 'notify') {
|
|
398
|
+
try {
|
|
399
|
+
await this.sock?.readMessages([{
|
|
400
|
+
remoteJid,
|
|
401
|
+
id,
|
|
402
|
+
participant: participantJid,
|
|
403
|
+
fromMe: false,
|
|
404
|
+
}]);
|
|
405
|
+
} catch {
|
|
406
|
+
// Ignore read receipt errors
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Skip history/offline catch-up messages
|
|
411
|
+
if (upsertType === 'append') return;
|
|
412
|
+
|
|
413
|
+
const messageTimestampMs = msg.messageTimestamp
|
|
414
|
+
? Number(msg.messageTimestamp) * 1000
|
|
415
|
+
: undefined;
|
|
416
|
+
|
|
417
|
+
// Download audio attachment if present
|
|
418
|
+
const attachments: MessageAttachment[] = [];
|
|
419
|
+
if (msg.message?.audioMessage) {
|
|
420
|
+
const audioAttachment = await this.downloadAudioAttachment(msg.message);
|
|
421
|
+
if (audioAttachment) {
|
|
422
|
+
attachments.push(audioAttachment);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Create incoming message
|
|
427
|
+
const incomingMessage: IncomingMessage = {
|
|
428
|
+
messageId: id || `wa-${Date.now()}`,
|
|
429
|
+
channel: 'whatsapp',
|
|
430
|
+
userId: senderE164 || participantJid || remoteJid,
|
|
431
|
+
userName: msg.pushName || senderE164 || 'Unknown',
|
|
432
|
+
chatId: remoteJid,
|
|
433
|
+
text: body || (attachments.length === 0 ? this.extractMediaPlaceholder(msg.message) : '') || '',
|
|
434
|
+
timestamp: messageTimestampMs ? new Date(messageTimestampMs) : new Date(),
|
|
435
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
436
|
+
raw: msg,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Notify message handlers
|
|
440
|
+
await this.handleIncomingMessage(incomingMessage);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Disconnect from WhatsApp
|
|
445
|
+
*/
|
|
446
|
+
async disconnect(): Promise<void> {
|
|
447
|
+
this.resetBackoff();
|
|
448
|
+
|
|
449
|
+
// Clear timers
|
|
450
|
+
if (this.dedupCleanupTimer) {
|
|
451
|
+
clearInterval(this.dedupCleanupTimer);
|
|
452
|
+
this.dedupCleanupTimer = undefined;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Clear caches
|
|
456
|
+
this.processedMessages.clear();
|
|
457
|
+
this.groupMetaCache.clear();
|
|
458
|
+
this.currentQr = undefined;
|
|
459
|
+
|
|
460
|
+
if (this.sock) {
|
|
461
|
+
try {
|
|
462
|
+
this.sock.ws?.close();
|
|
463
|
+
} catch {
|
|
464
|
+
// Ignore close errors
|
|
465
|
+
}
|
|
466
|
+
this.sock = null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
this._selfJid = undefined;
|
|
470
|
+
this._selfE164 = undefined;
|
|
471
|
+
this.setStatus('disconnected');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Convert standard Markdown to WhatsApp-compatible formatting
|
|
476
|
+
* WhatsApp supports: *bold*, _italic_, ~strikethrough~, ```monospace```
|
|
477
|
+
*/
|
|
478
|
+
private convertMarkdownToWhatsApp(text: string): string {
|
|
479
|
+
let result = text;
|
|
480
|
+
|
|
481
|
+
// Convert headers (### Header) to bold text
|
|
482
|
+
result = result.replace(/^#{1,6}\s+(.+)$/gm, '*$1*');
|
|
483
|
+
|
|
484
|
+
// Convert **bold** to *bold* (WhatsApp style)
|
|
485
|
+
result = result.replace(/\*\*(.+?)\*\*/g, '*$1*');
|
|
486
|
+
|
|
487
|
+
// Convert __bold__ to *bold*
|
|
488
|
+
result = result.replace(/__(.+?)__/g, '*$1*');
|
|
489
|
+
|
|
490
|
+
// Convert _italic_ - already WhatsApp compatible, but handle markdown style
|
|
491
|
+
// Note: Single underscores are already WhatsApp italic
|
|
492
|
+
|
|
493
|
+
// Convert ~~strikethrough~~ to ~strikethrough~
|
|
494
|
+
result = result.replace(/~~(.+?)~~/g, '~$1~');
|
|
495
|
+
|
|
496
|
+
// Convert inline code `code` to monospace (WhatsApp uses triple backticks but single works in some clients)
|
|
497
|
+
// Keep as-is since WhatsApp renders `code` reasonably
|
|
498
|
+
|
|
499
|
+
// Convert code blocks ```code``` - already WhatsApp compatible
|
|
500
|
+
|
|
501
|
+
// Convert [link text](url) to "link text (url)" since WhatsApp auto-links URLs
|
|
502
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)');
|
|
503
|
+
|
|
504
|
+
// Convert horizontal rules (---, ***, ___) to a line
|
|
505
|
+
result = result.replace(/^[-*_]{3,}$/gm, '───────────');
|
|
506
|
+
|
|
507
|
+
// Clean up excessive newlines
|
|
508
|
+
result = result.replace(/\n{3,}/g, '\n\n');
|
|
509
|
+
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Send a message to a WhatsApp chat
|
|
515
|
+
*/
|
|
516
|
+
async sendMessage(message: OutgoingMessage): Promise<string> {
|
|
517
|
+
if (!this.sock || this._status !== 'connected') {
|
|
518
|
+
throw new Error('WhatsApp is not connected');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const jid = this.toWhatsAppJid(message.chatId);
|
|
522
|
+
let messageId = '';
|
|
523
|
+
|
|
524
|
+
// Convert markdown to WhatsApp formatting and apply response prefix
|
|
525
|
+
let textToSend = message.text ? this.convertMarkdownToWhatsApp(message.text) : message.text;
|
|
526
|
+
if (this.isSelfChatMode && textToSend && textToSend.trim()) {
|
|
527
|
+
const prefix = this.responsePrefix;
|
|
528
|
+
// Only add prefix if not already present
|
|
529
|
+
if (!textToSend.startsWith(prefix)) {
|
|
530
|
+
textToSend = `${prefix} ${textToSend}`;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Send media attachments first
|
|
535
|
+
if (message.attachments && message.attachments.length > 0) {
|
|
536
|
+
for (const attachment of message.attachments) {
|
|
537
|
+
const result = await this.sendMediaAttachment(jid, attachment, textToSend);
|
|
538
|
+
messageId = result;
|
|
539
|
+
// Clear text after first media with caption
|
|
540
|
+
if (textToSend) {
|
|
541
|
+
textToSend = '';
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Send text message if no media or text remains
|
|
547
|
+
if (textToSend && textToSend.trim()) {
|
|
548
|
+
// Send composing presence
|
|
549
|
+
await this.sendComposingTo(jid);
|
|
550
|
+
|
|
551
|
+
const result = await this.sock.sendMessage(jid, { text: textToSend });
|
|
552
|
+
messageId = result?.key?.id || `wa-${Date.now()}`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return messageId;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Send a media attachment
|
|
560
|
+
*/
|
|
561
|
+
private async sendMediaAttachment(
|
|
562
|
+
jid: string,
|
|
563
|
+
attachment: MessageAttachment,
|
|
564
|
+
caption?: string
|
|
565
|
+
): Promise<string> {
|
|
566
|
+
if (!this.sock) throw new Error('WhatsApp is not connected');
|
|
567
|
+
|
|
568
|
+
let content: AnyMessageContent;
|
|
569
|
+
|
|
570
|
+
if (attachment.type === 'image' && attachment.url) {
|
|
571
|
+
const buffer = fs.readFileSync(attachment.url);
|
|
572
|
+
content = {
|
|
573
|
+
image: buffer,
|
|
574
|
+
caption,
|
|
575
|
+
mimetype: attachment.mimeType || 'image/jpeg',
|
|
576
|
+
};
|
|
577
|
+
} else if (attachment.type === 'document' && attachment.url) {
|
|
578
|
+
const buffer = fs.readFileSync(attachment.url);
|
|
579
|
+
content = {
|
|
580
|
+
document: buffer,
|
|
581
|
+
fileName: attachment.fileName || path.basename(attachment.url),
|
|
582
|
+
mimetype: attachment.mimeType || 'application/octet-stream',
|
|
583
|
+
caption,
|
|
584
|
+
};
|
|
585
|
+
} else if (attachment.type === 'audio' && attachment.url) {
|
|
586
|
+
const buffer = fs.readFileSync(attachment.url);
|
|
587
|
+
content = {
|
|
588
|
+
audio: buffer,
|
|
589
|
+
mimetype: attachment.mimeType || 'audio/mpeg',
|
|
590
|
+
ptt: true, // Voice note
|
|
591
|
+
};
|
|
592
|
+
} else if (attachment.type === 'video' && attachment.url) {
|
|
593
|
+
const buffer = fs.readFileSync(attachment.url);
|
|
594
|
+
content = {
|
|
595
|
+
video: buffer,
|
|
596
|
+
caption,
|
|
597
|
+
mimetype: attachment.mimeType || 'video/mp4',
|
|
598
|
+
};
|
|
599
|
+
} else {
|
|
600
|
+
throw new Error(`Unsupported attachment type: ${attachment.type}`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const result = await this.sock.sendMessage(jid, content);
|
|
604
|
+
return result?.key?.id || `wa-${Date.now()}`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Send composing (typing) indicator
|
|
609
|
+
*/
|
|
610
|
+
async sendComposingTo(chatId: string): Promise<void> {
|
|
611
|
+
if (!this.sock) return;
|
|
612
|
+
|
|
613
|
+
const jid = this.toWhatsAppJid(chatId);
|
|
614
|
+
try {
|
|
615
|
+
await this.sock.sendPresenceUpdate('composing', jid);
|
|
616
|
+
} catch {
|
|
617
|
+
// Ignore presence errors
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Send typing indicator (alias for sendComposingTo)
|
|
623
|
+
*/
|
|
624
|
+
async sendTyping(chatId: string): Promise<void> {
|
|
625
|
+
await this.sendComposingTo(chatId);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Edit an existing message (not supported by WhatsApp Web API)
|
|
630
|
+
*/
|
|
631
|
+
async editMessage(_chatId: string, _messageId: string, _text: string): Promise<void> {
|
|
632
|
+
throw new Error('WhatsApp does not support message editing');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Delete a message
|
|
637
|
+
*/
|
|
638
|
+
async deleteMessage(chatId: string, messageId: string): Promise<void> {
|
|
639
|
+
if (!this.sock) throw new Error('WhatsApp is not connected');
|
|
640
|
+
|
|
641
|
+
const jid = this.toWhatsAppJid(chatId);
|
|
642
|
+
await this.sock.sendMessage(jid, {
|
|
643
|
+
delete: {
|
|
644
|
+
remoteJid: jid,
|
|
645
|
+
fromMe: true,
|
|
646
|
+
id: messageId,
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Send a document/file
|
|
653
|
+
*/
|
|
654
|
+
async sendDocument(chatId: string, filePath: string, caption?: string): Promise<string> {
|
|
655
|
+
if (!this.sock) throw new Error('WhatsApp is not connected');
|
|
656
|
+
|
|
657
|
+
const jid = this.toWhatsAppJid(chatId);
|
|
658
|
+
const buffer = fs.readFileSync(filePath);
|
|
659
|
+
const fileName = path.basename(filePath);
|
|
660
|
+
|
|
661
|
+
const result = await this.sock.sendMessage(jid, {
|
|
662
|
+
document: buffer,
|
|
663
|
+
fileName,
|
|
664
|
+
mimetype: 'application/octet-stream',
|
|
665
|
+
caption,
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
return result?.key?.id || `wa-${Date.now()}`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Send a photo/image
|
|
673
|
+
*/
|
|
674
|
+
async sendPhoto(chatId: string, filePath: string, caption?: string): Promise<string> {
|
|
675
|
+
if (!this.sock) throw new Error('WhatsApp is not connected');
|
|
676
|
+
|
|
677
|
+
const jid = this.toWhatsAppJid(chatId);
|
|
678
|
+
const buffer = fs.readFileSync(filePath);
|
|
679
|
+
|
|
680
|
+
const result = await this.sock.sendMessage(jid, {
|
|
681
|
+
image: buffer,
|
|
682
|
+
caption,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
return result?.key?.id || `wa-${Date.now()}`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Add a reaction to a message
|
|
690
|
+
*/
|
|
691
|
+
async addReaction(chatId: string, messageId: string, emoji: string): Promise<void> {
|
|
692
|
+
if (!this.sock) throw new Error('WhatsApp is not connected');
|
|
693
|
+
|
|
694
|
+
const jid = this.toWhatsAppJid(chatId);
|
|
695
|
+
await this.sock.sendMessage(jid, {
|
|
696
|
+
react: {
|
|
697
|
+
text: emoji,
|
|
698
|
+
key: {
|
|
699
|
+
remoteJid: jid,
|
|
700
|
+
id: messageId,
|
|
701
|
+
fromMe: false,
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Remove a reaction from a message
|
|
709
|
+
*/
|
|
710
|
+
async removeReaction(chatId: string, messageId: string): Promise<void> {
|
|
711
|
+
await this.addReaction(chatId, messageId, ''); // Empty string removes reaction
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Register a message handler
|
|
716
|
+
*/
|
|
717
|
+
onMessage(handler: MessageHandler): void {
|
|
718
|
+
this.messageHandlers.push(handler);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Register a callback query handler (not supported by WhatsApp)
|
|
723
|
+
*/
|
|
724
|
+
onCallbackQuery(_handler: CallbackQueryHandler): void {
|
|
725
|
+
// WhatsApp doesn't support inline keyboards/callback queries
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Register an error handler
|
|
730
|
+
*/
|
|
731
|
+
onError(handler: ErrorHandler): void {
|
|
732
|
+
this.errorHandlers.push(handler);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Register a status change handler
|
|
737
|
+
*/
|
|
738
|
+
onStatusChange(handler: StatusHandler): void {
|
|
739
|
+
this.statusHandlers.push(handler);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Register a QR code handler
|
|
744
|
+
*/
|
|
745
|
+
onQrCode(handler: QrCodeHandler): void {
|
|
746
|
+
this.qrCodeHandlers.push(handler);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Get channel info
|
|
751
|
+
*/
|
|
752
|
+
async getInfo(): Promise<ChannelInfo> {
|
|
753
|
+
return {
|
|
754
|
+
type: 'whatsapp',
|
|
755
|
+
status: this._status,
|
|
756
|
+
botId: this._selfJid,
|
|
757
|
+
botUsername: this._selfE164,
|
|
758
|
+
botDisplayName: this._selfE164,
|
|
759
|
+
extra: {
|
|
760
|
+
qrCode: this.currentQr,
|
|
761
|
+
hasCredentials: await this.hasCredentials(),
|
|
762
|
+
},
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Logout and clear credentials
|
|
768
|
+
*/
|
|
769
|
+
async logout(): Promise<void> {
|
|
770
|
+
await this.disconnect();
|
|
771
|
+
await this.clearCredentials();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Clear stored credentials
|
|
776
|
+
*/
|
|
777
|
+
private async clearCredentials(): Promise<void> {
|
|
778
|
+
try {
|
|
779
|
+
if (fs.existsSync(this.authDir)) {
|
|
780
|
+
const files = fs.readdirSync(this.authDir);
|
|
781
|
+
for (const file of files) {
|
|
782
|
+
fs.unlinkSync(path.join(this.authDir, file));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
} catch (error) {
|
|
786
|
+
console.error('Error clearing WhatsApp credentials:', error);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ============================================================================
|
|
791
|
+
// Private Helper Methods
|
|
792
|
+
// ============================================================================
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Handle incoming message notification
|
|
796
|
+
*/
|
|
797
|
+
private async handleIncomingMessage(message: IncomingMessage): Promise<void> {
|
|
798
|
+
for (const handler of this.messageHandlers) {
|
|
799
|
+
try {
|
|
800
|
+
await handler(message);
|
|
801
|
+
} catch (error) {
|
|
802
|
+
console.error('Error in WhatsApp message handler:', error);
|
|
803
|
+
this.handleError(
|
|
804
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
805
|
+
'messageHandler'
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Handle errors
|
|
813
|
+
*/
|
|
814
|
+
private handleError(error: Error, context?: string): void {
|
|
815
|
+
for (const handler of this.errorHandlers) {
|
|
816
|
+
try {
|
|
817
|
+
handler(error, context);
|
|
818
|
+
} catch (e) {
|
|
819
|
+
console.error('Error in error handler:', e);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Set status and notify handlers
|
|
826
|
+
*/
|
|
827
|
+
private setStatus(status: ChannelStatus, error?: Error): void {
|
|
828
|
+
this._status = status;
|
|
829
|
+
for (const handler of this.statusHandlers) {
|
|
830
|
+
try {
|
|
831
|
+
handler(status, error);
|
|
832
|
+
} catch (e) {
|
|
833
|
+
console.error('Error in status handler:', e);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Attempt reconnection with exponential backoff
|
|
840
|
+
*/
|
|
841
|
+
private async attemptReconnection(): Promise<void> {
|
|
842
|
+
if (this.isReconnecting) return;
|
|
843
|
+
|
|
844
|
+
const config = this.DEFAULT_BACKOFF;
|
|
845
|
+
|
|
846
|
+
if (this.backoffAttempt >= config.maxAttempts) {
|
|
847
|
+
console.error(`WhatsApp: Max reconnection attempts (${config.maxAttempts}) reached`);
|
|
848
|
+
this.setStatus('error', new Error('Max reconnection attempts reached'));
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
this.isReconnecting = true;
|
|
853
|
+
this.backoffAttempt++;
|
|
854
|
+
|
|
855
|
+
const delay = this.calculateBackoffDelay(config);
|
|
856
|
+
console.log(`WhatsApp: Reconnection attempt ${this.backoffAttempt}/${config.maxAttempts} in ${delay}ms`);
|
|
857
|
+
|
|
858
|
+
this.backoffTimer = setTimeout(async () => {
|
|
859
|
+
try {
|
|
860
|
+
this.sock = null;
|
|
861
|
+
this.isReconnecting = false;
|
|
862
|
+
this.setStatus('disconnected');
|
|
863
|
+
await this.connect();
|
|
864
|
+
} catch (error) {
|
|
865
|
+
this.isReconnecting = false;
|
|
866
|
+
console.error('WhatsApp reconnection attempt failed:', error);
|
|
867
|
+
await this.attemptReconnection();
|
|
868
|
+
}
|
|
869
|
+
}, delay);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Calculate backoff delay with jitter
|
|
874
|
+
*/
|
|
875
|
+
private calculateBackoffDelay(config: BackoffConfig): number {
|
|
876
|
+
let delay = config.initialDelay * Math.pow(config.multiplier, this.backoffAttempt - 1);
|
|
877
|
+
delay = Math.min(delay, config.maxDelay);
|
|
878
|
+
|
|
879
|
+
const jitterAmount = delay * config.jitter;
|
|
880
|
+
const jitter = (Math.random() * 2 - 1) * jitterAmount;
|
|
881
|
+
delay = Math.round(delay + jitter);
|
|
882
|
+
|
|
883
|
+
return Math.max(1000, delay);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Reset backoff state
|
|
888
|
+
*/
|
|
889
|
+
private resetBackoff(): void {
|
|
890
|
+
this.backoffAttempt = 0;
|
|
891
|
+
this.isReconnecting = false;
|
|
892
|
+
if (this.backoffTimer) {
|
|
893
|
+
clearTimeout(this.backoffTimer);
|
|
894
|
+
this.backoffTimer = undefined;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Start deduplication cache cleanup
|
|
900
|
+
*/
|
|
901
|
+
private startDedupCleanup(): void {
|
|
902
|
+
this.dedupCleanupTimer = setInterval(() => {
|
|
903
|
+
this.cleanupDedupCache();
|
|
904
|
+
}, this.DEDUP_CACHE_TTL);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Clean up old dedup cache entries
|
|
909
|
+
*/
|
|
910
|
+
private cleanupDedupCache(): void {
|
|
911
|
+
const now = Date.now();
|
|
912
|
+
for (const [key, timestamp] of this.processedMessages) {
|
|
913
|
+
if (now - timestamp > this.DEDUP_CACHE_TTL) {
|
|
914
|
+
this.processedMessages.delete(key);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Get group metadata with caching
|
|
921
|
+
*/
|
|
922
|
+
private async getGroupMeta(jid: string): Promise<{ subject?: string }> {
|
|
923
|
+
const cached = this.groupMetaCache.get(jid);
|
|
924
|
+
if (cached && cached.expires > Date.now()) {
|
|
925
|
+
return cached;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
try {
|
|
929
|
+
const meta = await this.sock?.groupMetadata(jid);
|
|
930
|
+
const entry = {
|
|
931
|
+
subject: meta?.subject,
|
|
932
|
+
expires: Date.now() + this.GROUP_META_TTL_MS,
|
|
933
|
+
};
|
|
934
|
+
this.groupMetaCache.set(jid, entry);
|
|
935
|
+
return entry;
|
|
936
|
+
} catch {
|
|
937
|
+
return { subject: undefined };
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Convert JID to E.164 phone number format
|
|
943
|
+
*/
|
|
944
|
+
private jidToE164(jid: string | null | undefined): string | null {
|
|
945
|
+
if (!jid) return null;
|
|
946
|
+
|
|
947
|
+
// Remove @s.whatsapp.net or @c.us suffix
|
|
948
|
+
const match = jid.match(/^(\d+)@/);
|
|
949
|
+
if (!match) return null;
|
|
950
|
+
|
|
951
|
+
return match[1];
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Convert phone number/chat ID to WhatsApp JID
|
|
956
|
+
*/
|
|
957
|
+
private toWhatsAppJid(chatId: string): string {
|
|
958
|
+
// Already a JID
|
|
959
|
+
if (chatId.includes('@')) return chatId;
|
|
960
|
+
|
|
961
|
+
// Group ID
|
|
962
|
+
if (chatId.includes('-')) {
|
|
963
|
+
return `${chatId}@g.us`;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Phone number - remove any non-numeric characters
|
|
967
|
+
const cleaned = chatId.replace(/[^0-9]/g, '');
|
|
968
|
+
return `${cleaned}@s.whatsapp.net`;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Extract text from WhatsApp message
|
|
973
|
+
*/
|
|
974
|
+
private extractText(message: proto.IMessage | null | undefined): string | undefined {
|
|
975
|
+
if (!message) return undefined;
|
|
976
|
+
|
|
977
|
+
// Direct text message
|
|
978
|
+
if (message.conversation) {
|
|
979
|
+
return message.conversation.trim();
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Extended text message
|
|
983
|
+
if (message.extendedTextMessage?.text) {
|
|
984
|
+
return message.extendedTextMessage.text.trim();
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Image/video/document caption
|
|
988
|
+
const caption =
|
|
989
|
+
message.imageMessage?.caption ||
|
|
990
|
+
message.videoMessage?.caption ||
|
|
991
|
+
message.documentMessage?.caption;
|
|
992
|
+
|
|
993
|
+
if (caption) return caption.trim();
|
|
994
|
+
|
|
995
|
+
return undefined;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Extract media placeholder from message
|
|
1000
|
+
*/
|
|
1001
|
+
private extractMediaPlaceholder(message: proto.IMessage | null | undefined): string | undefined {
|
|
1002
|
+
if (!message) return undefined;
|
|
1003
|
+
|
|
1004
|
+
if (message.imageMessage) return '<media:image>';
|
|
1005
|
+
if (message.videoMessage) return '<media:video>';
|
|
1006
|
+
if (message.audioMessage) return '<media:audio>';
|
|
1007
|
+
if (message.documentMessage) return '<media:document>';
|
|
1008
|
+
if (message.stickerMessage) return '<media:sticker>';
|
|
1009
|
+
if (message.contactMessage) return '<contact>';
|
|
1010
|
+
if (message.locationMessage) return '<location>';
|
|
1011
|
+
|
|
1012
|
+
return undefined;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Download audio from a WhatsApp message and return as attachment
|
|
1017
|
+
*/
|
|
1018
|
+
private async downloadAudioAttachment(message: proto.IMessage): Promise<MessageAttachment | null> {
|
|
1019
|
+
const audioMessage = message.audioMessage;
|
|
1020
|
+
if (!audioMessage) return null;
|
|
1021
|
+
|
|
1022
|
+
try {
|
|
1023
|
+
console.log('[WhatsApp] Downloading audio message...');
|
|
1024
|
+
|
|
1025
|
+
// Download the audio content
|
|
1026
|
+
const stream = await downloadContentFromMessage(
|
|
1027
|
+
audioMessage as DownloadableMessage,
|
|
1028
|
+
'audio'
|
|
1029
|
+
);
|
|
1030
|
+
|
|
1031
|
+
// Collect the stream into a buffer
|
|
1032
|
+
const chunks: Buffer[] = [];
|
|
1033
|
+
for await (const chunk of stream) {
|
|
1034
|
+
chunks.push(chunk as Buffer);
|
|
1035
|
+
}
|
|
1036
|
+
const audioBuffer = Buffer.concat(chunks);
|
|
1037
|
+
|
|
1038
|
+
console.log(`[WhatsApp] Downloaded audio: ${audioBuffer.length} bytes`);
|
|
1039
|
+
|
|
1040
|
+
// Determine mime type (usually audio/ogg for voice messages)
|
|
1041
|
+
const mimeType = audioMessage.mimetype || 'audio/ogg; codecs=opus';
|
|
1042
|
+
const isVoiceNote = audioMessage.ptt === true;
|
|
1043
|
+
const fileName = isVoiceNote
|
|
1044
|
+
? `voice_message_${Date.now()}.ogg`
|
|
1045
|
+
: `audio_${Date.now()}.${this.getAudioExtension(mimeType)}`;
|
|
1046
|
+
|
|
1047
|
+
return {
|
|
1048
|
+
type: 'audio',
|
|
1049
|
+
data: audioBuffer,
|
|
1050
|
+
mimeType,
|
|
1051
|
+
fileName,
|
|
1052
|
+
size: audioBuffer.length,
|
|
1053
|
+
};
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
console.error('[WhatsApp] Failed to download audio:', error);
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Get file extension from mime type
|
|
1062
|
+
*/
|
|
1063
|
+
private getAudioExtension(mimeType: string): string {
|
|
1064
|
+
if (mimeType.includes('ogg')) return 'ogg';
|
|
1065
|
+
if (mimeType.includes('mp3') || mimeType.includes('mpeg')) return 'mp3';
|
|
1066
|
+
if (mimeType.includes('wav')) return 'wav';
|
|
1067
|
+
if (mimeType.includes('m4a') || mimeType.includes('mp4')) return 'm4a';
|
|
1068
|
+
if (mimeType.includes('webm')) return 'webm';
|
|
1069
|
+
return 'audio';
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Get status code from disconnect error
|
|
1074
|
+
*/
|
|
1075
|
+
private getStatusCode(err: unknown): number | undefined {
|
|
1076
|
+
if (!err) return undefined;
|
|
1077
|
+
|
|
1078
|
+
const asAny = err as any;
|
|
1079
|
+
return (
|
|
1080
|
+
asAny?.output?.statusCode ||
|
|
1081
|
+
asAny?.status ||
|
|
1082
|
+
undefined
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Ensure directory exists
|
|
1088
|
+
*/
|
|
1089
|
+
private async ensureDir(dirPath: string): Promise<void> {
|
|
1090
|
+
if (!fs.existsSync(dirPath)) {
|
|
1091
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Create a WhatsApp adapter from configuration
|
|
1098
|
+
*/
|
|
1099
|
+
export function createWhatsAppAdapter(config: WhatsAppConfig): WhatsAppAdapter {
|
|
1100
|
+
return new WhatsAppAdapter(config);
|
|
1101
|
+
}
|