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,1390 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Telegram Channel Adapter
|
|
4
|
+
*
|
|
5
|
+
* Implements the ChannelAdapter interface using grammY for Telegram Bot API.
|
|
6
|
+
* Supports both polling and webhook modes.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - API throttling to prevent rate limits
|
|
10
|
+
* - Message deduplication to prevent double processing
|
|
11
|
+
* - Text fragment assembly for split long messages
|
|
12
|
+
* - ACK reactions while processing
|
|
13
|
+
* - Draft streaming for real-time response preview
|
|
14
|
+
* - Sequential message processing to prevent race conditions
|
|
15
|
+
* - Connection conflict detection (409 errors)
|
|
16
|
+
* - Exponential backoff with jitter for error recovery
|
|
17
|
+
* - Health check endpoint for webhook mode
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.TelegramAdapter = void 0;
|
|
54
|
+
exports.createTelegramAdapter = createTelegramAdapter;
|
|
55
|
+
const grammy_1 = require("grammy");
|
|
56
|
+
const runner_1 = require("@grammyjs/runner");
|
|
57
|
+
const transformer_throttler_1 = require("@grammyjs/transformer-throttler");
|
|
58
|
+
const fs = __importStar(require("fs"));
|
|
59
|
+
const path = __importStar(require("path"));
|
|
60
|
+
const http = __importStar(require("http"));
|
|
61
|
+
class TelegramAdapter {
|
|
62
|
+
constructor(config) {
|
|
63
|
+
this.type = 'telegram';
|
|
64
|
+
this.bot = null;
|
|
65
|
+
this._status = 'disconnected';
|
|
66
|
+
this.messageHandlers = [];
|
|
67
|
+
this.errorHandlers = [];
|
|
68
|
+
this.statusHandlers = [];
|
|
69
|
+
this.callbackQueryHandlers = [];
|
|
70
|
+
// Message deduplication: track processed update IDs
|
|
71
|
+
this.processedUpdates = new Map(); // updateId -> timestamp
|
|
72
|
+
this.DEDUP_CACHE_TTL = 60000; // 1 minute
|
|
73
|
+
this.DEDUP_CACHE_MAX_SIZE = 1000;
|
|
74
|
+
// Text fragment assembly: buffer split messages
|
|
75
|
+
this.pendingFragments = new Map(); // chatId:userId -> fragment
|
|
76
|
+
this.DEFAULT_FRAGMENT_TIMEOUT = 1500; // 1.5 seconds
|
|
77
|
+
// Draft streaming state
|
|
78
|
+
this.draftStates = new Map(); // chatId -> draft state
|
|
79
|
+
this.DRAFT_UPDATE_INTERVAL = 500; // Update draft every 500ms
|
|
80
|
+
// Exponential backoff state
|
|
81
|
+
this.backoffAttempt = 0;
|
|
82
|
+
this.isReconnecting = false;
|
|
83
|
+
// Default backoff configuration
|
|
84
|
+
this.DEFAULT_BACKOFF = {
|
|
85
|
+
initialDelay: 2000,
|
|
86
|
+
maxDelay: 30000,
|
|
87
|
+
multiplier: 1.8,
|
|
88
|
+
jitter: 0.25,
|
|
89
|
+
maxAttempts: 10,
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Get sequential key for message ordering
|
|
93
|
+
* Messages from the same chat are processed sequentially
|
|
94
|
+
*/
|
|
95
|
+
this.getSequentialKey = (ctx) => {
|
|
96
|
+
const chatId = ctx.chat?.id;
|
|
97
|
+
if (!chatId)
|
|
98
|
+
return undefined;
|
|
99
|
+
// Use chat ID + thread ID for forum topics
|
|
100
|
+
const threadId = ctx.message?.message_thread_id;
|
|
101
|
+
if (threadId) {
|
|
102
|
+
return `${chatId}:${threadId}`;
|
|
103
|
+
}
|
|
104
|
+
return String(chatId);
|
|
105
|
+
};
|
|
106
|
+
this.config = {
|
|
107
|
+
deduplicationEnabled: true,
|
|
108
|
+
ackReactionEnabled: true,
|
|
109
|
+
draftStreamingEnabled: true,
|
|
110
|
+
fragmentAssemblyTimeout: 1500,
|
|
111
|
+
sequentialProcessingEnabled: true,
|
|
112
|
+
...config,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
get status() {
|
|
116
|
+
return this._status;
|
|
117
|
+
}
|
|
118
|
+
get botUsername() {
|
|
119
|
+
return this._botUsername;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Connect to Telegram using long polling
|
|
123
|
+
*/
|
|
124
|
+
async connect() {
|
|
125
|
+
if (this._status === 'connected' || this._status === 'connecting') {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.setStatus('connecting');
|
|
129
|
+
this.resetBackoff();
|
|
130
|
+
try {
|
|
131
|
+
// Create bot instance
|
|
132
|
+
this.bot = new grammy_1.Bot(this.config.botToken);
|
|
133
|
+
// Add API throttling to prevent rate limits
|
|
134
|
+
const throttler = (0, transformer_throttler_1.apiThrottler)();
|
|
135
|
+
this.bot.api.config.use(throttler);
|
|
136
|
+
// Add sequential processing to prevent race conditions
|
|
137
|
+
if (this.config.sequentialProcessingEnabled) {
|
|
138
|
+
this.bot.use((0, runner_1.sequentialize)(this.getSequentialKey));
|
|
139
|
+
}
|
|
140
|
+
// Get bot info
|
|
141
|
+
const me = await this.bot.api.getMe();
|
|
142
|
+
this._botUsername = me.username;
|
|
143
|
+
// Register expanded bot commands for the "/" menu
|
|
144
|
+
await this.registerBotCommands();
|
|
145
|
+
// Set up message handler with deduplication and fragment assembly
|
|
146
|
+
this.bot.on('message:text', async (ctx) => {
|
|
147
|
+
await this.handleTextMessage(ctx);
|
|
148
|
+
});
|
|
149
|
+
// Set up callback query handler for inline keyboards
|
|
150
|
+
this.bot.on('callback_query:data', async (ctx) => {
|
|
151
|
+
await this.handleCallbackQuery(ctx);
|
|
152
|
+
});
|
|
153
|
+
// Handle errors with 409 detection and backoff
|
|
154
|
+
this.bot.catch(async (err) => {
|
|
155
|
+
await this.handleBotError(err);
|
|
156
|
+
});
|
|
157
|
+
// Start deduplication cleanup timer
|
|
158
|
+
if (this.config.deduplicationEnabled) {
|
|
159
|
+
this.startDedupCleanup();
|
|
160
|
+
}
|
|
161
|
+
// Start polling with error handling
|
|
162
|
+
await this.startPolling();
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
166
|
+
// Check for connection conflict (409) during initial connection
|
|
167
|
+
if (this.isConnectionConflictError(error)) {
|
|
168
|
+
console.error('Connection conflict detected: Another bot instance is running');
|
|
169
|
+
this.setStatus('error', new Error('Connection conflict: Another bot instance is running. Stop the other instance first.'));
|
|
170
|
+
throw new Error('Connection conflict: Another bot instance is running');
|
|
171
|
+
}
|
|
172
|
+
this.setStatus('error', err);
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Register bot commands for the "/" menu
|
|
178
|
+
*/
|
|
179
|
+
async registerBotCommands() {
|
|
180
|
+
if (!this.bot)
|
|
181
|
+
return;
|
|
182
|
+
await this.bot.api.setMyCommands([
|
|
183
|
+
// Core commands
|
|
184
|
+
{ command: 'start', description: 'Start the bot and see welcome message' },
|
|
185
|
+
{ command: 'help', description: 'Show all available commands' },
|
|
186
|
+
{ command: 'status', description: 'Check bot connection and system status' },
|
|
187
|
+
// Workspace management
|
|
188
|
+
{ command: 'workspaces', description: 'List all available workspaces' },
|
|
189
|
+
{ command: 'workspace', description: 'Select or show current workspace' },
|
|
190
|
+
{ command: 'addworkspace', description: 'Add a new workspace by path' },
|
|
191
|
+
{ command: 'removeworkspace', description: 'Remove a workspace from the list' },
|
|
192
|
+
// Task management
|
|
193
|
+
{ command: 'newtask', description: 'Start a fresh task/conversation' },
|
|
194
|
+
{ command: 'cancel', description: 'Cancel the current running task' },
|
|
195
|
+
{ command: 'retry', description: 'Retry the last failed task' },
|
|
196
|
+
{ command: 'history', description: 'Show recent task history' },
|
|
197
|
+
// Model configuration
|
|
198
|
+
{ command: 'provider', description: 'Change or show current LLM provider' },
|
|
199
|
+
{ command: 'providers', description: 'List all available LLM providers' },
|
|
200
|
+
{ command: 'model', description: 'Change or show current model' },
|
|
201
|
+
{ command: 'models', description: 'List available AI models' },
|
|
202
|
+
// Skills management
|
|
203
|
+
{ command: 'skills', description: 'List available skills' },
|
|
204
|
+
{ command: 'skill', description: 'Enable or disable a skill' },
|
|
205
|
+
// Settings
|
|
206
|
+
{ command: 'settings', description: 'View and modify bot settings' },
|
|
207
|
+
{ command: 'debug', description: 'Toggle debug mode on/off' },
|
|
208
|
+
{ command: 'version', description: 'Show bot version information' },
|
|
209
|
+
]);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Start polling with error handling and reconnection
|
|
213
|
+
*/
|
|
214
|
+
async startPolling() {
|
|
215
|
+
if (!this.bot)
|
|
216
|
+
return;
|
|
217
|
+
this.bot.start({
|
|
218
|
+
onStart: () => {
|
|
219
|
+
console.log(`Telegram bot @${this._botUsername} started`);
|
|
220
|
+
this.setStatus('connected');
|
|
221
|
+
this.resetBackoff();
|
|
222
|
+
},
|
|
223
|
+
drop_pending_updates: true,
|
|
224
|
+
allowed_updates: ['message', 'message_reaction', 'callback_query'],
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Handle bot errors including 409 conflict detection
|
|
229
|
+
*/
|
|
230
|
+
async handleBotError(err) {
|
|
231
|
+
console.error('Telegram bot error:', err);
|
|
232
|
+
// Check for connection conflict (409)
|
|
233
|
+
if (this.isConnectionConflictError(err)) {
|
|
234
|
+
console.error('Connection conflict detected (409): Another bot instance may be running');
|
|
235
|
+
this.setStatus('error', new Error('Connection conflict: Another bot instance is running'));
|
|
236
|
+
// Don't reconnect on 409 - let the user resolve the conflict
|
|
237
|
+
this.handleError(new Error('Connection conflict: Another bot instance is running. Stop the other instance and restart.'), 'connection_conflict');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// Check for network errors that warrant reconnection
|
|
241
|
+
if (this.isNetworkError(err)) {
|
|
242
|
+
console.log('Network error detected, will attempt reconnection with backoff');
|
|
243
|
+
await this.attemptReconnection();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// Handle other errors normally
|
|
247
|
+
this.handleError(err instanceof Error ? err : new Error(String(err)), 'bot.catch');
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Check if error is a connection conflict (409)
|
|
251
|
+
*/
|
|
252
|
+
isConnectionConflictError(err) {
|
|
253
|
+
if (err instanceof grammy_1.GrammyError) {
|
|
254
|
+
return err.error_code === 409;
|
|
255
|
+
}
|
|
256
|
+
if (err instanceof grammy_1.HttpError) {
|
|
257
|
+
return err.status === 409;
|
|
258
|
+
}
|
|
259
|
+
// Check error message for 409 indicators
|
|
260
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
261
|
+
return message.includes('409') || message.includes('Conflict') || message.includes('terminated by other getUpdates');
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Check if error is a network error that warrants reconnection
|
|
265
|
+
*/
|
|
266
|
+
isNetworkError(err) {
|
|
267
|
+
if (err instanceof grammy_1.HttpError) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
271
|
+
return (message.includes('ECONNRESET') ||
|
|
272
|
+
message.includes('ETIMEDOUT') ||
|
|
273
|
+
message.includes('ENOTFOUND') ||
|
|
274
|
+
message.includes('network') ||
|
|
275
|
+
message.includes('socket') ||
|
|
276
|
+
message.includes('502') ||
|
|
277
|
+
message.includes('503') ||
|
|
278
|
+
message.includes('504'));
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Attempt reconnection with exponential backoff
|
|
282
|
+
*/
|
|
283
|
+
async attemptReconnection() {
|
|
284
|
+
if (this.isReconnecting) {
|
|
285
|
+
console.log('Reconnection already in progress');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const backoffConfig = { ...this.DEFAULT_BACKOFF, ...this.config.backoff };
|
|
289
|
+
if (this.backoffAttempt >= backoffConfig.maxAttempts) {
|
|
290
|
+
console.error(`Max reconnection attempts (${backoffConfig.maxAttempts}) reached`);
|
|
291
|
+
this.setStatus('error', new Error('Max reconnection attempts reached'));
|
|
292
|
+
this.handleError(new Error('Failed to reconnect after maximum attempts'), 'reconnection_failed');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
this.isReconnecting = true;
|
|
296
|
+
this.backoffAttempt++;
|
|
297
|
+
const delay = this.calculateBackoffDelay(backoffConfig);
|
|
298
|
+
console.log(`Reconnection attempt ${this.backoffAttempt}/${backoffConfig.maxAttempts} in ${delay}ms`);
|
|
299
|
+
this.backoffTimer = setTimeout(async () => {
|
|
300
|
+
try {
|
|
301
|
+
// Stop existing bot if any
|
|
302
|
+
if (this.bot) {
|
|
303
|
+
await this.bot.stop();
|
|
304
|
+
this.bot = null;
|
|
305
|
+
}
|
|
306
|
+
this.isReconnecting = false;
|
|
307
|
+
this.setStatus('disconnected');
|
|
308
|
+
// Attempt to reconnect
|
|
309
|
+
await this.connect();
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
this.isReconnecting = false;
|
|
313
|
+
console.error('Reconnection attempt failed:', error);
|
|
314
|
+
// Schedule next attempt if not a 409 conflict
|
|
315
|
+
if (!this.isConnectionConflictError(error)) {
|
|
316
|
+
await this.attemptReconnection();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}, delay);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Calculate backoff delay with jitter
|
|
323
|
+
*/
|
|
324
|
+
calculateBackoffDelay(config) {
|
|
325
|
+
// Calculate base delay: initialDelay * multiplier^attempt
|
|
326
|
+
let delay = config.initialDelay * Math.pow(config.multiplier, this.backoffAttempt - 1);
|
|
327
|
+
// Cap at max delay
|
|
328
|
+
delay = Math.min(delay, config.maxDelay);
|
|
329
|
+
// Add jitter: delay ± (delay * jitter * random)
|
|
330
|
+
const jitterAmount = delay * config.jitter;
|
|
331
|
+
const jitter = (Math.random() * 2 - 1) * jitterAmount; // Random between -jitterAmount and +jitterAmount
|
|
332
|
+
delay = Math.round(delay + jitter);
|
|
333
|
+
// Ensure minimum delay of 1 second
|
|
334
|
+
return Math.max(1000, delay);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Reset backoff state
|
|
338
|
+
*/
|
|
339
|
+
resetBackoff() {
|
|
340
|
+
this.backoffAttempt = 0;
|
|
341
|
+
this.isReconnecting = false;
|
|
342
|
+
if (this.backoffTimer) {
|
|
343
|
+
clearTimeout(this.backoffTimer);
|
|
344
|
+
this.backoffTimer = undefined;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Handle incoming text message with deduplication and fragment assembly
|
|
349
|
+
*/
|
|
350
|
+
async handleTextMessage(ctx) {
|
|
351
|
+
const msg = ctx.message;
|
|
352
|
+
const updateId = ctx.update.update_id;
|
|
353
|
+
// Feature 4: Message deduplication - check if already processed
|
|
354
|
+
if (this.config.deduplicationEnabled && this.isUpdateProcessed(updateId)) {
|
|
355
|
+
console.log(`Skipping duplicate update ${updateId}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
// Mark update as processed
|
|
359
|
+
if (this.config.deduplicationEnabled) {
|
|
360
|
+
this.markUpdateProcessed(updateId);
|
|
361
|
+
}
|
|
362
|
+
// Feature 3: Text fragment assembly - buffer split messages
|
|
363
|
+
const fragmentKey = `${msg.chat.id}:${msg.from.id}`;
|
|
364
|
+
const existingFragment = this.pendingFragments.get(fragmentKey);
|
|
365
|
+
if (existingFragment) {
|
|
366
|
+
// Add to existing fragment
|
|
367
|
+
clearTimeout(existingFragment.timer);
|
|
368
|
+
existingFragment.messages.push({
|
|
369
|
+
messageId: msg.message_id.toString(),
|
|
370
|
+
text: msg.text || '',
|
|
371
|
+
timestamp: new Date(msg.date * 1000),
|
|
372
|
+
ctx,
|
|
373
|
+
});
|
|
374
|
+
// Reset timer
|
|
375
|
+
existingFragment.timer = setTimeout(() => {
|
|
376
|
+
this.processFragments(fragmentKey);
|
|
377
|
+
}, this.config.fragmentAssemblyTimeout || this.DEFAULT_FRAGMENT_TIMEOUT);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// Check if this might be a split message (long text arriving in chunks)
|
|
381
|
+
// Telegram splits messages at ~4096 chars, so check if message ends mid-sentence
|
|
382
|
+
const mightBeSplit = this.mightBeSplitMessage(msg.text || '');
|
|
383
|
+
if (mightBeSplit) {
|
|
384
|
+
// Start new fragment buffer
|
|
385
|
+
const timer = setTimeout(() => {
|
|
386
|
+
this.processFragments(fragmentKey);
|
|
387
|
+
}, this.config.fragmentAssemblyTimeout || this.DEFAULT_FRAGMENT_TIMEOUT);
|
|
388
|
+
this.pendingFragments.set(fragmentKey, {
|
|
389
|
+
chatId: msg.chat.id.toString(),
|
|
390
|
+
userId: msg.from.id.toString(),
|
|
391
|
+
messages: [{
|
|
392
|
+
messageId: msg.message_id.toString(),
|
|
393
|
+
text: msg.text || '',
|
|
394
|
+
timestamp: new Date(msg.date * 1000),
|
|
395
|
+
ctx,
|
|
396
|
+
}],
|
|
397
|
+
timer,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
// Process immediately (single message)
|
|
402
|
+
await this.processMessage(ctx);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Check if a message might be part of a split message
|
|
408
|
+
*/
|
|
409
|
+
mightBeSplitMessage(text) {
|
|
410
|
+
// Messages near Telegram's limit or ending abruptly might be split
|
|
411
|
+
if (text.length >= 4000)
|
|
412
|
+
return true;
|
|
413
|
+
// Check if text ends mid-sentence (no terminal punctuation)
|
|
414
|
+
const trimmed = text.trim();
|
|
415
|
+
if (trimmed.length > 100) {
|
|
416
|
+
const lastChar = trimmed.charAt(trimmed.length - 1);
|
|
417
|
+
const terminalPunctuation = ['.', '!', '?', ')', ']', '}', '"', "'", '`'];
|
|
418
|
+
if (!terminalPunctuation.includes(lastChar)) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Process assembled fragments
|
|
426
|
+
*/
|
|
427
|
+
async processFragments(fragmentKey) {
|
|
428
|
+
const fragment = this.pendingFragments.get(fragmentKey);
|
|
429
|
+
if (!fragment)
|
|
430
|
+
return;
|
|
431
|
+
this.pendingFragments.delete(fragmentKey);
|
|
432
|
+
if (fragment.messages.length === 1) {
|
|
433
|
+
// Single message, process normally
|
|
434
|
+
await this.processMessage(fragment.messages[0].ctx);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
// Multiple messages, combine them
|
|
438
|
+
const combinedText = fragment.messages
|
|
439
|
+
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())
|
|
440
|
+
.map(m => m.text)
|
|
441
|
+
.join('');
|
|
442
|
+
// Use the first message's context but with combined text
|
|
443
|
+
const firstCtx = fragment.messages[0].ctx;
|
|
444
|
+
const message = this.mapContextToMessage(firstCtx, combinedText);
|
|
445
|
+
console.log(`Assembled ${fragment.messages.length} text fragments into single message (${combinedText.length} chars)`);
|
|
446
|
+
await this.handleIncomingMessage(message);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Process a single message (with ACK reaction)
|
|
451
|
+
*/
|
|
452
|
+
async processMessage(ctx) {
|
|
453
|
+
const message = this.mapContextToMessage(ctx);
|
|
454
|
+
// Feature 2: Send ACK reaction (👀) while processing
|
|
455
|
+
if (this.config.ackReactionEnabled) {
|
|
456
|
+
try {
|
|
457
|
+
await this.sendAckReaction(ctx);
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
// Ignore reaction errors (might not have permission)
|
|
461
|
+
console.debug('Could not send ACK reaction:', err);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
await this.handleIncomingMessage(message);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Send ACK reaction (👀) to indicate message received
|
|
468
|
+
*/
|
|
469
|
+
async sendAckReaction(ctx) {
|
|
470
|
+
if (!this.bot || !ctx.message)
|
|
471
|
+
return;
|
|
472
|
+
try {
|
|
473
|
+
await this.bot.api.setMessageReaction(ctx.message.chat.id, ctx.message.message_id, [{ type: 'emoji', emoji: '👀' }]);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Silently fail - reactions might not be available
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Remove ACK reaction after processing
|
|
481
|
+
*/
|
|
482
|
+
async removeAckReaction(chatId, messageId) {
|
|
483
|
+
if (!this.bot)
|
|
484
|
+
return;
|
|
485
|
+
try {
|
|
486
|
+
await this.bot.api.setMessageReaction(chatId, parseInt(messageId, 10), [] // Empty array removes reactions
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
// Silently fail
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Send a completion reaction when done
|
|
495
|
+
* Note: Telegram only allows specific reaction emojis, using 👍 for completion
|
|
496
|
+
*/
|
|
497
|
+
async sendCompletionReaction(chatId, messageId) {
|
|
498
|
+
if (!this.bot)
|
|
499
|
+
return;
|
|
500
|
+
try {
|
|
501
|
+
await this.bot.api.setMessageReaction(chatId, parseInt(messageId, 10), [{ type: 'emoji', emoji: '👍' }]);
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// Silently fail
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Check if update was already processed (deduplication)
|
|
509
|
+
*/
|
|
510
|
+
isUpdateProcessed(updateId) {
|
|
511
|
+
return this.processedUpdates.has(updateId);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Mark update as processed
|
|
515
|
+
*/
|
|
516
|
+
markUpdateProcessed(updateId) {
|
|
517
|
+
this.processedUpdates.set(updateId, Date.now());
|
|
518
|
+
// Prevent unbounded growth
|
|
519
|
+
if (this.processedUpdates.size > this.DEDUP_CACHE_MAX_SIZE) {
|
|
520
|
+
this.cleanupDedupCache();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Start periodic cleanup of dedup cache
|
|
525
|
+
*/
|
|
526
|
+
startDedupCleanup() {
|
|
527
|
+
this.dedupCleanupTimer = setInterval(() => {
|
|
528
|
+
this.cleanupDedupCache();
|
|
529
|
+
}, this.DEDUP_CACHE_TTL);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Clean up old entries from dedup cache
|
|
533
|
+
*/
|
|
534
|
+
cleanupDedupCache() {
|
|
535
|
+
const now = Date.now();
|
|
536
|
+
for (const [updateId, timestamp] of this.processedUpdates) {
|
|
537
|
+
if (now - timestamp > this.DEDUP_CACHE_TTL) {
|
|
538
|
+
this.processedUpdates.delete(updateId);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Disconnect from Telegram
|
|
544
|
+
*/
|
|
545
|
+
async disconnect() {
|
|
546
|
+
// Reset backoff state
|
|
547
|
+
this.resetBackoff();
|
|
548
|
+
// Clear timers
|
|
549
|
+
if (this.dedupCleanupTimer) {
|
|
550
|
+
clearInterval(this.dedupCleanupTimer);
|
|
551
|
+
this.dedupCleanupTimer = undefined;
|
|
552
|
+
}
|
|
553
|
+
// Clear pending fragments
|
|
554
|
+
for (const fragment of this.pendingFragments.values()) {
|
|
555
|
+
clearTimeout(fragment.timer);
|
|
556
|
+
}
|
|
557
|
+
this.pendingFragments.clear();
|
|
558
|
+
// Clear draft states
|
|
559
|
+
this.draftStates.clear();
|
|
560
|
+
// Clear dedup cache
|
|
561
|
+
this.processedUpdates.clear();
|
|
562
|
+
// Stop webhook server if running
|
|
563
|
+
if (this.webhookServer) {
|
|
564
|
+
await this.stopWebhookServer();
|
|
565
|
+
}
|
|
566
|
+
if (this.bot) {
|
|
567
|
+
await this.bot.stop();
|
|
568
|
+
this.bot = null;
|
|
569
|
+
}
|
|
570
|
+
this._botUsername = undefined;
|
|
571
|
+
this.setStatus('disconnected');
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Send a message to a Telegram chat
|
|
575
|
+
*/
|
|
576
|
+
async sendMessage(message) {
|
|
577
|
+
if (!this.bot || this._status !== 'connected') {
|
|
578
|
+
throw new Error('Telegram bot is not connected');
|
|
579
|
+
}
|
|
580
|
+
// Handle image attachments first (send images before text)
|
|
581
|
+
let lastMessageId;
|
|
582
|
+
if (message.attachments && message.attachments.length > 0) {
|
|
583
|
+
for (const attachment of message.attachments) {
|
|
584
|
+
if (attachment.type === 'image' && attachment.url) {
|
|
585
|
+
try {
|
|
586
|
+
// attachment.url is the file path for local images
|
|
587
|
+
const msgId = await this.sendPhoto(message.chatId, attachment.url);
|
|
588
|
+
lastMessageId = msgId;
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
console.error('Failed to send image attachment:', err);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// If we have text to send, send it
|
|
597
|
+
if (message.text && message.text.trim()) {
|
|
598
|
+
// Process text for Telegram compatibility
|
|
599
|
+
let processedText = message.text;
|
|
600
|
+
if (message.parseMode === 'markdown') {
|
|
601
|
+
processedText = this.convertMarkdownForTelegram(message.text);
|
|
602
|
+
}
|
|
603
|
+
const options = {};
|
|
604
|
+
// Set parse mode
|
|
605
|
+
// Use legacy Markdown (not MarkdownV2) to avoid escaping issues with special characters
|
|
606
|
+
if (message.parseMode === 'markdown') {
|
|
607
|
+
options.parse_mode = 'Markdown';
|
|
608
|
+
}
|
|
609
|
+
else if (message.parseMode === 'html') {
|
|
610
|
+
options.parse_mode = 'HTML';
|
|
611
|
+
}
|
|
612
|
+
// Reply to message if specified
|
|
613
|
+
if (message.replyTo) {
|
|
614
|
+
options.reply_to_message_id = parseInt(message.replyTo, 10);
|
|
615
|
+
}
|
|
616
|
+
// Forum topic thread support
|
|
617
|
+
if (message.threadId) {
|
|
618
|
+
options.message_thread_id = parseInt(message.threadId, 10);
|
|
619
|
+
}
|
|
620
|
+
// Link preview control
|
|
621
|
+
if (message.disableLinkPreview) {
|
|
622
|
+
options.link_preview_options = { is_disabled: true };
|
|
623
|
+
}
|
|
624
|
+
// Inline keyboard support
|
|
625
|
+
if (message.inlineKeyboard && message.inlineKeyboard.length > 0) {
|
|
626
|
+
options.reply_markup = this.buildInlineKeyboard(message.inlineKeyboard);
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const sent = await this.bot.api.sendMessage(message.chatId, processedText, options);
|
|
630
|
+
return sent.message_id.toString();
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
// If markdown parsing fails, retry without parse_mode
|
|
634
|
+
if (error?.error_code === 400 && error?.description?.includes("can't parse entities")) {
|
|
635
|
+
console.log('Markdown parsing failed, retrying without parse_mode');
|
|
636
|
+
const plainOptions = {
|
|
637
|
+
...(message.threadId && { message_thread_id: parseInt(message.threadId, 10) }),
|
|
638
|
+
...(message.disableLinkPreview && { link_preview_options: { is_disabled: true } }),
|
|
639
|
+
...(message.inlineKeyboard && { reply_markup: this.buildInlineKeyboard(message.inlineKeyboard) }),
|
|
640
|
+
};
|
|
641
|
+
if (message.replyTo) {
|
|
642
|
+
plainOptions.reply_to_message_id = parseInt(message.replyTo, 10);
|
|
643
|
+
}
|
|
644
|
+
const sent = await this.bot.api.sendMessage(message.chatId, message.text, plainOptions);
|
|
645
|
+
return sent.message_id.toString();
|
|
646
|
+
}
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// If no text but had attachments, return the last attachment message ID
|
|
651
|
+
return lastMessageId || '';
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Build grammY InlineKeyboard from our button format
|
|
655
|
+
*/
|
|
656
|
+
buildInlineKeyboard(buttons) {
|
|
657
|
+
const keyboard = new grammy_1.InlineKeyboard();
|
|
658
|
+
for (const row of buttons) {
|
|
659
|
+
for (const button of row) {
|
|
660
|
+
if (button.url) {
|
|
661
|
+
keyboard.url(button.text, button.url);
|
|
662
|
+
}
|
|
663
|
+
else if (button.callbackData) {
|
|
664
|
+
keyboard.text(button.text, button.callbackData);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
keyboard.row();
|
|
668
|
+
}
|
|
669
|
+
return keyboard;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Handle incoming callback query from inline keyboard button press
|
|
673
|
+
*/
|
|
674
|
+
async handleCallbackQuery(ctx) {
|
|
675
|
+
const query = ctx.callbackQuery;
|
|
676
|
+
if (!query.data || !query.message) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const callbackQuery = {
|
|
680
|
+
id: query.id,
|
|
681
|
+
userId: query.from.id.toString(),
|
|
682
|
+
userName: query.from.first_name + (query.from.last_name ? ` ${query.from.last_name}` : ''),
|
|
683
|
+
chatId: query.message.chat.id.toString(),
|
|
684
|
+
messageId: query.message.message_id.toString(),
|
|
685
|
+
data: query.data,
|
|
686
|
+
threadId: query.message.message_thread_id?.toString(),
|
|
687
|
+
raw: ctx,
|
|
688
|
+
};
|
|
689
|
+
// Notify all registered handlers
|
|
690
|
+
for (const handler of this.callbackQueryHandlers) {
|
|
691
|
+
try {
|
|
692
|
+
await handler(callbackQuery);
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
console.error('Error in callback query handler:', error);
|
|
696
|
+
this.handleError(error instanceof Error ? error : new Error(String(error)), 'callbackQueryHandler');
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Feature 1: Draft streaming - Start streaming a response
|
|
702
|
+
* Creates or updates a draft message that shows response as it generates
|
|
703
|
+
*/
|
|
704
|
+
async startDraftStream(chatId) {
|
|
705
|
+
if (!this.config.draftStreamingEnabled)
|
|
706
|
+
return;
|
|
707
|
+
this.draftStates.set(chatId, {
|
|
708
|
+
chatId,
|
|
709
|
+
currentText: '',
|
|
710
|
+
lastUpdateTime: Date.now(),
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Update draft stream with new content
|
|
715
|
+
*/
|
|
716
|
+
async updateDraftStream(chatId, text) {
|
|
717
|
+
if (!this.bot || !this.config.draftStreamingEnabled)
|
|
718
|
+
return;
|
|
719
|
+
const state = this.draftStates.get(chatId);
|
|
720
|
+
if (!state)
|
|
721
|
+
return;
|
|
722
|
+
const now = Date.now();
|
|
723
|
+
// Throttle updates to prevent API spam
|
|
724
|
+
if (now - state.lastUpdateTime < this.DRAFT_UPDATE_INTERVAL) {
|
|
725
|
+
// Just update the text, don't send yet
|
|
726
|
+
state.currentText = text;
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
// Add typing indicator suffix
|
|
730
|
+
const displayText = text + ' ▌';
|
|
731
|
+
try {
|
|
732
|
+
if (state.messageId) {
|
|
733
|
+
// Edit existing message
|
|
734
|
+
await this.bot.api.editMessageText(chatId, parseInt(state.messageId, 10), displayText);
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
// Create new message
|
|
738
|
+
const sent = await this.bot.api.sendMessage(chatId, displayText);
|
|
739
|
+
state.messageId = sent.message_id.toString();
|
|
740
|
+
}
|
|
741
|
+
state.currentText = text;
|
|
742
|
+
state.lastUpdateTime = now;
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
// Ignore "message not modified" errors
|
|
746
|
+
if (!error?.description?.includes('message is not modified')) {
|
|
747
|
+
console.error('Draft stream update error:', error);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Finalize draft stream with final content
|
|
753
|
+
*/
|
|
754
|
+
async finalizeDraftStream(chatId, finalText) {
|
|
755
|
+
if (!this.bot)
|
|
756
|
+
throw new Error('Bot not connected');
|
|
757
|
+
const state = this.draftStates.get(chatId);
|
|
758
|
+
this.draftStates.delete(chatId);
|
|
759
|
+
if (!this.config.draftStreamingEnabled || !state?.messageId) {
|
|
760
|
+
// No draft exists, send as new message
|
|
761
|
+
const sent = await this.bot.api.sendMessage(chatId, finalText);
|
|
762
|
+
return sent.message_id.toString();
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
// Edit the draft message to final content (remove typing indicator)
|
|
766
|
+
await this.bot.api.editMessageText(chatId, parseInt(state.messageId, 10), finalText);
|
|
767
|
+
return state.messageId;
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
// If edit fails, send as new message
|
|
771
|
+
console.error('Failed to finalize draft, sending new message:', error);
|
|
772
|
+
const sent = await this.bot.api.sendMessage(chatId, finalText);
|
|
773
|
+
return sent.message_id.toString();
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Cancel draft stream (delete the draft message)
|
|
778
|
+
*/
|
|
779
|
+
async cancelDraftStream(chatId) {
|
|
780
|
+
const state = this.draftStates.get(chatId);
|
|
781
|
+
this.draftStates.delete(chatId);
|
|
782
|
+
if (state?.messageId && this.bot) {
|
|
783
|
+
try {
|
|
784
|
+
await this.bot.api.deleteMessage(chatId, parseInt(state.messageId, 10));
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
// Ignore deletion errors
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Convert GitHub-flavored markdown to Telegram-compatible format
|
|
793
|
+
* Telegram legacy Markdown only supports: *bold*, _italic_, `code`, ```code blocks```, [links](url)
|
|
794
|
+
*/
|
|
795
|
+
convertMarkdownForTelegram(text) {
|
|
796
|
+
let result = text;
|
|
797
|
+
// Convert markdown headers (## Header) to bold (*Header*)
|
|
798
|
+
// Must be done before ** conversion
|
|
799
|
+
result = result.replace(/^#{1,6}\s+(.+)$/gm, '*$1*');
|
|
800
|
+
// Convert markdown tables to code blocks
|
|
801
|
+
// Tables start with | and have a separator line like |---|---|
|
|
802
|
+
const tableRegex = /(\|[^\n]+\|\n)+/g;
|
|
803
|
+
const hasSeparatorLine = /\|[\s-:]+\|/;
|
|
804
|
+
result = result.replace(tableRegex, (match) => {
|
|
805
|
+
// Check if this looks like a table (has separator line with dashes)
|
|
806
|
+
if (hasSeparatorLine.test(match)) {
|
|
807
|
+
// Convert table to code block for monospace display
|
|
808
|
+
// Remove the separator line (|---|---|) as it's just formatting
|
|
809
|
+
const lines = match.split('\n').filter(line => line.trim());
|
|
810
|
+
const cleanedLines = lines.filter(line => !(/^\|[\s-:]+\|$/.test(line.trim())));
|
|
811
|
+
// Format table nicely
|
|
812
|
+
const formattedTable = cleanedLines.map(line => {
|
|
813
|
+
// Remove leading/trailing pipes and clean up
|
|
814
|
+
return line.replace(/^\||\|$/g, '').trim();
|
|
815
|
+
}).join('\n');
|
|
816
|
+
return '```\n' + formattedTable + '\n```\n';
|
|
817
|
+
}
|
|
818
|
+
return match;
|
|
819
|
+
});
|
|
820
|
+
// Convert **bold** to *bold* (Telegram uses single asterisk)
|
|
821
|
+
result = result.replace(/\*\*([^*]+)\*\*/g, '*$1*');
|
|
822
|
+
// Convert __bold__ to *bold* (alternative bold syntax)
|
|
823
|
+
result = result.replace(/__([^_]+)__/g, '*$1*');
|
|
824
|
+
// Convert horizontal rules (---, ***) to a line
|
|
825
|
+
result = result.replace(/^[-*]{3,}$/gm, '─────────────────');
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Edit an existing message
|
|
830
|
+
*/
|
|
831
|
+
async editMessage(chatId, messageId, text) {
|
|
832
|
+
if (!this.bot || this._status !== 'connected') {
|
|
833
|
+
throw new Error('Telegram bot is not connected');
|
|
834
|
+
}
|
|
835
|
+
const msgId = parseInt(messageId, 10);
|
|
836
|
+
if (isNaN(msgId)) {
|
|
837
|
+
throw new Error(`Invalid message ID: ${messageId}`);
|
|
838
|
+
}
|
|
839
|
+
await this.bot.api.editMessageText(chatId, msgId, text);
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Delete a message
|
|
843
|
+
*/
|
|
844
|
+
async deleteMessage(chatId, messageId) {
|
|
845
|
+
if (!this.bot || this._status !== 'connected') {
|
|
846
|
+
throw new Error('Telegram bot is not connected');
|
|
847
|
+
}
|
|
848
|
+
const msgId = parseInt(messageId, 10);
|
|
849
|
+
if (isNaN(msgId)) {
|
|
850
|
+
throw new Error(`Invalid message ID: ${messageId}`);
|
|
851
|
+
}
|
|
852
|
+
await this.bot.api.deleteMessage(chatId, msgId);
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Send a document/file to a chat
|
|
856
|
+
*/
|
|
857
|
+
async sendDocument(chatId, filePath, caption) {
|
|
858
|
+
if (!this.bot || this._status !== 'connected') {
|
|
859
|
+
throw new Error('Telegram bot is not connected');
|
|
860
|
+
}
|
|
861
|
+
// Check if file exists
|
|
862
|
+
if (!fs.existsSync(filePath)) {
|
|
863
|
+
throw new Error(`File not found: ${filePath}`);
|
|
864
|
+
}
|
|
865
|
+
const fileName = path.basename(filePath);
|
|
866
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
867
|
+
const sent = await this.bot.api.sendDocument(chatId, new grammy_1.InputFile(fileBuffer, fileName), { caption });
|
|
868
|
+
return sent.message_id.toString();
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Send a photo/image to a chat
|
|
872
|
+
*/
|
|
873
|
+
async sendPhoto(chatId, filePath, caption) {
|
|
874
|
+
if (!this.bot || this._status !== 'connected') {
|
|
875
|
+
throw new Error('Telegram bot is not connected');
|
|
876
|
+
}
|
|
877
|
+
// Check if file exists
|
|
878
|
+
if (!fs.existsSync(filePath)) {
|
|
879
|
+
throw new Error(`File not found: ${filePath}`);
|
|
880
|
+
}
|
|
881
|
+
const fileName = path.basename(filePath);
|
|
882
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
883
|
+
const sent = await this.bot.api.sendPhoto(chatId, new grammy_1.InputFile(fileBuffer, fileName), { caption });
|
|
884
|
+
return sent.message_id.toString();
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Register a message handler
|
|
888
|
+
*/
|
|
889
|
+
onMessage(handler) {
|
|
890
|
+
this.messageHandlers.push(handler);
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Register a callback query handler (for inline keyboard buttons)
|
|
894
|
+
*/
|
|
895
|
+
onCallbackQuery(handler) {
|
|
896
|
+
this.callbackQueryHandlers.push(handler);
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Answer a callback query (acknowledge button press)
|
|
900
|
+
* Call this to remove the loading state from the button.
|
|
901
|
+
*/
|
|
902
|
+
async answerCallbackQuery(queryId, text, showAlert) {
|
|
903
|
+
if (!this.bot || this._status !== 'connected') {
|
|
904
|
+
throw new Error('Telegram bot is not connected');
|
|
905
|
+
}
|
|
906
|
+
await this.bot.api.answerCallbackQuery(queryId, {
|
|
907
|
+
text,
|
|
908
|
+
show_alert: showAlert,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Edit a message with a new inline keyboard
|
|
913
|
+
*/
|
|
914
|
+
async editMessageWithKeyboard(chatId, messageId, text, inlineKeyboard) {
|
|
915
|
+
if (!this.bot || this._status !== 'connected') {
|
|
916
|
+
throw new Error('Telegram bot is not connected');
|
|
917
|
+
}
|
|
918
|
+
const msgId = parseInt(messageId, 10);
|
|
919
|
+
if (isNaN(msgId)) {
|
|
920
|
+
throw new Error(`Invalid message ID: ${messageId}`);
|
|
921
|
+
}
|
|
922
|
+
const options = {};
|
|
923
|
+
if (inlineKeyboard && inlineKeyboard.length > 0) {
|
|
924
|
+
options.reply_markup = this.buildInlineKeyboard(inlineKeyboard);
|
|
925
|
+
}
|
|
926
|
+
if (text) {
|
|
927
|
+
await this.bot.api.editMessageText(chatId, msgId, text, options);
|
|
928
|
+
}
|
|
929
|
+
else if (inlineKeyboard) {
|
|
930
|
+
await this.bot.api.editMessageReplyMarkup(chatId, msgId, options);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// ============================================================================
|
|
934
|
+
// Extended Features
|
|
935
|
+
// ============================================================================
|
|
936
|
+
/**
|
|
937
|
+
* Send typing indicator (chat action)
|
|
938
|
+
*/
|
|
939
|
+
async sendTyping(chatId, threadId) {
|
|
940
|
+
if (!this.bot || this._status !== 'connected') {
|
|
941
|
+
throw new Error('Telegram bot is not connected');
|
|
942
|
+
}
|
|
943
|
+
const options = {};
|
|
944
|
+
if (threadId) {
|
|
945
|
+
options.message_thread_id = parseInt(threadId, 10);
|
|
946
|
+
}
|
|
947
|
+
await this.bot.api.sendChatAction(chatId, 'typing', options);
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Add reaction to a message
|
|
951
|
+
*/
|
|
952
|
+
async addReaction(chatId, messageId, emoji) {
|
|
953
|
+
if (!this.bot || this._status !== 'connected') {
|
|
954
|
+
throw new Error('Telegram bot is not connected');
|
|
955
|
+
}
|
|
956
|
+
const msgId = parseInt(messageId, 10);
|
|
957
|
+
// Cast emoji to the expected type - Telegram will reject invalid emojis at runtime
|
|
958
|
+
await this.bot.api.setMessageReaction(chatId, msgId, [{ type: 'emoji', emoji: emoji }]);
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Remove reaction from a message
|
|
962
|
+
*/
|
|
963
|
+
async removeReaction(chatId, messageId) {
|
|
964
|
+
if (!this.bot || this._status !== 'connected') {
|
|
965
|
+
throw new Error('Telegram bot is not connected');
|
|
966
|
+
}
|
|
967
|
+
const msgId = parseInt(messageId, 10);
|
|
968
|
+
await this.bot.api.setMessageReaction(chatId, msgId, []);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Send a poll
|
|
972
|
+
*/
|
|
973
|
+
async sendPoll(chatId, poll, threadId) {
|
|
974
|
+
if (!this.bot || this._status !== 'connected') {
|
|
975
|
+
throw new Error('Telegram bot is not connected');
|
|
976
|
+
}
|
|
977
|
+
const options = {
|
|
978
|
+
is_anonymous: poll.isAnonymous ?? true,
|
|
979
|
+
allows_multiple_answers: poll.allowsMultipleAnswers ?? false,
|
|
980
|
+
};
|
|
981
|
+
if (threadId) {
|
|
982
|
+
options.message_thread_id = parseInt(threadId, 10);
|
|
983
|
+
}
|
|
984
|
+
if (poll.type === 'quiz' && poll.correctOptionId !== undefined) {
|
|
985
|
+
options.type = 'quiz';
|
|
986
|
+
options.correct_option_id = poll.correctOptionId;
|
|
987
|
+
if (poll.explanation) {
|
|
988
|
+
options.explanation = poll.explanation;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (poll.openPeriod) {
|
|
992
|
+
options.open_period = poll.openPeriod;
|
|
993
|
+
}
|
|
994
|
+
else if (poll.closeDate) {
|
|
995
|
+
options.close_date = Math.floor(poll.closeDate.getTime() / 1000);
|
|
996
|
+
}
|
|
997
|
+
const sent = await this.bot.api.sendPoll(chatId, poll.question, poll.options.map(o => o.text), options);
|
|
998
|
+
return sent.message_id.toString();
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Send message with reply keyboard (persistent keyboard below input)
|
|
1002
|
+
*/
|
|
1003
|
+
async sendWithReplyKeyboard(chatId, text, keyboard, threadId) {
|
|
1004
|
+
if (!this.bot || this._status !== 'connected') {
|
|
1005
|
+
throw new Error('Telegram bot is not connected');
|
|
1006
|
+
}
|
|
1007
|
+
const replyMarkup = {
|
|
1008
|
+
keyboard: keyboard.buttons.map(row => row.map(btn => ({
|
|
1009
|
+
text: btn.text,
|
|
1010
|
+
request_contact: btn.requestContact,
|
|
1011
|
+
request_location: btn.requestLocation,
|
|
1012
|
+
}))),
|
|
1013
|
+
resize_keyboard: keyboard.resizeKeyboard ?? true,
|
|
1014
|
+
one_time_keyboard: keyboard.oneTimeKeyboard ?? false,
|
|
1015
|
+
input_field_placeholder: keyboard.inputPlaceholder,
|
|
1016
|
+
};
|
|
1017
|
+
const options = {
|
|
1018
|
+
reply_markup: replyMarkup,
|
|
1019
|
+
};
|
|
1020
|
+
if (threadId) {
|
|
1021
|
+
options.message_thread_id = parseInt(threadId, 10);
|
|
1022
|
+
}
|
|
1023
|
+
const sent = await this.bot.api.sendMessage(chatId, text, options);
|
|
1024
|
+
return sent.message_id.toString();
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Remove reply keyboard (send message that hides the keyboard)
|
|
1028
|
+
*/
|
|
1029
|
+
async removeReplyKeyboard(chatId, text, threadId) {
|
|
1030
|
+
if (!this.bot || this._status !== 'connected') {
|
|
1031
|
+
throw new Error('Telegram bot is not connected');
|
|
1032
|
+
}
|
|
1033
|
+
const options = {
|
|
1034
|
+
reply_markup: { remove_keyboard: true },
|
|
1035
|
+
};
|
|
1036
|
+
if (threadId) {
|
|
1037
|
+
options.message_thread_id = parseInt(threadId, 10);
|
|
1038
|
+
}
|
|
1039
|
+
const sent = await this.bot.api.sendMessage(chatId, text, options);
|
|
1040
|
+
return sent.message_id.toString();
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Send a sticker
|
|
1044
|
+
*/
|
|
1045
|
+
async sendSticker(chatId, stickerId, threadId) {
|
|
1046
|
+
if (!this.bot || this._status !== 'connected') {
|
|
1047
|
+
throw new Error('Telegram bot is not connected');
|
|
1048
|
+
}
|
|
1049
|
+
const options = {};
|
|
1050
|
+
if (threadId) {
|
|
1051
|
+
options.message_thread_id = parseInt(threadId, 10);
|
|
1052
|
+
}
|
|
1053
|
+
const sent = await this.bot.api.sendSticker(chatId, stickerId, options);
|
|
1054
|
+
return sent.message_id.toString();
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Send location
|
|
1058
|
+
*/
|
|
1059
|
+
async sendLocation(chatId, latitude, longitude, threadId) {
|
|
1060
|
+
if (!this.bot || this._status !== 'connected') {
|
|
1061
|
+
throw new Error('Telegram bot is not connected');
|
|
1062
|
+
}
|
|
1063
|
+
const options = {};
|
|
1064
|
+
if (threadId) {
|
|
1065
|
+
options.message_thread_id = parseInt(threadId, 10);
|
|
1066
|
+
}
|
|
1067
|
+
const sent = await this.bot.api.sendLocation(chatId, latitude, longitude, options);
|
|
1068
|
+
return sent.message_id.toString();
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Send a media group (album)
|
|
1072
|
+
*/
|
|
1073
|
+
async sendMediaGroup(chatId, media, threadId) {
|
|
1074
|
+
if (!this.bot || this._status !== 'connected') {
|
|
1075
|
+
throw new Error('Telegram bot is not connected');
|
|
1076
|
+
}
|
|
1077
|
+
const inputMedia = media.map((m, index) => {
|
|
1078
|
+
const fileBuffer = fs.readFileSync(m.filePath);
|
|
1079
|
+
const fileName = path.basename(m.filePath);
|
|
1080
|
+
return {
|
|
1081
|
+
type: m.type,
|
|
1082
|
+
media: new grammy_1.InputFile(fileBuffer, fileName),
|
|
1083
|
+
caption: index === 0 ? m.caption : undefined, // Caption on first item only
|
|
1084
|
+
};
|
|
1085
|
+
});
|
|
1086
|
+
const options = {};
|
|
1087
|
+
if (threadId) {
|
|
1088
|
+
options.message_thread_id = parseInt(threadId, 10);
|
|
1089
|
+
}
|
|
1090
|
+
const sent = await this.bot.api.sendMediaGroup(chatId, inputMedia, options);
|
|
1091
|
+
return sent.map(m => m.message_id.toString());
|
|
1092
|
+
}
|
|
1093
|
+
// ============================================================================
|
|
1094
|
+
// Handler Registration
|
|
1095
|
+
// ============================================================================
|
|
1096
|
+
/**
|
|
1097
|
+
* Register an error handler
|
|
1098
|
+
*/
|
|
1099
|
+
onError(handler) {
|
|
1100
|
+
this.errorHandlers.push(handler);
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Register a status change handler
|
|
1104
|
+
*/
|
|
1105
|
+
onStatusChange(handler) {
|
|
1106
|
+
this.statusHandlers.push(handler);
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Get channel info
|
|
1110
|
+
*/
|
|
1111
|
+
async getInfo() {
|
|
1112
|
+
let botId;
|
|
1113
|
+
let botDisplayName;
|
|
1114
|
+
if (this.bot && this._status === 'connected') {
|
|
1115
|
+
try {
|
|
1116
|
+
const me = await this.bot.api.getMe();
|
|
1117
|
+
botId = me.id.toString();
|
|
1118
|
+
botDisplayName = me.first_name;
|
|
1119
|
+
this._botUsername = me.username;
|
|
1120
|
+
}
|
|
1121
|
+
catch {
|
|
1122
|
+
// Ignore errors getting info
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return {
|
|
1126
|
+
type: 'telegram',
|
|
1127
|
+
status: this._status,
|
|
1128
|
+
botId,
|
|
1129
|
+
botUsername: this._botUsername,
|
|
1130
|
+
botDisplayName,
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Get webhook callback for Express/Fastify/etc.
|
|
1135
|
+
* Use this when running in webhook mode instead of polling.
|
|
1136
|
+
*/
|
|
1137
|
+
getWebhookCallback() {
|
|
1138
|
+
if (!this.bot) {
|
|
1139
|
+
throw new Error('Bot not initialized');
|
|
1140
|
+
}
|
|
1141
|
+
return (0, grammy_1.webhookCallback)(this.bot, 'express');
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Set webhook URL
|
|
1145
|
+
*/
|
|
1146
|
+
async setWebhook(url, secretToken) {
|
|
1147
|
+
if (!this.bot) {
|
|
1148
|
+
throw new Error('Bot not initialized');
|
|
1149
|
+
}
|
|
1150
|
+
await this.bot.api.setWebhook(url, {
|
|
1151
|
+
secret_token: secretToken,
|
|
1152
|
+
allowed_updates: ['message'],
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Remove webhook
|
|
1157
|
+
*/
|
|
1158
|
+
async deleteWebhook() {
|
|
1159
|
+
if (!this.bot) {
|
|
1160
|
+
throw new Error('Bot not initialized');
|
|
1161
|
+
}
|
|
1162
|
+
await this.bot.api.deleteWebhook();
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Start webhook server with health check endpoint
|
|
1166
|
+
* This creates an HTTP server that handles both webhook callbacks and health checks.
|
|
1167
|
+
*/
|
|
1168
|
+
async startWebhookServer(config) {
|
|
1169
|
+
if (this.webhookServer) {
|
|
1170
|
+
throw new Error('Webhook server is already running');
|
|
1171
|
+
}
|
|
1172
|
+
if (!this.bot) {
|
|
1173
|
+
throw new Error('Bot not initialized. Call connect() first or initialize bot manually.');
|
|
1174
|
+
}
|
|
1175
|
+
const { port, host = '0.0.0.0', secretToken, webhookPath = '/webhook', healthPath = '/healthz', } = config;
|
|
1176
|
+
// Create HTTP server
|
|
1177
|
+
this.webhookServer = http.createServer(async (req, res) => {
|
|
1178
|
+
const url = req.url || '/';
|
|
1179
|
+
// Health check endpoint
|
|
1180
|
+
if (req.method === 'GET' && url === healthPath) {
|
|
1181
|
+
await this.handleHealthCheck(req, res);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
// Webhook endpoint
|
|
1185
|
+
if (req.method === 'POST' && url === webhookPath) {
|
|
1186
|
+
// Validate secret token if configured
|
|
1187
|
+
if (secretToken) {
|
|
1188
|
+
const requestToken = req.headers['x-telegram-bot-api-secret-token'];
|
|
1189
|
+
if (requestToken !== secretToken) {
|
|
1190
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1191
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
// Handle webhook callback
|
|
1196
|
+
await this.handleWebhookRequest(req, res);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
// 404 for unknown routes
|
|
1200
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1201
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
1202
|
+
});
|
|
1203
|
+
// Start listening
|
|
1204
|
+
return new Promise((resolve, reject) => {
|
|
1205
|
+
this.webhookServer.listen(port, host, () => {
|
|
1206
|
+
console.log(`Telegram webhook server listening on ${host}:${port}`);
|
|
1207
|
+
console.log(` Webhook endpoint: ${webhookPath}`);
|
|
1208
|
+
console.log(` Health check: ${healthPath}`);
|
|
1209
|
+
resolve();
|
|
1210
|
+
});
|
|
1211
|
+
this.webhookServer.on('error', (error) => {
|
|
1212
|
+
console.error('Webhook server error:', error);
|
|
1213
|
+
reject(error);
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Stop the webhook server
|
|
1219
|
+
*/
|
|
1220
|
+
async stopWebhookServer() {
|
|
1221
|
+
if (!this.webhookServer) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
return new Promise((resolve) => {
|
|
1225
|
+
this.webhookServer.close(() => {
|
|
1226
|
+
console.log('Webhook server stopped');
|
|
1227
|
+
this.webhookServer = undefined;
|
|
1228
|
+
resolve();
|
|
1229
|
+
});
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Handle health check requests
|
|
1234
|
+
*/
|
|
1235
|
+
async handleHealthCheck(_req, res) {
|
|
1236
|
+
const health = {
|
|
1237
|
+
status: this._status === 'connected' ? 'healthy' : 'unhealthy',
|
|
1238
|
+
timestamp: new Date().toISOString(),
|
|
1239
|
+
bot: {
|
|
1240
|
+
status: this._status,
|
|
1241
|
+
username: this._botUsername || null,
|
|
1242
|
+
connected: this._status === 'connected',
|
|
1243
|
+
},
|
|
1244
|
+
uptime: process.uptime(),
|
|
1245
|
+
memory: process.memoryUsage(),
|
|
1246
|
+
};
|
|
1247
|
+
const statusCode = health.status === 'healthy' ? 200 : 503;
|
|
1248
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1249
|
+
res.end(JSON.stringify(health, null, 2));
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Handle webhook requests
|
|
1253
|
+
*/
|
|
1254
|
+
async handleWebhookRequest(req, res) {
|
|
1255
|
+
let body = '';
|
|
1256
|
+
req.on('data', (chunk) => {
|
|
1257
|
+
body += chunk.toString();
|
|
1258
|
+
});
|
|
1259
|
+
req.on('end', async () => {
|
|
1260
|
+
try {
|
|
1261
|
+
const update = JSON.parse(body);
|
|
1262
|
+
// Process the update using grammY's webhook handler
|
|
1263
|
+
if (this.bot) {
|
|
1264
|
+
await this.bot.handleUpdate(update);
|
|
1265
|
+
}
|
|
1266
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1267
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) {
|
|
1270
|
+
console.error('Error processing webhook:', error);
|
|
1271
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1272
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Connect using webhook mode instead of polling
|
|
1278
|
+
* Sets up the bot, registers commands, and starts the webhook server.
|
|
1279
|
+
*/
|
|
1280
|
+
async connectWithWebhook(webhookUrl, serverConfig) {
|
|
1281
|
+
if (this._status === 'connected' || this._status === 'connecting') {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
this.setStatus('connecting');
|
|
1285
|
+
this.resetBackoff();
|
|
1286
|
+
try {
|
|
1287
|
+
// Create bot instance
|
|
1288
|
+
this.bot = new grammy_1.Bot(this.config.botToken);
|
|
1289
|
+
// Add API throttling
|
|
1290
|
+
const throttler = (0, transformer_throttler_1.apiThrottler)();
|
|
1291
|
+
this.bot.api.config.use(throttler);
|
|
1292
|
+
// Add sequential processing
|
|
1293
|
+
if (this.config.sequentialProcessingEnabled) {
|
|
1294
|
+
this.bot.use((0, runner_1.sequentialize)(this.getSequentialKey));
|
|
1295
|
+
}
|
|
1296
|
+
// Get bot info
|
|
1297
|
+
const me = await this.bot.api.getMe();
|
|
1298
|
+
this._botUsername = me.username;
|
|
1299
|
+
// Register bot commands
|
|
1300
|
+
await this.registerBotCommands();
|
|
1301
|
+
// Set up message handler
|
|
1302
|
+
this.bot.on('message:text', async (ctx) => {
|
|
1303
|
+
await this.handleTextMessage(ctx);
|
|
1304
|
+
});
|
|
1305
|
+
// Handle errors
|
|
1306
|
+
this.bot.catch(async (err) => {
|
|
1307
|
+
await this.handleBotError(err);
|
|
1308
|
+
});
|
|
1309
|
+
// Start deduplication cleanup
|
|
1310
|
+
if (this.config.deduplicationEnabled) {
|
|
1311
|
+
this.startDedupCleanup();
|
|
1312
|
+
}
|
|
1313
|
+
// Start webhook server
|
|
1314
|
+
await this.startWebhookServer(serverConfig);
|
|
1315
|
+
// Set webhook URL with Telegram
|
|
1316
|
+
await this.setWebhook(webhookUrl, serverConfig.secretToken);
|
|
1317
|
+
console.log(`Telegram bot @${this._botUsername} connected via webhook`);
|
|
1318
|
+
this.setStatus('connected');
|
|
1319
|
+
}
|
|
1320
|
+
catch (error) {
|
|
1321
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1322
|
+
this.setStatus('error', err);
|
|
1323
|
+
throw err;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
// Private methods
|
|
1327
|
+
mapContextToMessage(ctx, overrideText) {
|
|
1328
|
+
const msg = ctx.message;
|
|
1329
|
+
const from = msg.from;
|
|
1330
|
+
const chat = msg.chat;
|
|
1331
|
+
// Check for forum topic (message_thread_id indicates a forum topic)
|
|
1332
|
+
const threadId = msg.message_thread_id?.toString();
|
|
1333
|
+
const isForumTopic = msg.is_topic_message === true || threadId !== undefined;
|
|
1334
|
+
return {
|
|
1335
|
+
messageId: msg.message_id.toString(),
|
|
1336
|
+
channel: 'telegram',
|
|
1337
|
+
userId: from.id.toString(),
|
|
1338
|
+
userName: from.first_name + (from.last_name ? ` ${from.last_name}` : ''),
|
|
1339
|
+
chatId: chat.id.toString(),
|
|
1340
|
+
text: overrideText ?? msg.text ?? '',
|
|
1341
|
+
timestamp: new Date(msg.date * 1000),
|
|
1342
|
+
replyTo: msg.reply_to_message?.message_id.toString(),
|
|
1343
|
+
threadId,
|
|
1344
|
+
isForumTopic,
|
|
1345
|
+
raw: ctx,
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
async handleIncomingMessage(message) {
|
|
1349
|
+
for (const handler of this.messageHandlers) {
|
|
1350
|
+
try {
|
|
1351
|
+
await handler(message);
|
|
1352
|
+
}
|
|
1353
|
+
catch (error) {
|
|
1354
|
+
console.error('Error in message handler:', error);
|
|
1355
|
+
this.handleError(error instanceof Error ? error : new Error(String(error)), 'messageHandler');
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
handleError(error, context) {
|
|
1360
|
+
for (const handler of this.errorHandlers) {
|
|
1361
|
+
try {
|
|
1362
|
+
handler(error, context);
|
|
1363
|
+
}
|
|
1364
|
+
catch (e) {
|
|
1365
|
+
console.error('Error in error handler:', e);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
setStatus(status, error) {
|
|
1370
|
+
this._status = status;
|
|
1371
|
+
for (const handler of this.statusHandlers) {
|
|
1372
|
+
try {
|
|
1373
|
+
handler(status, error);
|
|
1374
|
+
}
|
|
1375
|
+
catch (e) {
|
|
1376
|
+
console.error('Error in status handler:', e);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
exports.TelegramAdapter = TelegramAdapter;
|
|
1382
|
+
/**
|
|
1383
|
+
* Create a Telegram adapter from configuration
|
|
1384
|
+
*/
|
|
1385
|
+
function createTelegramAdapter(config) {
|
|
1386
|
+
if (!config.botToken) {
|
|
1387
|
+
throw new Error('Telegram bot token is required');
|
|
1388
|
+
}
|
|
1389
|
+
return new TelegramAdapter(config);
|
|
1390
|
+
}
|