@swarmclawai/swarmclaw 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Bot, InputFile } from 'grammy'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import type { Connector } from '@/types'
|
|
5
|
+
import type { PlatformConnector, ConnectorInstance, InboundMessage, InboundMediaType } from './types'
|
|
6
|
+
import { downloadInboundMediaToUpload, inferInboundMediaType, mimeFromPath, isImageMime } from './media'
|
|
7
|
+
import { isNoMessage } from './manager'
|
|
8
|
+
|
|
9
|
+
const telegram: PlatformConnector = {
|
|
10
|
+
async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
|
|
11
|
+
const bot = new Bot(botToken)
|
|
12
|
+
|
|
13
|
+
// Optional: restrict to specific chat IDs
|
|
14
|
+
const allowedChats = connector.config.chatIds
|
|
15
|
+
? connector.config.chatIds.split(',').map((s) => s.trim()).filter(Boolean)
|
|
16
|
+
: null
|
|
17
|
+
|
|
18
|
+
// Log all errors
|
|
19
|
+
bot.catch((err) => {
|
|
20
|
+
console.error(`[telegram] Bot error:`, err.message || err)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Delete any existing webhook so long polling works
|
|
24
|
+
await bot.api.deleteWebhook().catch((err) => {
|
|
25
|
+
console.error('[telegram] Failed to delete webhook:', err.message)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Log all incoming updates for debugging
|
|
29
|
+
bot.use(async (ctx, next) => {
|
|
30
|
+
console.log(`[telegram] Update received: chat=${ctx.chat?.id}, from=${ctx.from?.first_name}, hasText=${!!ctx.message?.text}`)
|
|
31
|
+
await next()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Handle /start command (required for new conversations)
|
|
35
|
+
bot.command('start', async (ctx) => {
|
|
36
|
+
console.log(`[telegram] /start from ${ctx.from?.first_name} (chat=${ctx.chat.id})`)
|
|
37
|
+
await ctx.reply('Hello! I\'m ready to chat. Send me a message.')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
bot.on('message', async (ctx) => {
|
|
41
|
+
if (!ctx.message || !ctx.from || !ctx.chat) return
|
|
42
|
+
const chatId = String(ctx.chat.id)
|
|
43
|
+
const raw = ctx.message as any
|
|
44
|
+
const text = raw.text || raw.caption || ''
|
|
45
|
+
console.log(`[telegram] Message from ${ctx.from.first_name} (chat=${chatId}): ${String(text).slice(0, 80)}`)
|
|
46
|
+
|
|
47
|
+
// Filter by allowed chats if configured
|
|
48
|
+
if (allowedChats && !allowedChats.includes(chatId)) {
|
|
49
|
+
console.log(`[telegram] Skipping — chat ${chatId} not in allowed list: ${allowedChats.join(',')}`)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const media: NonNullable<InboundMessage['media']> = []
|
|
54
|
+
const mediaCandidates: Array<{ fileId: string; mimeType?: string; fileName?: string; type: InboundMediaType }> = []
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(raw.photo) && raw.photo.length > 0) {
|
|
57
|
+
const largest = raw.photo[raw.photo.length - 1]
|
|
58
|
+
if (largest?.file_id) mediaCandidates.push({ fileId: largest.file_id, type: 'image' })
|
|
59
|
+
}
|
|
60
|
+
if (raw.video?.file_id) {
|
|
61
|
+
mediaCandidates.push({
|
|
62
|
+
fileId: raw.video.file_id,
|
|
63
|
+
type: 'video',
|
|
64
|
+
mimeType: raw.video.mime_type || undefined,
|
|
65
|
+
fileName: raw.video.file_name || undefined,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
if (raw.audio?.file_id) {
|
|
69
|
+
mediaCandidates.push({
|
|
70
|
+
fileId: raw.audio.file_id,
|
|
71
|
+
type: 'audio',
|
|
72
|
+
mimeType: raw.audio.mime_type || undefined,
|
|
73
|
+
fileName: raw.audio.file_name || undefined,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
if (raw.voice?.file_id) {
|
|
77
|
+
mediaCandidates.push({
|
|
78
|
+
fileId: raw.voice.file_id,
|
|
79
|
+
type: 'audio',
|
|
80
|
+
mimeType: raw.voice.mime_type || 'audio/ogg',
|
|
81
|
+
fileName: 'voice.ogg',
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
if (raw.document?.file_id) {
|
|
85
|
+
mediaCandidates.push({
|
|
86
|
+
fileId: raw.document.file_id,
|
|
87
|
+
type: inferInboundMediaType(raw.document.mime_type || undefined, raw.document.file_name || undefined, 'document'),
|
|
88
|
+
mimeType: raw.document.mime_type || undefined,
|
|
89
|
+
fileName: raw.document.file_name || undefined,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
if (raw.animation?.file_id) {
|
|
93
|
+
mediaCandidates.push({
|
|
94
|
+
fileId: raw.animation.file_id,
|
|
95
|
+
type: 'video',
|
|
96
|
+
mimeType: raw.animation.mime_type || undefined,
|
|
97
|
+
fileName: raw.animation.file_name || undefined,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const m of mediaCandidates) {
|
|
102
|
+
try {
|
|
103
|
+
const file = await bot.api.getFile(m.fileId)
|
|
104
|
+
if (!file?.file_path) throw new Error('Missing Telegram file_path')
|
|
105
|
+
const sourceUrl = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`
|
|
106
|
+
const stored = await downloadInboundMediaToUpload({
|
|
107
|
+
connectorId: connector.id,
|
|
108
|
+
mediaType: m.type,
|
|
109
|
+
url: sourceUrl,
|
|
110
|
+
fileName: m.fileName,
|
|
111
|
+
mimeType: m.mimeType,
|
|
112
|
+
})
|
|
113
|
+
if (stored) media.push(stored)
|
|
114
|
+
} catch (err: any) {
|
|
115
|
+
console.warn(`[telegram] Failed to fetch media ${m.fileId}:`, err?.message || String(err))
|
|
116
|
+
media.push({
|
|
117
|
+
type: m.type,
|
|
118
|
+
fileName: m.fileName,
|
|
119
|
+
mimeType: m.mimeType,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const inbound: InboundMessage = {
|
|
125
|
+
platform: 'telegram',
|
|
126
|
+
channelId: chatId,
|
|
127
|
+
channelName: ctx.chat.type === 'private'
|
|
128
|
+
? `DM:${ctx.from.first_name}`
|
|
129
|
+
: ('title' in ctx.chat ? ctx.chat.title : chatId),
|
|
130
|
+
senderId: String(ctx.from.id),
|
|
131
|
+
senderName: ctx.from.first_name + (ctx.from.last_name ? ` ${ctx.from.last_name}` : ''),
|
|
132
|
+
text: text || (media.length > 0 ? '(media message)' : ''),
|
|
133
|
+
imageUrl: media.find((m) => m.type === 'image')?.url,
|
|
134
|
+
media,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await ctx.api.sendChatAction(ctx.chat.id, 'typing')
|
|
139
|
+
const response = await onMessage(inbound)
|
|
140
|
+
|
|
141
|
+
if (isNoMessage(response)) return
|
|
142
|
+
|
|
143
|
+
// Telegram has a 4096 char limit
|
|
144
|
+
if (response.length <= 4096) {
|
|
145
|
+
await ctx.reply(response)
|
|
146
|
+
} else {
|
|
147
|
+
const chunks = response.match(/[\s\S]{1,4090}/g) || [response]
|
|
148
|
+
for (const chunk of chunks) {
|
|
149
|
+
await ctx.api.sendMessage(ctx.chat.id, chunk)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (err: any) {
|
|
153
|
+
console.error(`[telegram] Error handling message:`, err.message)
|
|
154
|
+
try {
|
|
155
|
+
await ctx.reply('Sorry, I encountered an error processing your message.')
|
|
156
|
+
} catch { /* ignore */ }
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Start polling — not awaited (runs in background)
|
|
161
|
+
bot.start({
|
|
162
|
+
allowed_updates: ['message', 'edited_message'],
|
|
163
|
+
onStart: (botInfo) => {
|
|
164
|
+
console.log(`[telegram] Bot started as @${botInfo.username} — polling for updates`)
|
|
165
|
+
},
|
|
166
|
+
}).catch((err) => {
|
|
167
|
+
console.error(`[telegram] Polling stopped with error:`, err.message || err)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
connector,
|
|
172
|
+
async sendMessage(channelId, text, options) {
|
|
173
|
+
const chatId = channelId
|
|
174
|
+
const caption = options?.caption || text || undefined
|
|
175
|
+
|
|
176
|
+
// Local file
|
|
177
|
+
if (options?.mediaPath) {
|
|
178
|
+
if (!fs.existsSync(options.mediaPath)) throw new Error(`File not found: ${options.mediaPath}`)
|
|
179
|
+
const mime = options.mimeType || mimeFromPath(options.mediaPath)
|
|
180
|
+
const inputFile = new InputFile(options.mediaPath, options.fileName || path.basename(options.mediaPath))
|
|
181
|
+
if (isImageMime(mime)) {
|
|
182
|
+
const msg = await bot.api.sendPhoto(chatId, inputFile, { caption })
|
|
183
|
+
return { messageId: String(msg.message_id) }
|
|
184
|
+
} else {
|
|
185
|
+
const msg = await bot.api.sendDocument(chatId, inputFile, { caption })
|
|
186
|
+
return { messageId: String(msg.message_id) }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// URL-based image
|
|
190
|
+
if (options?.imageUrl) {
|
|
191
|
+
const msg = await bot.api.sendPhoto(chatId, options.imageUrl, { caption })
|
|
192
|
+
return { messageId: String(msg.message_id) }
|
|
193
|
+
}
|
|
194
|
+
// URL-based file
|
|
195
|
+
if (options?.fileUrl) {
|
|
196
|
+
const msg = await bot.api.sendDocument(chatId, options.fileUrl, { caption })
|
|
197
|
+
return { messageId: String(msg.message_id) }
|
|
198
|
+
}
|
|
199
|
+
// Text only
|
|
200
|
+
const payload = text || caption || ''
|
|
201
|
+
if (payload.length <= 4096) {
|
|
202
|
+
const msg = await bot.api.sendMessage(chatId, payload)
|
|
203
|
+
return { messageId: String(msg.message_id) }
|
|
204
|
+
}
|
|
205
|
+
const chunks = payload.match(/[\s\S]{1,4090}/g) || [payload]
|
|
206
|
+
let lastId: string | undefined
|
|
207
|
+
for (const chunk of chunks) {
|
|
208
|
+
const msg = await bot.api.sendMessage(chatId, chunk)
|
|
209
|
+
lastId = String(msg.message_id)
|
|
210
|
+
}
|
|
211
|
+
return { messageId: lastId }
|
|
212
|
+
},
|
|
213
|
+
async stop() {
|
|
214
|
+
await bot.stop()
|
|
215
|
+
console.log(`[telegram] Bot stopped`)
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export default telegram
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Connector } from '@/types'
|
|
2
|
+
|
|
3
|
+
export type InboundMediaType = 'image' | 'video' | 'audio' | 'document' | 'file'
|
|
4
|
+
|
|
5
|
+
export interface InboundMedia {
|
|
6
|
+
type: InboundMediaType
|
|
7
|
+
fileName?: string
|
|
8
|
+
mimeType?: string
|
|
9
|
+
sizeBytes?: number
|
|
10
|
+
/** Public URL when available (typically /api/uploads/...) */
|
|
11
|
+
url?: string
|
|
12
|
+
/** Absolute local path where media was persisted, if stored */
|
|
13
|
+
localPath?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Inbound message from a chat platform */
|
|
17
|
+
export interface InboundMessage {
|
|
18
|
+
platform: string
|
|
19
|
+
channelId: string // platform-specific channel/chat ID
|
|
20
|
+
channelName?: string // human-readable name
|
|
21
|
+
senderId: string // platform-specific user ID
|
|
22
|
+
senderName: string // display name
|
|
23
|
+
text: string
|
|
24
|
+
imageUrl?: string
|
|
25
|
+
media?: InboundMedia[]
|
|
26
|
+
replyToMessageId?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A running connector instance */
|
|
30
|
+
export interface ConnectorInstance {
|
|
31
|
+
connector: Connector
|
|
32
|
+
stop: () => Promise<void>
|
|
33
|
+
/** Optional outbound send support for proactive agent notifications */
|
|
34
|
+
sendMessage?: (
|
|
35
|
+
channelId: string,
|
|
36
|
+
text: string,
|
|
37
|
+
options?: {
|
|
38
|
+
imageUrl?: string
|
|
39
|
+
fileUrl?: string
|
|
40
|
+
/** Absolute local file path (e.g. screenshot saved to disk) */
|
|
41
|
+
mediaPath?: string
|
|
42
|
+
mimeType?: string
|
|
43
|
+
fileName?: string
|
|
44
|
+
caption?: string
|
|
45
|
+
},
|
|
46
|
+
) => Promise<{ messageId?: string } | void>
|
|
47
|
+
/** Current QR code data URL (WhatsApp only, null when paired) */
|
|
48
|
+
qrDataUrl?: string | null
|
|
49
|
+
/** Whether the connector has successfully authenticated (WhatsApp only) */
|
|
50
|
+
authenticated?: boolean
|
|
51
|
+
/** Whether the connector has existing saved credentials (WhatsApp only) */
|
|
52
|
+
hasCredentials?: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Platform-specific connector implementation */
|
|
56
|
+
export interface PlatformConnector {
|
|
57
|
+
start(
|
|
58
|
+
connector: Connector,
|
|
59
|
+
botToken: string,
|
|
60
|
+
onMessage: (msg: InboundMessage) => Promise<string>,
|
|
61
|
+
): Promise<ConnectorInstance>
|
|
62
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import makeWASocket, {
|
|
2
|
+
useMultiFileAuthState,
|
|
3
|
+
DisconnectReason,
|
|
4
|
+
fetchLatestBaileysVersion,
|
|
5
|
+
normalizeMessageContent,
|
|
6
|
+
downloadMediaMessage,
|
|
7
|
+
} from '@whiskeysockets/baileys'
|
|
8
|
+
import QRCode from 'qrcode'
|
|
9
|
+
import path from 'path'
|
|
10
|
+
import fs from 'fs'
|
|
11
|
+
import type { Connector } from '@/types'
|
|
12
|
+
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
13
|
+
import { saveInboundMediaBuffer, mimeFromPath, isImageMime } from './media'
|
|
14
|
+
import { isNoMessage } from './manager'
|
|
15
|
+
|
|
16
|
+
import { DATA_DIR } from '../data-dir'
|
|
17
|
+
|
|
18
|
+
const AUTH_DIR = path.join(DATA_DIR, 'whatsapp-auth')
|
|
19
|
+
|
|
20
|
+
/** Normalize a phone number for JID matching — strip leading 0 or + */
|
|
21
|
+
function normalizeNumber(num: string): string {
|
|
22
|
+
let n = num.replace(/[\s\-()]/g, '')
|
|
23
|
+
// UK local: 07xxx → 447xxx
|
|
24
|
+
if (n.startsWith('0') && n.length >= 10) {
|
|
25
|
+
n = '44' + n.slice(1)
|
|
26
|
+
}
|
|
27
|
+
// Strip leading +
|
|
28
|
+
if (n.startsWith('+')) n = n.slice(1)
|
|
29
|
+
return n
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Check if auth directory has saved credentials */
|
|
33
|
+
function hasStoredCreds(authDir: string): boolean {
|
|
34
|
+
try {
|
|
35
|
+
return fs.existsSync(path.join(authDir, 'creds.json'))
|
|
36
|
+
} catch { return false }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Clear auth directory to force fresh QR pairing */
|
|
40
|
+
export function clearAuthDir(connectorId: string): void {
|
|
41
|
+
const authDir = path.join(AUTH_DIR, connectorId)
|
|
42
|
+
if (fs.existsSync(authDir)) {
|
|
43
|
+
fs.rmSync(authDir, { recursive: true, force: true })
|
|
44
|
+
console.log(`[whatsapp] Cleared auth state for connector ${connectorId}`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const whatsapp: PlatformConnector = {
|
|
49
|
+
async start(connector, _botToken, onMessage): Promise<ConnectorInstance> {
|
|
50
|
+
// Each connector gets its own auth directory
|
|
51
|
+
const authDir = path.join(AUTH_DIR, connector.id)
|
|
52
|
+
if (!fs.existsSync(authDir)) fs.mkdirSync(authDir, { recursive: true })
|
|
53
|
+
|
|
54
|
+
const { state, saveCreds } = await useMultiFileAuthState(authDir)
|
|
55
|
+
const { version } = await fetchLatestBaileysVersion()
|
|
56
|
+
|
|
57
|
+
let sock: ReturnType<typeof makeWASocket> | null = null
|
|
58
|
+
let stopped = false
|
|
59
|
+
let socketGen = 0 // Track socket generation to ignore stale events
|
|
60
|
+
|
|
61
|
+
const instance: ConnectorInstance = {
|
|
62
|
+
connector,
|
|
63
|
+
qrDataUrl: null,
|
|
64
|
+
authenticated: false,
|
|
65
|
+
hasCredentials: hasStoredCreds(authDir),
|
|
66
|
+
async sendMessage(channelId, text, options) {
|
|
67
|
+
if (!sock) throw new Error('WhatsApp connector is not connected')
|
|
68
|
+
// Local file path takes priority
|
|
69
|
+
if (options?.mediaPath) {
|
|
70
|
+
if (!fs.existsSync(options.mediaPath)) throw new Error(`File not found: ${options.mediaPath}`)
|
|
71
|
+
const buf = fs.readFileSync(options.mediaPath)
|
|
72
|
+
const mime = options.mimeType || mimeFromPath(options.mediaPath)
|
|
73
|
+
const caption = options.caption || text || undefined
|
|
74
|
+
const fName = options.fileName || path.basename(options.mediaPath)
|
|
75
|
+
let sent
|
|
76
|
+
if (isImageMime(mime)) {
|
|
77
|
+
sent = await sock.sendMessage(channelId, { image: buf, caption, mimetype: mime })
|
|
78
|
+
} else {
|
|
79
|
+
sent = await sock.sendMessage(channelId, { document: buf, fileName: fName, mimetype: mime, caption })
|
|
80
|
+
}
|
|
81
|
+
if (sent?.key?.id) sentMessageIds.add(sent.key.id)
|
|
82
|
+
return { messageId: sent?.key?.id || undefined }
|
|
83
|
+
}
|
|
84
|
+
if (options?.imageUrl) {
|
|
85
|
+
const sent = await sock.sendMessage(channelId, {
|
|
86
|
+
image: { url: options.imageUrl },
|
|
87
|
+
caption: options.caption || text || undefined,
|
|
88
|
+
})
|
|
89
|
+
if (sent?.key?.id) sentMessageIds.add(sent.key.id)
|
|
90
|
+
return { messageId: sent?.key?.id || undefined }
|
|
91
|
+
}
|
|
92
|
+
if (options?.fileUrl) {
|
|
93
|
+
const sent = await sock.sendMessage(channelId, {
|
|
94
|
+
document: { url: options.fileUrl },
|
|
95
|
+
fileName: options.fileName || 'attachment',
|
|
96
|
+
mimetype: options.mimeType || 'application/octet-stream',
|
|
97
|
+
caption: options.caption || text || undefined,
|
|
98
|
+
})
|
|
99
|
+
if (sent?.key?.id) sentMessageIds.add(sent.key.id)
|
|
100
|
+
return { messageId: sent?.key?.id || undefined }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const payload = text || options?.caption || ''
|
|
104
|
+
const chunks = payload.length <= 4096 ? [payload] : (payload.match(/[\s\S]{1,4000}/g) || [payload])
|
|
105
|
+
let lastMessageId: string | undefined
|
|
106
|
+
for (const chunk of chunks) {
|
|
107
|
+
const sent = await sock.sendMessage(channelId, { text: chunk })
|
|
108
|
+
if (sent?.key?.id) {
|
|
109
|
+
lastMessageId = sent.key.id
|
|
110
|
+
sentMessageIds.add(sent.key.id)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { messageId: lastMessageId }
|
|
114
|
+
},
|
|
115
|
+
async stop() {
|
|
116
|
+
stopped = true
|
|
117
|
+
try { sock?.end(undefined) } catch { /* ignore */ }
|
|
118
|
+
sock = null
|
|
119
|
+
console.log(`[whatsapp] Stopped connector: ${connector.name}`)
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Normalize allowed JIDs for matching
|
|
124
|
+
const allowedJids = connector.config.allowedJids
|
|
125
|
+
? connector.config.allowedJids.split(',').map((s) => normalizeNumber(s.trim())).filter(Boolean)
|
|
126
|
+
: null
|
|
127
|
+
|
|
128
|
+
// Track message IDs sent by the bot to avoid infinite loops in self-chat
|
|
129
|
+
const sentMessageIds = new Set<string>()
|
|
130
|
+
|
|
131
|
+
if (allowedJids) {
|
|
132
|
+
console.log(`[whatsapp] Allowed JIDs (normalized): ${allowedJids.join(', ')}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const startSocket = () => {
|
|
136
|
+
// Close previous socket to prevent stale event handlers
|
|
137
|
+
if (sock) {
|
|
138
|
+
try { sock.ev.removeAllListeners('connection.update') } catch { /* ignore */ }
|
|
139
|
+
try { sock.ev.removeAllListeners('messages.upsert') } catch { /* ignore */ }
|
|
140
|
+
try { sock.ev.removeAllListeners('creds.update') } catch { /* ignore */ }
|
|
141
|
+
try { sock.end(undefined) } catch { /* ignore */ }
|
|
142
|
+
sock = null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const gen = ++socketGen // Capture generation for stale detection
|
|
146
|
+
console.log(`[whatsapp] Starting socket gen=${gen} for ${connector.name} (hasCreds=${instance.hasCredentials})`)
|
|
147
|
+
|
|
148
|
+
sock = makeWASocket({
|
|
149
|
+
version,
|
|
150
|
+
auth: state,
|
|
151
|
+
browser: ['SwarmClaw', 'Chrome', '120.0'],
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
sock.ev.on('creds.update', () => {
|
|
155
|
+
saveCreds()
|
|
156
|
+
// Update hasCredentials after first cred save
|
|
157
|
+
instance.hasCredentials = true
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
sock.ev.on('connection.update', async (update) => {
|
|
161
|
+
if (gen !== socketGen) return // Ignore events from stale sockets
|
|
162
|
+
|
|
163
|
+
const { connection, lastDisconnect, qr } = update
|
|
164
|
+
console.log(`[whatsapp] Connection update gen=${gen}: connection=${connection}, hasQR=${!!qr}`)
|
|
165
|
+
|
|
166
|
+
if (qr) {
|
|
167
|
+
console.log(`[whatsapp] QR code generated for ${connector.name}`)
|
|
168
|
+
try {
|
|
169
|
+
instance.qrDataUrl = await QRCode.toDataURL(qr, {
|
|
170
|
+
width: 280,
|
|
171
|
+
margin: 2,
|
|
172
|
+
color: { dark: '#000000', light: '#ffffff' },
|
|
173
|
+
})
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error('[whatsapp] Failed to generate QR data URL:', err)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (connection === 'close') {
|
|
179
|
+
instance.qrDataUrl = null
|
|
180
|
+
const reason = (lastDisconnect?.error as any)?.output?.statusCode
|
|
181
|
+
console.log(`[whatsapp] Connection closed: reason=${reason} stopped=${stopped}`)
|
|
182
|
+
|
|
183
|
+
if (reason === DisconnectReason.loggedOut) {
|
|
184
|
+
// Session invalidated — clear auth and restart to get fresh QR
|
|
185
|
+
console.log(`[whatsapp] Logged out — clearing auth and restarting for fresh QR`)
|
|
186
|
+
instance.authenticated = false
|
|
187
|
+
instance.hasCredentials = false
|
|
188
|
+
clearAuthDir(connector.id)
|
|
189
|
+
if (!stopped) {
|
|
190
|
+
// Recreate auth dir and state for fresh start
|
|
191
|
+
fs.mkdirSync(authDir, { recursive: true })
|
|
192
|
+
setTimeout(startSocket, 1000)
|
|
193
|
+
}
|
|
194
|
+
} else if (reason === 440) {
|
|
195
|
+
// Conflict — another session replaced this one. Do NOT reconnect
|
|
196
|
+
// (reconnecting would create a ping-pong loop with the other session)
|
|
197
|
+
console.log(`[whatsapp] Session conflict (replaced by another connection) — stopping`)
|
|
198
|
+
instance.authenticated = false
|
|
199
|
+
} else if (!stopped) {
|
|
200
|
+
console.log(`[whatsapp] Reconnecting in 3s...`)
|
|
201
|
+
setTimeout(startSocket, 3000)
|
|
202
|
+
} else {
|
|
203
|
+
console.log(`[whatsapp] Disconnected permanently`)
|
|
204
|
+
}
|
|
205
|
+
} else if (connection === 'open') {
|
|
206
|
+
instance.authenticated = true
|
|
207
|
+
instance.hasCredentials = true
|
|
208
|
+
instance.qrDataUrl = null
|
|
209
|
+
console.log(`[whatsapp] Connected as ${sock?.user?.id}`)
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
sock.ev.on('messages.upsert', async (upsert) => {
|
|
214
|
+
const { messages, type } = upsert
|
|
215
|
+
console.log(`[whatsapp] messages.upsert gen=${gen}: type=${type}, count=${messages.length}`)
|
|
216
|
+
|
|
217
|
+
if (gen !== socketGen) {
|
|
218
|
+
console.log(`[whatsapp] Ignoring stale socket event (gen=${gen}, current=${socketGen})`)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
if (type !== 'notify') {
|
|
222
|
+
console.log(`[whatsapp] Ignoring non-notify upsert type: ${type}`)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const msg of messages) {
|
|
227
|
+
console.log(`[whatsapp] Processing message: fromMe=${msg.key.fromMe}, jid=${msg.key.remoteJid}, hasConversation=${!!msg.message?.conversation}, hasExtended=${!!msg.message?.extendedTextMessage}`)
|
|
228
|
+
|
|
229
|
+
if (msg.key.remoteJid === 'status@broadcast') continue
|
|
230
|
+
|
|
231
|
+
// Skip messages sent by the bot itself (tracked by ID to prevent infinite loops)
|
|
232
|
+
if (msg.key.id && sentMessageIds.has(msg.key.id)) {
|
|
233
|
+
console.log(`[whatsapp] Skipping own bot reply: ${msg.key.id}`)
|
|
234
|
+
sentMessageIds.delete(msg.key.id) // Clean up
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handle self-chat (same number messaging itself for testing)
|
|
239
|
+
// Self-chat JID can be phone format (447xxx@s.whatsapp.net) or LID format (185xxx@lid)
|
|
240
|
+
const remoteNum = msg.key.remoteJid?.split('@')[0] || ''
|
|
241
|
+
const remoteHost = msg.key.remoteJid?.split('@')[1] || ''
|
|
242
|
+
const myPhoneNum = sock?.user?.id?.split(':')[0] || ''
|
|
243
|
+
const myLid = sock?.user?.lid?.split(':')[0] || ''
|
|
244
|
+
const isSelfChat = (remoteNum === myPhoneNum) || (remoteHost === 'lid' && (myLid ? remoteNum === myLid : true))
|
|
245
|
+
console.log(`[whatsapp] Self-chat check: remote=${remoteNum}@${remoteHost}, myPhone=${myPhoneNum}, myLid=${myLid}, isSelf=${isSelfChat}`)
|
|
246
|
+
if (msg.key.fromMe && !isSelfChat) continue
|
|
247
|
+
|
|
248
|
+
const jid = msg.key.remoteJid || ''
|
|
249
|
+
|
|
250
|
+
// Match allowed JIDs using normalized numbers
|
|
251
|
+
// Self-chat always passes the filter (it's the bot's own account)
|
|
252
|
+
if (allowedJids && !isSelfChat) {
|
|
253
|
+
const jidNumber = jid.split('@')[0]
|
|
254
|
+
const matched = allowedJids.some((n) => jidNumber.includes(n) || n.includes(jidNumber))
|
|
255
|
+
console.log(`[whatsapp] JID filter: jidNumber=${jidNumber}, allowedJids=${allowedJids.join(',')}, matched=${matched}`)
|
|
256
|
+
if (!matched) {
|
|
257
|
+
console.log(`[whatsapp] Skipping message from non-allowed JID: ${jid}`)
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const content: any = normalizeMessageContent(msg.message as any) || msg.message || {}
|
|
263
|
+
const text = content?.conversation
|
|
264
|
+
|| content?.extendedTextMessage?.text
|
|
265
|
+
|| content?.imageMessage?.caption
|
|
266
|
+
|| content?.videoMessage?.caption
|
|
267
|
+
|| content?.documentMessage?.caption
|
|
268
|
+
|| ''
|
|
269
|
+
|
|
270
|
+
const media: NonNullable<InboundMessage['media']> = []
|
|
271
|
+
const mediaCandidate:
|
|
272
|
+
| { kind: 'image' | 'video' | 'audio' | 'document' | 'file'; payload: any }
|
|
273
|
+
| null =
|
|
274
|
+
content?.imageMessage
|
|
275
|
+
? { kind: 'image', payload: content.imageMessage }
|
|
276
|
+
: content?.videoMessage
|
|
277
|
+
? { kind: 'video', payload: content.videoMessage }
|
|
278
|
+
: content?.audioMessage
|
|
279
|
+
? { kind: 'audio', payload: content.audioMessage }
|
|
280
|
+
: content?.documentMessage
|
|
281
|
+
? { kind: 'document', payload: content.documentMessage }
|
|
282
|
+
: content?.stickerMessage
|
|
283
|
+
? { kind: 'image', payload: content.stickerMessage }
|
|
284
|
+
: null
|
|
285
|
+
|
|
286
|
+
if (mediaCandidate) {
|
|
287
|
+
try {
|
|
288
|
+
const buffer = await downloadMediaMessage(msg as any, 'buffer', {})
|
|
289
|
+
const saved = saveInboundMediaBuffer({
|
|
290
|
+
connectorId: connector.id,
|
|
291
|
+
buffer: buffer as Buffer,
|
|
292
|
+
mediaType: mediaCandidate.kind,
|
|
293
|
+
mimeType: mediaCandidate.payload?.mimetype || undefined,
|
|
294
|
+
fileName: mediaCandidate.payload?.fileName || undefined,
|
|
295
|
+
})
|
|
296
|
+
media.push(saved)
|
|
297
|
+
} catch (err: any) {
|
|
298
|
+
console.error(`[whatsapp] Failed to decode media: ${err?.message || String(err)}`)
|
|
299
|
+
media.push({
|
|
300
|
+
type: mediaCandidate.kind,
|
|
301
|
+
fileName: mediaCandidate.payload?.fileName || undefined,
|
|
302
|
+
mimeType: mediaCandidate.payload?.mimetype || undefined,
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!text && media.length === 0) continue
|
|
308
|
+
|
|
309
|
+
const senderName = msg.pushName || jid.split('@')[0]
|
|
310
|
+
const isGroup = jid.endsWith('@g.us')
|
|
311
|
+
|
|
312
|
+
console.log(`[whatsapp] Message from ${senderName} (${jid}): ${text.slice(0, 80)}`)
|
|
313
|
+
|
|
314
|
+
const inbound: InboundMessage = {
|
|
315
|
+
platform: 'whatsapp',
|
|
316
|
+
channelId: jid,
|
|
317
|
+
channelName: isGroup ? jid : `DM:${senderName}`,
|
|
318
|
+
senderId: msg.key.participant || jid,
|
|
319
|
+
senderName,
|
|
320
|
+
text: text || '(media message)',
|
|
321
|
+
imageUrl: media.find((m) => m.type === 'image')?.url,
|
|
322
|
+
media,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
await sock!.sendPresenceUpdate('composing', jid)
|
|
327
|
+
const response = await onMessage(inbound)
|
|
328
|
+
await sock!.sendPresenceUpdate('paused', jid)
|
|
329
|
+
|
|
330
|
+
if (!isNoMessage(response)) {
|
|
331
|
+
await instance.sendMessage?.(jid, response)
|
|
332
|
+
}
|
|
333
|
+
} catch (err: any) {
|
|
334
|
+
console.error(`[whatsapp] Error handling message:`, err.message)
|
|
335
|
+
try {
|
|
336
|
+
await sock!.sendMessage(jid, { text: 'Sorry, I encountered an error processing your message.' })
|
|
337
|
+
} catch { /* ignore */ }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
startSocket()
|
|
344
|
+
|
|
345
|
+
return instance
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export default whatsapp
|