@poolzin/pool-bot 2026.2.20 → 2026.2.22
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/CHANGELOG.md +25 -0
- package/dist/agents/api-key-rotation.js +47 -0
- package/dist/agents/apply-patch-update.js +19 -9
- package/dist/agents/apply-patch.js +72 -47
- package/dist/agents/bash-tools.exec.js +141 -559
- package/dist/agents/cli-backends.js +49 -6
- package/dist/agents/cli-runner/helpers.js +69 -152
- package/dist/agents/cli-runner.js +70 -19
- package/dist/agents/identity.js +20 -1
- package/dist/agents/image-sanitization.js +9 -0
- package/dist/agents/live-auth-keys.js +123 -26
- package/dist/agents/live-model-filter.js +13 -4
- package/dist/agents/model-auth.js +12 -0
- package/dist/agents/model-catalog.js +40 -9
- package/dist/agents/model-fallback.js +24 -0
- package/dist/agents/model-forward-compat.js +60 -23
- package/dist/agents/model-selection.js +134 -41
- package/dist/agents/pi-auth-json.js +2 -2
- package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
- package/dist/agents/pi-embedded-helpers/errors.js +140 -15
- package/dist/agents/pi-embedded-helpers/images.js +22 -12
- package/dist/agents/pi-embedded-helpers.js +2 -2
- package/dist/agents/pi-embedded-runner/abort.js +10 -3
- package/dist/agents/pi-embedded-runner/compact.js +230 -32
- package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
- package/dist/agents/pi-embedded-runner/google.js +109 -19
- package/dist/agents/pi-embedded-runner/history.js +35 -17
- package/dist/agents/pi-embedded-runner/run/attempt.js +386 -80
- package/dist/agents/pi-embedded-runner/run/images.js +81 -55
- package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
- package/dist/agents/pi-embedded-runner/run.js +193 -25
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
- package/dist/agents/pi-embedded-runner/runs.js +17 -8
- package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
- package/dist/agents/pi-embedded-runner.js +1 -1
- package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
- package/dist/agents/pi-embedded-subscribe.js +37 -0
- package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
- package/dist/agents/pi-model-discovery.js +9 -2
- package/dist/agents/pi-tool-definition-adapter.js +60 -8
- package/dist/agents/pi-tools.before-tool-call.js +1 -1
- package/dist/agents/pi-tools.js +113 -94
- package/dist/agents/pi-tools.read.js +337 -38
- package/dist/agents/poolbot-tools.js +14 -5
- package/dist/agents/provider/config-loader.js +76 -0
- package/dist/agents/provider/index.js +15 -0
- package/dist/agents/provider/integration.js +136 -0
- package/dist/agents/provider/models-dev.js +129 -0
- package/dist/agents/provider/rate-limits.js +458 -0
- package/dist/agents/provider/request-monitor.js +449 -0
- package/dist/agents/provider/session-binding.js +376 -0
- package/dist/agents/provider/token-pool.js +541 -0
- package/dist/agents/sandbox/docker.js +10 -5
- package/dist/agents/sandbox/registry.js +96 -46
- package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
- package/dist/agents/sandbox-paths.js +43 -10
- package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
- package/dist/agents/session-tool-result-guard.js +39 -39
- package/dist/agents/session-transcript-repair.js +36 -33
- package/dist/agents/session-write-lock.js +62 -44
- package/dist/agents/skills/frontmatter.js +49 -88
- package/dist/agents/skills/workspace.js +335 -28
- package/dist/agents/subagent-announce.js +508 -174
- package/dist/agents/subagent-registry.js +45 -4
- package/dist/agents/subagent-spawn.js +16 -33
- package/dist/agents/system-prompt-report.js +27 -10
- package/dist/agents/system-prompt.js +26 -32
- package/dist/agents/tool-call-id.js +69 -17
- package/dist/agents/tool-display-common.js +1 -1
- package/dist/agents/tool-images.js +64 -31
- package/dist/agents/tools/canvas-tool.js +17 -11
- package/dist/agents/tools/common.js +37 -19
- package/dist/agents/tools/cron-tool.js +40 -38
- package/dist/agents/tools/gateway.js +70 -2
- package/dist/agents/tools/message-tool.js +181 -40
- package/dist/agents/tools/nodes-tool.js +128 -36
- package/dist/agents/tools/nodes-utils.js +12 -38
- package/dist/agents/tools/session-status-tool.js +24 -71
- package/dist/agents/tools/sessions-helpers.js +38 -210
- package/dist/agents/tools/sessions-spawn-tool.js +28 -198
- package/dist/agents/tools/telegram-actions.js +58 -7
- package/dist/agents/tools/web-fetch-utils.js +112 -7
- package/dist/agents/tools/web-fetch.js +279 -175
- package/dist/agents/tools/web-shared.js +71 -8
- package/dist/agents/usage.js +25 -16
- package/dist/auto-reply/commands-registry.data.js +85 -11
- package/dist/auto-reply/dispatch.js +40 -21
- package/dist/auto-reply/reply/abort.js +102 -33
- package/dist/auto-reply/reply/commands-core.js +82 -33
- package/dist/auto-reply/reply/commands-export-session.js +1 -1
- package/dist/auto-reply/reply/commands-info.js +41 -12
- package/dist/auto-reply/reply/commands-subagents.js +352 -100
- package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
- package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
- package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
- package/dist/auto-reply/reply/inbound-meta.js +12 -1
- package/dist/auto-reply/reply/mentions.js +18 -11
- package/dist/auto-reply/reply/normalize-reply.js +17 -8
- package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
- package/dist/auto-reply/reply/session.js +102 -21
- package/dist/auto-reply/reply/streaming-directives.js +16 -5
- package/dist/auto-reply/status.js +73 -50
- package/dist/browser/extension-relay.js +3 -3
- package/dist/browser/http-auth.js +1 -1
- package/dist/browser/paths.js +2 -2
- package/dist/build-info.json +3 -3
- package/dist/channels/allowlist-match.js +20 -0
- package/dist/channels/allowlists/resolve-utils.js +65 -2
- package/dist/channels/chat-type.js +8 -4
- package/dist/channels/dock.js +127 -35
- package/dist/channels/draft-stream-loop.js +6 -2
- package/dist/channels/plugins/actions/telegram.js +42 -18
- package/dist/channels/plugins/allowlist-match.js +1 -1
- package/dist/channels/plugins/group-mentions.js +51 -41
- package/dist/channels/plugins/message-action-names.js +2 -0
- package/dist/channels/plugins/message-actions.js +24 -5
- package/dist/channels/plugins/normalize/discord.js +26 -4
- package/dist/channels/plugins/normalize/signal.js +35 -22
- package/dist/channels/plugins/onboarding/helpers.js +8 -26
- package/dist/channels/plugins/outbound/imessage.js +15 -14
- package/dist/channels/registry.js +20 -7
- package/dist/cli/acp-cli.js +7 -5
- package/dist/cli/browser-cli-extension.js +25 -12
- package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
- package/dist/cli/browser-cli-state.js +101 -145
- package/dist/cli/command-options.js +28 -0
- package/dist/cli/completion-cli.js +6 -6
- package/dist/cli/cron-cli/register.cron-add.js +25 -1
- package/dist/cli/cron-cli/register.cron-edit.js +44 -0
- package/dist/cli/cron-cli/shared.js +7 -1
- package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
- package/dist/cli/daemon-cli/lifecycle.js +23 -247
- package/dist/cli/daemon-cli/register-service-commands.js +25 -4
- package/dist/cli/daemon-cli.js +1 -0
- package/dist/cli/devices-cli.js +33 -20
- package/dist/cli/gateway-cli/register.js +37 -105
- package/dist/cli/gateway-cli/run.js +49 -11
- package/dist/cli/nodes-camera.js +59 -4
- package/dist/cli/nodes-cli/register.camera.js +27 -24
- package/dist/cli/nodes-cli/rpc.js +21 -38
- package/dist/cli/qr-cli.js +2 -2
- package/dist/cli/skills-cli.format.js +2 -2
- package/dist/cli/update-cli/progress.js +2 -2
- package/dist/cli/update-cli/restart-helper.js +28 -7
- package/dist/cli/update-cli/shared.js +7 -7
- package/dist/cli/update-cli/status.js +1 -1
- package/dist/cli/update-cli/update-command.js +14 -8
- package/dist/cli/update-cli/wizard.js +2 -2
- package/dist/cli/update-cli.js +21 -1027
- package/dist/commands/auth-choice.apply.anthropic.js +10 -2
- package/dist/commands/channels/add-mutators.js +3 -35
- package/dist/commands/channels/add.js +39 -51
- package/dist/commands/config-validation.js +1 -1
- package/dist/commands/configure.gateway-auth.js +52 -15
- package/dist/commands/configure.gateway.js +84 -40
- package/dist/commands/doctor-completion.js +3 -3
- package/dist/commands/doctor-config-flow.js +536 -16
- package/dist/commands/doctor-gateway-services.js +103 -79
- package/dist/commands/doctor-memory-search.js +9 -9
- package/dist/commands/doctor-platform-notes.js +57 -30
- package/dist/commands/doctor-prompter.js +26 -15
- package/dist/commands/doctor-session-locks.js +1 -1
- package/dist/commands/doctor.js +21 -9
- package/dist/commands/model-picker.js +120 -95
- package/dist/commands/models/set.js +2 -21
- package/dist/commands/models/shared.js +65 -37
- package/dist/commands/onboard-helpers.js +81 -39
- package/dist/commands/openai-codex-oauth.js +1 -1
- package/dist/commands/sessions.js +52 -53
- package/dist/commands/status.summary.js +52 -34
- package/dist/commands/test-wizard-helpers.js +2 -2
- package/dist/config/defaults.js +79 -42
- package/dist/config/group-policy.js +50 -18
- package/dist/config/includes.js +37 -10
- package/dist/config/schema.help.js +5 -4
- package/dist/config/schema.hints.js +2 -2
- package/dist/config/schema.labels.js +1 -0
- package/dist/config/sessions/group.js +12 -11
- package/dist/config/sessions/paths.js +137 -11
- package/dist/config/sessions/store.js +185 -65
- package/dist/config/sessions/types.js +15 -1
- package/dist/config/sessions.js +1 -0
- package/dist/config/telegram-custom-commands.js +3 -2
- package/dist/config/types.js +2 -0
- package/dist/config/zod-schema.agent-defaults.js +6 -27
- package/dist/config/zod-schema.agent-runtime.js +171 -79
- package/dist/config/zod-schema.providers-core.js +138 -65
- package/dist/config/zod-schema.session.js +49 -22
- package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
- package/dist/cron/isolated-agent/run.js +224 -57
- package/dist/cron/normalize.js +48 -45
- package/dist/cron/run-log.js +14 -0
- package/dist/cron/service/jobs.js +190 -28
- package/dist/cron/service/normalize.js +29 -11
- package/dist/cron/service/store.js +30 -44
- package/dist/cron/service/timer.js +182 -96
- package/dist/cron/service.js +3 -0
- package/dist/cron/stagger.js +37 -0
- package/dist/daemon/inspect.js +132 -92
- package/dist/daemon/runtime-paths.js +25 -4
- package/dist/daemon/service-audit.js +47 -16
- package/dist/discord/accounts.js +23 -20
- package/dist/discord/monitor/agent-components.js +1115 -219
- package/dist/discord/monitor/allow-list.js +114 -34
- package/dist/discord/monitor/listeners.js +204 -97
- package/dist/discord/monitor/message-handler.js +21 -10
- package/dist/discord/monitor/message-handler.preflight.js +195 -101
- package/dist/discord/monitor/message-handler.process.js +384 -123
- package/dist/discord/monitor/message-utils.js +86 -23
- package/dist/discord/monitor/native-command.js +77 -57
- package/dist/discord/monitor/provider.js +122 -117
- package/dist/discord/monitor/reply-context.js +20 -16
- package/dist/discord/monitor/reply-delivery.js +40 -8
- package/dist/discord/monitor/rest-fetch.js +22 -0
- package/dist/discord/monitor/threading.js +117 -24
- package/dist/discord/send.js +2 -1
- package/dist/discord/send.outbound.js +124 -11
- package/dist/discord/send.shared.js +112 -72
- package/dist/discord/voice-message.js +3 -3
- package/dist/gateway/auth.js +119 -44
- package/dist/gateway/call.js +76 -34
- package/dist/gateway/channel-health-monitor.js +57 -50
- package/dist/gateway/client.js +63 -29
- package/dist/gateway/control-ui-contract.js +1 -1
- package/dist/gateway/gateway-config-prompts.shared.js +2 -2
- package/dist/gateway/net.js +109 -1
- package/dist/gateway/protocol/index.js +5 -8
- package/dist/gateway/protocol/schema/agent.js +19 -1
- package/dist/gateway/protocol/schema/channels.js +21 -0
- package/dist/gateway/protocol/schema/cron.js +43 -30
- package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
- package/dist/gateway/protocol/schema/sessions.js +5 -1
- package/dist/gateway/protocol/schema.js +0 -1
- package/dist/gateway/server/presence-events.js +12 -0
- package/dist/gateway/server/ws-connection/message-handler.js +203 -212
- package/dist/gateway/server/ws-connection.js +58 -21
- package/dist/gateway/server-broadcast.js +18 -13
- package/dist/gateway/server-cron.js +177 -10
- package/dist/gateway/server-methods/agent-job.js +131 -38
- package/dist/gateway/server-methods/send.js +60 -14
- package/dist/gateway/server-methods/sessions.js +160 -96
- package/dist/gateway/server-methods/system.js +5 -7
- package/dist/gateway/server-methods-list.js +8 -0
- package/dist/gateway/server-methods.js +24 -8
- package/dist/gateway/server-node-events.js +278 -68
- package/dist/gateway/session-utils.fs.js +316 -75
- package/dist/gateway/session-utils.js +224 -70
- package/dist/gateway/sessions-patch.js +63 -20
- package/dist/gateway/test-temp-config.js +1 -1
- package/dist/gateway/tools-invoke-http.js +118 -70
- package/dist/gateway/ws-log.js +135 -107
- package/dist/hooks/frontmatter.js +36 -82
- package/dist/hooks/install.js +149 -139
- package/dist/hooks/internal-hooks.js +29 -4
- package/dist/hooks/plugin-hooks.js +2 -1
- package/dist/imessage/monitor/deliver.js +10 -4
- package/dist/imessage/monitor/monitor-provider.js +138 -375
- package/dist/imessage/monitor/runtime.js +4 -8
- package/dist/imessage/send.js +65 -19
- package/dist/infra/exec-approvals-allowlist.js +7 -0
- package/dist/infra/exec-approvals.js +35 -920
- package/dist/infra/exec-safe-bin-trust.js +64 -0
- package/dist/infra/heartbeat-runner.js +207 -134
- package/dist/infra/heartbeat-wake.js +183 -22
- package/dist/infra/install-source-utils.js +47 -0
- package/dist/infra/net/ssrf.js +170 -36
- package/dist/infra/outbound/deliver.js +224 -58
- package/dist/infra/outbound/message-action-spec.js +12 -5
- package/dist/infra/outbound/outbound-session.js +27 -25
- package/dist/infra/poolbot-root.js +32 -22
- package/dist/infra/ports.js +14 -11
- package/dist/infra/skills-remote.js +48 -37
- package/dist/infra/system-events.js +25 -11
- package/dist/infra/system-presence.js +26 -33
- package/dist/infra/tmp-poolbot-dir.js +81 -2
- package/dist/infra/wsl.js +37 -1
- package/dist/line/bot-message-context.js +163 -191
- package/dist/logging/subsystem.js +59 -22
- package/dist/markdown/ir.js +124 -50
- package/dist/media/store.js +1 -1
- package/dist/media-understanding/runner.entries.js +42 -25
- package/dist/media-understanding/runner.js +53 -488
- package/dist/memory/embeddings-gemini.js +53 -38
- package/dist/memory/manager-embedding-ops.js +48 -69
- package/dist/pairing/pairing-store.js +178 -119
- package/dist/plugin-sdk/index.js +34 -6
- package/dist/plugins/hooks.js +135 -14
- package/dist/plugins/install.js +190 -152
- package/dist/polls.js +11 -0
- package/dist/routing/resolve-route.js +190 -56
- package/dist/routing/session-key.js +38 -22
- package/dist/runtime.js +35 -9
- package/dist/security/audit-channel.js +1 -1
- package/dist/sessions/session-key-utils.js +29 -11
- package/dist/shared/frontmatter.js +5 -5
- package/dist/shared/node-list-types.js +1 -0
- package/dist/shared/string-normalization.js +15 -0
- package/dist/signal/monitor/event-handler.js +68 -36
- package/dist/signal/send.js +29 -37
- package/dist/slack/monitor/allow-list.js +10 -11
- package/dist/slack/monitor/commands.js +14 -3
- package/dist/slack/monitor/events/interactions.js +4 -4
- package/dist/slack/monitor/media.js +224 -16
- package/dist/slack/monitor/message-handler/dispatch.js +247 -13
- package/dist/slack/monitor/message-handler/prepare.js +128 -45
- package/dist/slack/monitor/slash.js +357 -144
- package/dist/slack/streaming.js +77 -0
- package/dist/telegram/accounts.js +40 -13
- package/dist/telegram/allowed-updates.js +3 -0
- package/dist/telegram/bot/delivery.js +129 -66
- package/dist/telegram/bot/helpers.js +136 -122
- package/dist/telegram/bot-handlers.js +600 -339
- package/dist/telegram/bot-message-context.js +115 -73
- package/dist/telegram/bot-message-dispatch.js +235 -104
- package/dist/telegram/bot-native-command-menu.js +3 -1
- package/dist/telegram/bot-native-commands.js +213 -193
- package/dist/telegram/bot.js +24 -132
- package/dist/telegram/draft-stream.js +84 -75
- package/dist/telegram/format.js +150 -6
- package/dist/telegram/send.js +415 -255
- package/dist/telegram/targets.js +21 -2
- package/dist/telegram/update-offset-store.js +19 -3
- package/dist/terminal/restore.js +5 -2
- package/dist/test-utils/fetch-mock.js +5 -0
- package/dist/version.js +18 -5
- package/dist/web/auto-reply/monitor/broadcast.js +7 -3
- package/dist/web/auto-reply/monitor/on-message.js +6 -3
- package/dist/web/inbound/media.js +34 -8
- package/dist/web/inbound/monitor.js +34 -17
- package/dist/web/inbound/send-api.js +18 -17
- package/dist/web/outbound.js +12 -5
- package/dist/wizard/clack-prompter.js +40 -7
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
- package/skills/apple-reminders/SKILL.md +100 -49
- package/skills/coding-agent/SKILL.md +34 -28
- package/skills/github/SKILL.md +131 -16
- package/skills/imsg/SKILL.md +112 -15
- package/skills/openhue/SKILL.md +101 -19
- package/skills/plcode-controller/SKILL.md +156 -0
- package/skills/plcode-controller/assets/operator-prompts.md +65 -0
- package/skills/plcode-controller/references/command-cheatsheet.md +53 -0
- package/skills/plcode-controller/references/failure-handling.md +60 -0
- package/skills/plcode-controller/references/model-selection.md +57 -0
- package/skills/plcode-controller/references/plan-vs-build.md +52 -0
- package/skills/plcode-controller/references/question-handling.md +40 -0
- package/skills/plcode-controller/references/session-management.md +63 -0
- package/skills/plcode-controller/references/workflow.md +35 -0
- package/skills/tmux/SKILL.md +111 -79
- package/skills/weather/SKILL.md +88 -25
|
@@ -1,55 +1,83 @@
|
|
|
1
|
-
|
|
1
|
+
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
|
2
2
|
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
|
3
3
|
import { createInboundDebouncer, resolveInboundDebounceMs, } from "../auto-reply/inbound-debounce.js";
|
|
4
4
|
import { buildCommandsPaginationKeyboard } from "../auto-reply/reply/commands-info.js";
|
|
5
5
|
import { buildModelsProviderData } from "../auto-reply/reply/commands-models.js";
|
|
6
6
|
import { resolveStoredModelOverride } from "../auto-reply/reply/model-selection.js";
|
|
7
|
-
import { buildCommandsMessagePaginated } from "../auto-reply/status.js";
|
|
8
7
|
import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
|
|
9
|
-
import {
|
|
8
|
+
import { buildCommandsMessagePaginated } from "../auto-reply/status.js";
|
|
9
|
+
import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js";
|
|
10
10
|
import { loadConfig } from "../config/config.js";
|
|
11
|
+
import { writeConfigFile } from "../config/io.js";
|
|
11
12
|
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
|
|
13
|
+
import { danger, logVerbose, warn } from "../globals.js";
|
|
14
|
+
import { enqueueSystemEvent } from "../infra/system-events.js";
|
|
15
|
+
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
|
|
12
16
|
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
|
13
17
|
import { resolveThreadSessionKeys } from "../routing/session-key.js";
|
|
14
|
-
import { writeConfigFile } from "../config/io.js";
|
|
15
|
-
import { danger, logVerbose, warn } from "../globals.js";
|
|
16
|
-
import { resolveMedia } from "./bot/delivery.js";
|
|
17
18
|
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
|
18
|
-
import {
|
|
19
|
-
import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
|
|
19
|
+
import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore, } from "./bot-access.js";
|
|
20
20
|
import { MEDIA_GROUP_TIMEOUT_MS } from "./bot-updates.js";
|
|
21
|
+
import { resolveMedia } from "./bot/delivery.js";
|
|
22
|
+
import { buildTelegramGroupPeerId, buildTelegramParentPeer, resolveTelegramForumThreadId, resolveTelegramGroupAllowFromContext, } from "./bot/helpers.js";
|
|
23
|
+
import { evaluateTelegramGroupBaseAccess, evaluateTelegramGroupPolicyAccess, } from "./group-access.js";
|
|
21
24
|
import { migrateTelegramGroupConfig } from "./group-migration.js";
|
|
22
25
|
import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
|
|
23
|
-
import { readTelegramAllowFromStore } from "./pairing-store.js";
|
|
24
|
-
import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js";
|
|
25
26
|
import { buildModelsKeyboard, buildProviderKeyboard, calculateTotalPages, getModelsPageSize, parseModelCallbackData, } from "./model-buttons.js";
|
|
26
27
|
import { buildInlineKeyboard } from "./send.js";
|
|
28
|
+
import { wasSentByBot } from "./sent-message-cache.js";
|
|
27
29
|
export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, mediaMaxBytes, telegramCfg, groupAllowFrom, resolveGroupPolicy, resolveTelegramGroupConfig, shouldSkipUpdate, processMessage, logger, }) => {
|
|
30
|
+
const DEFAULT_TEXT_FRAGMENT_MAX_GAP_MS = 1500;
|
|
28
31
|
const TELEGRAM_TEXT_FRAGMENT_START_THRESHOLD_CHARS = 4000;
|
|
29
|
-
const TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS =
|
|
32
|
+
const TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS = typeof opts.testTimings?.textFragmentGapMs === "number" &&
|
|
33
|
+
Number.isFinite(opts.testTimings.textFragmentGapMs)
|
|
34
|
+
? Math.max(10, Math.floor(opts.testTimings.textFragmentGapMs))
|
|
35
|
+
: DEFAULT_TEXT_FRAGMENT_MAX_GAP_MS;
|
|
30
36
|
const TELEGRAM_TEXT_FRAGMENT_MAX_ID_GAP = 1;
|
|
31
37
|
const TELEGRAM_TEXT_FRAGMENT_MAX_PARTS = 12;
|
|
32
38
|
const TELEGRAM_TEXT_FRAGMENT_MAX_TOTAL_CHARS = 50_000;
|
|
39
|
+
const mediaGroupTimeoutMs = typeof opts.testTimings?.mediaGroupFlushMs === "number" &&
|
|
40
|
+
Number.isFinite(opts.testTimings.mediaGroupFlushMs)
|
|
41
|
+
? Math.max(10, Math.floor(opts.testTimings.mediaGroupFlushMs))
|
|
42
|
+
: MEDIA_GROUP_TIMEOUT_MS;
|
|
33
43
|
const mediaGroupBuffer = new Map();
|
|
34
44
|
let mediaGroupProcessing = Promise.resolve();
|
|
35
45
|
const textFragmentBuffer = new Map();
|
|
36
46
|
let textFragmentProcessing = Promise.resolve();
|
|
37
47
|
const debounceMs = resolveInboundDebounceMs({ cfg, channel: "telegram" });
|
|
48
|
+
const buildSyntheticTextMessage = (params) => ({
|
|
49
|
+
...params.base,
|
|
50
|
+
...(params.from ? { from: params.from } : {}),
|
|
51
|
+
text: params.text,
|
|
52
|
+
caption: undefined,
|
|
53
|
+
caption_entities: undefined,
|
|
54
|
+
entities: undefined,
|
|
55
|
+
...(params.date != null ? { date: params.date } : {}),
|
|
56
|
+
});
|
|
57
|
+
const buildSyntheticContext = (ctx, message) => {
|
|
58
|
+
const getFile = typeof ctx.getFile === "function"
|
|
59
|
+
? ctx.getFile.bind(ctx)
|
|
60
|
+
: async () => ({});
|
|
61
|
+
return { message, me: ctx.me, getFile };
|
|
62
|
+
};
|
|
38
63
|
const inboundDebouncer = createInboundDebouncer({
|
|
39
64
|
debounceMs,
|
|
40
65
|
buildKey: (entry) => entry.debounceKey,
|
|
41
66
|
shouldDebounce: (entry) => {
|
|
42
|
-
if (entry.allMedia.length > 0)
|
|
67
|
+
if (entry.allMedia.length > 0) {
|
|
43
68
|
return false;
|
|
69
|
+
}
|
|
44
70
|
const text = entry.msg.text ?? entry.msg.caption ?? "";
|
|
45
|
-
if (!text.trim())
|
|
71
|
+
if (!text.trim()) {
|
|
46
72
|
return false;
|
|
73
|
+
}
|
|
47
74
|
return !hasControlCommand(text, cfg, { botUsername: entry.botUsername });
|
|
48
75
|
},
|
|
49
76
|
onFlush: async (entries) => {
|
|
50
77
|
const last = entries.at(-1);
|
|
51
|
-
if (!last)
|
|
78
|
+
if (!last) {
|
|
52
79
|
return;
|
|
80
|
+
}
|
|
53
81
|
if (entries.length === 1) {
|
|
54
82
|
await processMessage(last.ctx, last.allMedia, last.storeAllowFrom);
|
|
55
83
|
return;
|
|
@@ -58,21 +86,18 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
58
86
|
.map((entry) => entry.msg.text ?? entry.msg.caption ?? "")
|
|
59
87
|
.filter(Boolean)
|
|
60
88
|
.join("\n");
|
|
61
|
-
if (!combinedText.trim())
|
|
89
|
+
if (!combinedText.trim()) {
|
|
62
90
|
return;
|
|
91
|
+
}
|
|
63
92
|
const first = entries[0];
|
|
64
93
|
const baseCtx = first.ctx;
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
...first.msg,
|
|
94
|
+
const syntheticMessage = buildSyntheticTextMessage({
|
|
95
|
+
base: first.msg,
|
|
68
96
|
text: combinedText,
|
|
69
|
-
caption: undefined,
|
|
70
|
-
caption_entities: undefined,
|
|
71
|
-
entities: undefined,
|
|
72
97
|
date: last.msg.date ?? first.msg.date,
|
|
73
|
-
};
|
|
98
|
+
});
|
|
74
99
|
const messageIdOverride = last.msg.message_id ? String(last.msg.message_id) : undefined;
|
|
75
|
-
await processMessage(
|
|
100
|
+
await processMessage(buildSyntheticContext(baseCtx, syntheticMessage), [], first.storeAllowFrom, messageIdOverride ? { messageIdOverride } : undefined);
|
|
76
101
|
},
|
|
77
102
|
onError: (err) => {
|
|
78
103
|
runtime.error?.(danger(`telegram debounce flush failed: ${String(err)}`));
|
|
@@ -97,7 +122,7 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
97
122
|
channel: "telegram",
|
|
98
123
|
accountId,
|
|
99
124
|
peer: {
|
|
100
|
-
kind: params.isGroup ? "group" : "
|
|
125
|
+
kind: params.isGroup ? "group" : "direct",
|
|
101
126
|
id: peerId,
|
|
102
127
|
},
|
|
103
128
|
parentPeer,
|
|
@@ -145,7 +170,7 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
145
170
|
});
|
|
146
171
|
}
|
|
147
172
|
}
|
|
148
|
-
const storeAllowFrom = await
|
|
173
|
+
const storeAllowFrom = await loadStoreAllowFrom();
|
|
149
174
|
await processMessage(primaryEntry.ctx, allMedia, storeAllowFrom);
|
|
150
175
|
}
|
|
151
176
|
catch (err) {
|
|
@@ -157,82 +182,415 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
157
182
|
entry.messages.sort((a, b) => a.msg.message_id - b.msg.message_id);
|
|
158
183
|
const first = entry.messages[0];
|
|
159
184
|
const last = entry.messages.at(-1);
|
|
160
|
-
if (!first || !last)
|
|
185
|
+
if (!first || !last) {
|
|
161
186
|
return;
|
|
187
|
+
}
|
|
162
188
|
const combinedText = entry.messages.map((m) => m.msg.text ?? "").join("");
|
|
163
|
-
if (!combinedText.trim())
|
|
189
|
+
if (!combinedText.trim()) {
|
|
164
190
|
return;
|
|
165
|
-
|
|
166
|
-
|
|
191
|
+
}
|
|
192
|
+
const syntheticMessage = buildSyntheticTextMessage({
|
|
193
|
+
base: first.msg,
|
|
167
194
|
text: combinedText,
|
|
168
|
-
caption: undefined,
|
|
169
|
-
caption_entities: undefined,
|
|
170
|
-
entities: undefined,
|
|
171
195
|
date: last.msg.date ?? first.msg.date,
|
|
172
|
-
};
|
|
173
|
-
const storeAllowFrom = await
|
|
196
|
+
});
|
|
197
|
+
const storeAllowFrom = await loadStoreAllowFrom();
|
|
174
198
|
const baseCtx = first.ctx;
|
|
175
|
-
|
|
176
|
-
|
|
199
|
+
await processMessage(buildSyntheticContext(baseCtx, syntheticMessage), [], storeAllowFrom, {
|
|
200
|
+
messageIdOverride: String(last.msg.message_id),
|
|
201
|
+
});
|
|
177
202
|
}
|
|
178
203
|
catch (err) {
|
|
179
204
|
runtime.error?.(danger(`text fragment handler failed: ${String(err)}`));
|
|
180
205
|
}
|
|
181
206
|
};
|
|
207
|
+
const queueTextFragmentFlush = async (entry) => {
|
|
208
|
+
textFragmentProcessing = textFragmentProcessing
|
|
209
|
+
.then(async () => {
|
|
210
|
+
await flushTextFragments(entry);
|
|
211
|
+
})
|
|
212
|
+
.catch(() => undefined);
|
|
213
|
+
await textFragmentProcessing;
|
|
214
|
+
};
|
|
215
|
+
const runTextFragmentFlush = async (entry) => {
|
|
216
|
+
textFragmentBuffer.delete(entry.key);
|
|
217
|
+
await queueTextFragmentFlush(entry);
|
|
218
|
+
};
|
|
182
219
|
const scheduleTextFragmentFlush = (entry) => {
|
|
183
220
|
clearTimeout(entry.timer);
|
|
184
221
|
entry.timer = setTimeout(async () => {
|
|
185
|
-
|
|
186
|
-
textFragmentProcessing = textFragmentProcessing
|
|
187
|
-
.then(async () => {
|
|
188
|
-
await flushTextFragments(entry);
|
|
189
|
-
})
|
|
190
|
-
.catch(() => undefined);
|
|
191
|
-
await textFragmentProcessing;
|
|
222
|
+
await runTextFragmentFlush(entry);
|
|
192
223
|
}, TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS);
|
|
193
224
|
};
|
|
225
|
+
const loadStoreAllowFrom = async () => readChannelAllowFromStore("telegram", process.env, accountId).catch(() => []);
|
|
226
|
+
const isAllowlistAuthorized = (allow, senderId, senderUsername) => allow.hasWildcard ||
|
|
227
|
+
(allow.hasEntries &&
|
|
228
|
+
isSenderAllowed({
|
|
229
|
+
allow,
|
|
230
|
+
senderId,
|
|
231
|
+
senderUsername,
|
|
232
|
+
}));
|
|
233
|
+
const shouldSkipGroupMessage = (params) => {
|
|
234
|
+
const { isGroup, chatId, chatTitle, resolvedThreadId, senderId, senderUsername, effectiveGroupAllow, hasGroupAllowOverride, groupConfig, topicConfig, } = params;
|
|
235
|
+
const baseAccess = evaluateTelegramGroupBaseAccess({
|
|
236
|
+
isGroup,
|
|
237
|
+
groupConfig,
|
|
238
|
+
topicConfig,
|
|
239
|
+
hasGroupAllowOverride,
|
|
240
|
+
effectiveGroupAllow,
|
|
241
|
+
senderId,
|
|
242
|
+
senderUsername,
|
|
243
|
+
enforceAllowOverride: true,
|
|
244
|
+
requireSenderForAllowOverride: true,
|
|
245
|
+
});
|
|
246
|
+
if (!baseAccess.allowed) {
|
|
247
|
+
if (baseAccess.reason === "group-disabled") {
|
|
248
|
+
logVerbose(`Blocked telegram group ${chatId} (group disabled)`);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
if (baseAccess.reason === "topic-disabled") {
|
|
252
|
+
logVerbose(`Blocked telegram topic ${chatId} (${resolvedThreadId ?? "unknown"}) (topic disabled)`);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
logVerbose(`Blocked telegram group sender ${senderId || "unknown"} (group allowFrom override)`);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (!isGroup) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
const policyAccess = evaluateTelegramGroupPolicyAccess({
|
|
262
|
+
isGroup,
|
|
263
|
+
chatId,
|
|
264
|
+
cfg,
|
|
265
|
+
telegramCfg,
|
|
266
|
+
topicConfig,
|
|
267
|
+
groupConfig,
|
|
268
|
+
effectiveGroupAllow,
|
|
269
|
+
senderId,
|
|
270
|
+
senderUsername,
|
|
271
|
+
resolveGroupPolicy,
|
|
272
|
+
enforcePolicy: true,
|
|
273
|
+
useTopicAndGroupOverrides: true,
|
|
274
|
+
enforceAllowlistAuthorization: true,
|
|
275
|
+
allowEmptyAllowlistEntries: false,
|
|
276
|
+
requireSenderForAllowlistAuthorization: true,
|
|
277
|
+
checkChatAllowlist: true,
|
|
278
|
+
});
|
|
279
|
+
if (!policyAccess.allowed) {
|
|
280
|
+
if (policyAccess.reason === "group-policy-disabled") {
|
|
281
|
+
logVerbose("Blocked telegram group message (groupPolicy: disabled)");
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
if (policyAccess.reason === "group-policy-allowlist-no-sender") {
|
|
285
|
+
logVerbose("Blocked telegram group message (no sender ID, groupPolicy: allowlist)");
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if (policyAccess.reason === "group-policy-allowlist-empty") {
|
|
289
|
+
logVerbose("Blocked telegram group message (groupPolicy: allowlist, no group allowlist entries)");
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
if (policyAccess.reason === "group-policy-allowlist-unauthorized") {
|
|
293
|
+
logVerbose(`Blocked telegram group message from ${senderId} (groupPolicy: allowlist)`);
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
logger.info({ chatId, title: chatTitle, reason: "not-allowed" }, "skipping group message");
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
};
|
|
301
|
+
// Handle emoji reactions to messages.
|
|
302
|
+
bot.on("message_reaction", async (ctx) => {
|
|
303
|
+
try {
|
|
304
|
+
const reaction = ctx.messageReaction;
|
|
305
|
+
if (!reaction) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (shouldSkipUpdate(ctx)) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const chatId = reaction.chat.id;
|
|
312
|
+
const messageId = reaction.message_id;
|
|
313
|
+
const user = reaction.user;
|
|
314
|
+
// Resolve reaction notification mode (default: "own").
|
|
315
|
+
const reactionMode = telegramCfg.reactionNotifications ?? "own";
|
|
316
|
+
if (reactionMode === "off") {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (user?.is_bot) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (reactionMode === "own" && !wasSentByBot(chatId, messageId)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Detect added reactions.
|
|
326
|
+
const oldEmojis = new Set(reaction.old_reaction
|
|
327
|
+
.filter((r) => r.type === "emoji")
|
|
328
|
+
.map((r) => r.emoji));
|
|
329
|
+
const addedReactions = reaction.new_reaction
|
|
330
|
+
.filter((r) => r.type === "emoji")
|
|
331
|
+
.filter((r) => !oldEmojis.has(r.emoji));
|
|
332
|
+
if (addedReactions.length === 0) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Build sender label.
|
|
336
|
+
const senderName = user
|
|
337
|
+
? [user.first_name, user.last_name].filter(Boolean).join(" ").trim() || user.username
|
|
338
|
+
: undefined;
|
|
339
|
+
const senderUsername = user?.username ? `@${user.username}` : undefined;
|
|
340
|
+
let senderLabel = senderName;
|
|
341
|
+
if (senderName && senderUsername) {
|
|
342
|
+
senderLabel = `${senderName} (${senderUsername})`;
|
|
343
|
+
}
|
|
344
|
+
else if (!senderName && senderUsername) {
|
|
345
|
+
senderLabel = senderUsername;
|
|
346
|
+
}
|
|
347
|
+
if (!senderLabel && user?.id) {
|
|
348
|
+
senderLabel = `id:${user.id}`;
|
|
349
|
+
}
|
|
350
|
+
senderLabel = senderLabel || "unknown";
|
|
351
|
+
// Reactions target a specific message_id; the Telegram Bot API does not include
|
|
352
|
+
// message_thread_id on MessageReactionUpdated, so we route to the chat-level
|
|
353
|
+
// session (forum topic routing is not available for reactions).
|
|
354
|
+
const isGroup = reaction.chat.type === "group" || reaction.chat.type === "supergroup";
|
|
355
|
+
const isForum = reaction.chat.is_forum === true;
|
|
356
|
+
const resolvedThreadId = isForum
|
|
357
|
+
? resolveTelegramForumThreadId({ isForum, messageThreadId: undefined })
|
|
358
|
+
: undefined;
|
|
359
|
+
const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
|
|
360
|
+
const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId });
|
|
361
|
+
// Fresh config for bindings lookup; other routing inputs are payload-derived.
|
|
362
|
+
const route = resolveAgentRoute({
|
|
363
|
+
cfg: loadConfig(),
|
|
364
|
+
channel: "telegram",
|
|
365
|
+
accountId,
|
|
366
|
+
peer: { kind: isGroup ? "group" : "direct", id: peerId },
|
|
367
|
+
parentPeer,
|
|
368
|
+
});
|
|
369
|
+
const sessionKey = route.sessionKey;
|
|
370
|
+
// Enqueue system event for each added reaction.
|
|
371
|
+
for (const r of addedReactions) {
|
|
372
|
+
const emoji = r.emoji;
|
|
373
|
+
const text = `Telegram reaction added: ${emoji} by ${senderLabel} on msg ${messageId}`;
|
|
374
|
+
enqueueSystemEvent(text, {
|
|
375
|
+
sessionKey,
|
|
376
|
+
contextKey: `telegram:reaction:add:${chatId}:${messageId}:${user?.id ?? "anon"}:${emoji}`,
|
|
377
|
+
});
|
|
378
|
+
logVerbose(`telegram: reaction event enqueued: ${text}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
runtime.error?.(danger(`telegram reaction handler failed: ${String(err)}`));
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
const processInboundMessage = async (params) => {
|
|
386
|
+
const { ctx, msg, chatId, resolvedThreadId, storeAllowFrom, sendOversizeWarning, oversizeLogMessage, } = params;
|
|
387
|
+
// Text fragment handling - Telegram splits long pastes into multiple inbound messages (~4096 chars).
|
|
388
|
+
// We buffer “near-limit” messages and append immediately-following parts.
|
|
389
|
+
const text = typeof msg.text === "string" ? msg.text : undefined;
|
|
390
|
+
const isCommandLike = (text ?? "").trim().startsWith("/");
|
|
391
|
+
if (text && !isCommandLike) {
|
|
392
|
+
const nowMs = Date.now();
|
|
393
|
+
const senderId = msg.from?.id != null ? String(msg.from.id) : "unknown";
|
|
394
|
+
const key = `text:${chatId}:${resolvedThreadId ?? "main"}:${senderId}`;
|
|
395
|
+
const existing = textFragmentBuffer.get(key);
|
|
396
|
+
if (existing) {
|
|
397
|
+
const last = existing.messages.at(-1);
|
|
398
|
+
const lastMsgId = last?.msg.message_id;
|
|
399
|
+
const lastReceivedAtMs = last?.receivedAtMs ?? nowMs;
|
|
400
|
+
const idGap = typeof lastMsgId === "number" ? msg.message_id - lastMsgId : Infinity;
|
|
401
|
+
const timeGapMs = nowMs - lastReceivedAtMs;
|
|
402
|
+
const canAppend = idGap > 0 &&
|
|
403
|
+
idGap <= TELEGRAM_TEXT_FRAGMENT_MAX_ID_GAP &&
|
|
404
|
+
timeGapMs >= 0 &&
|
|
405
|
+
timeGapMs <= TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS;
|
|
406
|
+
if (canAppend) {
|
|
407
|
+
const currentTotalChars = existing.messages.reduce((sum, m) => sum + (m.msg.text?.length ?? 0), 0);
|
|
408
|
+
const nextTotalChars = currentTotalChars + text.length;
|
|
409
|
+
if (existing.messages.length + 1 <= TELEGRAM_TEXT_FRAGMENT_MAX_PARTS &&
|
|
410
|
+
nextTotalChars <= TELEGRAM_TEXT_FRAGMENT_MAX_TOTAL_CHARS) {
|
|
411
|
+
existing.messages.push({ msg, ctx, receivedAtMs: nowMs });
|
|
412
|
+
scheduleTextFragmentFlush(existing);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// Not appendable (or limits exceeded): flush buffered entry first, then continue normally.
|
|
417
|
+
clearTimeout(existing.timer);
|
|
418
|
+
textFragmentBuffer.delete(key);
|
|
419
|
+
textFragmentProcessing = textFragmentProcessing
|
|
420
|
+
.then(async () => {
|
|
421
|
+
await flushTextFragments(existing);
|
|
422
|
+
})
|
|
423
|
+
.catch(() => undefined);
|
|
424
|
+
await textFragmentProcessing;
|
|
425
|
+
}
|
|
426
|
+
const shouldStart = text.length >= TELEGRAM_TEXT_FRAGMENT_START_THRESHOLD_CHARS;
|
|
427
|
+
if (shouldStart) {
|
|
428
|
+
const entry = {
|
|
429
|
+
key,
|
|
430
|
+
messages: [{ msg, ctx, receivedAtMs: nowMs }],
|
|
431
|
+
timer: setTimeout(() => { }, TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS),
|
|
432
|
+
};
|
|
433
|
+
textFragmentBuffer.set(key, entry);
|
|
434
|
+
scheduleTextFragmentFlush(entry);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Media group handling - buffer multi-image messages
|
|
439
|
+
const mediaGroupId = msg.media_group_id;
|
|
440
|
+
if (mediaGroupId) {
|
|
441
|
+
const existing = mediaGroupBuffer.get(mediaGroupId);
|
|
442
|
+
if (existing) {
|
|
443
|
+
clearTimeout(existing.timer);
|
|
444
|
+
existing.messages.push({ msg, ctx });
|
|
445
|
+
existing.timer = setTimeout(async () => {
|
|
446
|
+
mediaGroupBuffer.delete(mediaGroupId);
|
|
447
|
+
mediaGroupProcessing = mediaGroupProcessing
|
|
448
|
+
.then(async () => {
|
|
449
|
+
await processMediaGroup(existing);
|
|
450
|
+
})
|
|
451
|
+
.catch(() => undefined);
|
|
452
|
+
await mediaGroupProcessing;
|
|
453
|
+
}, mediaGroupTimeoutMs);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
const entry = {
|
|
457
|
+
messages: [{ msg, ctx }],
|
|
458
|
+
timer: setTimeout(async () => {
|
|
459
|
+
mediaGroupBuffer.delete(mediaGroupId);
|
|
460
|
+
mediaGroupProcessing = mediaGroupProcessing
|
|
461
|
+
.then(async () => {
|
|
462
|
+
await processMediaGroup(entry);
|
|
463
|
+
})
|
|
464
|
+
.catch(() => undefined);
|
|
465
|
+
await mediaGroupProcessing;
|
|
466
|
+
}, mediaGroupTimeoutMs),
|
|
467
|
+
};
|
|
468
|
+
mediaGroupBuffer.set(mediaGroupId, entry);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
let media = null;
|
|
473
|
+
try {
|
|
474
|
+
media = await resolveMedia(ctx, mediaMaxBytes, opts.token, opts.proxyFetch);
|
|
475
|
+
}
|
|
476
|
+
catch (mediaErr) {
|
|
477
|
+
const errMsg = String(mediaErr);
|
|
478
|
+
if (errMsg.includes("exceeds") && errMsg.includes("MB limit")) {
|
|
479
|
+
if (sendOversizeWarning) {
|
|
480
|
+
const limitMb = Math.round(mediaMaxBytes / (1024 * 1024));
|
|
481
|
+
await withTelegramApiErrorLogging({
|
|
482
|
+
operation: "sendMessage",
|
|
483
|
+
runtime,
|
|
484
|
+
fn: () => bot.api.sendMessage(chatId, `⚠️ File too large. Maximum size is ${limitMb}MB.`, {
|
|
485
|
+
reply_to_message_id: msg.message_id,
|
|
486
|
+
}),
|
|
487
|
+
}).catch(() => { });
|
|
488
|
+
}
|
|
489
|
+
logger.warn({ chatId, error: errMsg }, oversizeLogMessage);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
throw mediaErr;
|
|
493
|
+
}
|
|
494
|
+
// Skip sticker-only messages where the sticker was skipped (animated/video)
|
|
495
|
+
// These have no media and no text content to process.
|
|
496
|
+
const hasText = Boolean((msg.text ?? msg.caption ?? "").trim());
|
|
497
|
+
if (msg.sticker && !media && !hasText) {
|
|
498
|
+
logVerbose("telegram: skipping sticker-only message (unsupported sticker type)");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const allMedia = media
|
|
502
|
+
? [
|
|
503
|
+
{
|
|
504
|
+
path: media.path,
|
|
505
|
+
contentType: media.contentType,
|
|
506
|
+
stickerMetadata: media.stickerMetadata,
|
|
507
|
+
},
|
|
508
|
+
]
|
|
509
|
+
: [];
|
|
510
|
+
const senderId = msg.from?.id ? String(msg.from.id) : "";
|
|
511
|
+
const conversationKey = resolvedThreadId != null ? `${chatId}:topic:${resolvedThreadId}` : String(chatId);
|
|
512
|
+
const debounceKey = senderId
|
|
513
|
+
? `telegram:${accountId ?? "default"}:${conversationKey}:${senderId}`
|
|
514
|
+
: null;
|
|
515
|
+
await inboundDebouncer.enqueue({
|
|
516
|
+
ctx,
|
|
517
|
+
msg,
|
|
518
|
+
allMedia,
|
|
519
|
+
storeAllowFrom,
|
|
520
|
+
debounceKey,
|
|
521
|
+
botUsername: ctx.me?.username,
|
|
522
|
+
});
|
|
523
|
+
};
|
|
194
524
|
bot.on("callback_query", async (ctx) => {
|
|
195
525
|
const callback = ctx.callbackQuery;
|
|
196
|
-
if (!callback)
|
|
526
|
+
if (!callback) {
|
|
197
527
|
return;
|
|
198
|
-
|
|
528
|
+
}
|
|
529
|
+
if (shouldSkipUpdate(ctx)) {
|
|
199
530
|
return;
|
|
531
|
+
}
|
|
532
|
+
const answerCallbackQuery = typeof ctx.answerCallbackQuery === "function"
|
|
533
|
+
? () => ctx.answerCallbackQuery()
|
|
534
|
+
: () => bot.api.answerCallbackQuery(callback.id);
|
|
200
535
|
// Answer immediately to prevent Telegram from retrying while we process
|
|
201
536
|
await withTelegramApiErrorLogging({
|
|
202
537
|
operation: "answerCallbackQuery",
|
|
203
538
|
runtime,
|
|
204
|
-
fn:
|
|
539
|
+
fn: answerCallbackQuery,
|
|
205
540
|
}).catch(() => { });
|
|
206
541
|
try {
|
|
207
542
|
const data = (callback.data ?? "").trim();
|
|
208
543
|
const callbackMessage = callback.message;
|
|
209
|
-
if (!data || !callbackMessage)
|
|
544
|
+
if (!data || !callbackMessage) {
|
|
210
545
|
return;
|
|
546
|
+
}
|
|
547
|
+
const editCallbackMessage = async (text, params) => {
|
|
548
|
+
const editTextFn = ctx.editMessageText;
|
|
549
|
+
if (typeof editTextFn === "function") {
|
|
550
|
+
return await ctx.editMessageText(text, params);
|
|
551
|
+
}
|
|
552
|
+
return await bot.api.editMessageText(callbackMessage.chat.id, callbackMessage.message_id, text, params);
|
|
553
|
+
};
|
|
554
|
+
const deleteCallbackMessage = async () => {
|
|
555
|
+
const deleteFn = ctx.deleteMessage;
|
|
556
|
+
if (typeof deleteFn === "function") {
|
|
557
|
+
return await ctx.deleteMessage();
|
|
558
|
+
}
|
|
559
|
+
return await bot.api.deleteMessage(callbackMessage.chat.id, callbackMessage.message_id);
|
|
560
|
+
};
|
|
561
|
+
const replyToCallbackChat = async (text, params) => {
|
|
562
|
+
const replyFn = ctx.reply;
|
|
563
|
+
if (typeof replyFn === "function") {
|
|
564
|
+
return await ctx.reply(text, params);
|
|
565
|
+
}
|
|
566
|
+
return await bot.api.sendMessage(callbackMessage.chat.id, text, params);
|
|
567
|
+
};
|
|
211
568
|
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
|
212
569
|
cfg,
|
|
213
570
|
accountId,
|
|
214
571
|
});
|
|
215
|
-
if (inlineButtonsScope === "off")
|
|
572
|
+
if (inlineButtonsScope === "off") {
|
|
216
573
|
return;
|
|
574
|
+
}
|
|
217
575
|
const chatId = callbackMessage.chat.id;
|
|
218
576
|
const isGroup = callbackMessage.chat.type === "group" || callbackMessage.chat.type === "supergroup";
|
|
219
|
-
if (inlineButtonsScope === "dm" && isGroup)
|
|
577
|
+
if (inlineButtonsScope === "dm" && isGroup) {
|
|
220
578
|
return;
|
|
221
|
-
|
|
579
|
+
}
|
|
580
|
+
if (inlineButtonsScope === "group" && !isGroup) {
|
|
222
581
|
return;
|
|
582
|
+
}
|
|
223
583
|
const messageThreadId = callbackMessage.message_thread_id;
|
|
224
584
|
const isForum = callbackMessage.chat.is_forum === true;
|
|
225
|
-
const
|
|
585
|
+
const groupAllowContext = await resolveTelegramGroupAllowFromContext({
|
|
586
|
+
chatId,
|
|
587
|
+
accountId,
|
|
226
588
|
isForum,
|
|
227
589
|
messageThreadId,
|
|
590
|
+
groupAllowFrom,
|
|
591
|
+
resolveTelegramGroupConfig,
|
|
228
592
|
});
|
|
229
|
-
const { groupConfig, topicConfig } =
|
|
230
|
-
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
|
231
|
-
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
|
|
232
|
-
const effectiveGroupAllow = normalizeAllowFromWithStore({
|
|
233
|
-
allowFrom: groupAllowOverride ?? groupAllowFrom,
|
|
234
|
-
storeAllowFrom,
|
|
235
|
-
});
|
|
593
|
+
const { resolvedThreadId, storeAllowFrom, groupConfig, topicConfig, effectiveGroupAllow, hasGroupAllowOverride, } = groupAllowContext;
|
|
236
594
|
const effectiveDmAllow = normalizeAllowFromWithStore({
|
|
237
595
|
allowFrom: telegramCfg.allowFrom,
|
|
238
596
|
storeAllowFrom,
|
|
@@ -240,93 +598,49 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
240
598
|
const dmPolicy = telegramCfg.dmPolicy ?? "pairing";
|
|
241
599
|
const senderId = callback.from?.id ? String(callback.from.id) : "";
|
|
242
600
|
const senderUsername = callback.from?.username ?? "";
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
senderId,
|
|
257
|
-
senderUsername,
|
|
258
|
-
});
|
|
259
|
-
if (!allowed) {
|
|
260
|
-
logVerbose(`Blocked telegram group sender ${senderId || "unknown"} (group allowFrom override)`);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
|
265
|
-
const groupPolicy = firstDefined(topicConfig?.groupPolicy, groupConfig?.groupPolicy, telegramCfg.groupPolicy, defaultGroupPolicy, "open");
|
|
266
|
-
if (groupPolicy === "disabled") {
|
|
267
|
-
logVerbose(`Blocked telegram group message (groupPolicy: disabled)`);
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (groupPolicy === "allowlist") {
|
|
271
|
-
if (!senderId) {
|
|
272
|
-
logVerbose(`Blocked telegram group message (no sender ID, groupPolicy: allowlist)`);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (!effectiveGroupAllow.hasEntries) {
|
|
276
|
-
logVerbose("Blocked telegram group message (groupPolicy: allowlist, no group allowlist entries)");
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (!isSenderAllowed({
|
|
280
|
-
allow: effectiveGroupAllow,
|
|
281
|
-
senderId,
|
|
282
|
-
senderUsername,
|
|
283
|
-
})) {
|
|
284
|
-
logVerbose(`Blocked telegram group message from ${senderId} (groupPolicy: allowlist)`);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
const groupAllowlist = resolveGroupPolicy(chatId);
|
|
289
|
-
if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
|
|
290
|
-
logger.info({ chatId, title: callbackMessage.chat.title, reason: "not-allowed" }, "skipping group message");
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
601
|
+
if (shouldSkipGroupMessage({
|
|
602
|
+
isGroup,
|
|
603
|
+
chatId,
|
|
604
|
+
chatTitle: callbackMessage.chat.title,
|
|
605
|
+
resolvedThreadId,
|
|
606
|
+
senderId,
|
|
607
|
+
senderUsername,
|
|
608
|
+
effectiveGroupAllow,
|
|
609
|
+
hasGroupAllowOverride,
|
|
610
|
+
groupConfig,
|
|
611
|
+
topicConfig,
|
|
612
|
+
})) {
|
|
613
|
+
return;
|
|
293
614
|
}
|
|
294
615
|
if (inlineButtonsScope === "allowlist") {
|
|
295
616
|
if (!isGroup) {
|
|
296
|
-
if (dmPolicy === "disabled")
|
|
617
|
+
if (dmPolicy === "disabled") {
|
|
297
618
|
return;
|
|
619
|
+
}
|
|
298
620
|
if (dmPolicy !== "open") {
|
|
299
|
-
const allowed = effectiveDmAllow
|
|
300
|
-
|
|
301
|
-
isSenderAllowed({
|
|
302
|
-
allow: effectiveDmAllow,
|
|
303
|
-
senderId,
|
|
304
|
-
senderUsername,
|
|
305
|
-
}));
|
|
306
|
-
if (!allowed)
|
|
621
|
+
const allowed = isAllowlistAuthorized(effectiveDmAllow, senderId, senderUsername);
|
|
622
|
+
if (!allowed) {
|
|
307
623
|
return;
|
|
624
|
+
}
|
|
308
625
|
}
|
|
309
626
|
}
|
|
310
627
|
else {
|
|
311
|
-
const allowed = effectiveGroupAllow
|
|
312
|
-
|
|
313
|
-
isSenderAllowed({
|
|
314
|
-
allow: effectiveGroupAllow,
|
|
315
|
-
senderId,
|
|
316
|
-
senderUsername,
|
|
317
|
-
}));
|
|
318
|
-
if (!allowed)
|
|
628
|
+
const allowed = isAllowlistAuthorized(effectiveGroupAllow, senderId, senderUsername);
|
|
629
|
+
if (!allowed) {
|
|
319
630
|
return;
|
|
631
|
+
}
|
|
320
632
|
}
|
|
321
633
|
}
|
|
322
634
|
const paginationMatch = data.match(/^commands_page_(\d+|noop)(?::(.+))?$/);
|
|
323
635
|
if (paginationMatch) {
|
|
324
636
|
const pageValue = paginationMatch[1];
|
|
325
|
-
if (pageValue === "noop")
|
|
637
|
+
if (pageValue === "noop") {
|
|
326
638
|
return;
|
|
639
|
+
}
|
|
327
640
|
const page = Number.parseInt(pageValue, 10);
|
|
328
|
-
if (Number.isNaN(page) || page < 1)
|
|
641
|
+
if (Number.isNaN(page) || page < 1) {
|
|
329
642
|
return;
|
|
643
|
+
}
|
|
330
644
|
const agentId = paginationMatch[2]?.trim() || resolveDefaultAgentId(cfg) || undefined;
|
|
331
645
|
const skillCommands = listSkillCommandsForAgents({
|
|
332
646
|
cfg,
|
|
@@ -340,7 +654,7 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
340
654
|
? buildInlineKeyboard(buildCommandsPaginationKeyboard(result.currentPage, result.totalPages, agentId))
|
|
341
655
|
: undefined;
|
|
342
656
|
try {
|
|
343
|
-
await
|
|
657
|
+
await editCallbackMessage(result.text, keyboard ? { reply_markup: keyboard } : undefined);
|
|
344
658
|
}
|
|
345
659
|
catch (editErr) {
|
|
346
660
|
const errStr = String(editErr);
|
|
@@ -358,11 +672,18 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
358
672
|
const editMessageWithButtons = async (text, buttons) => {
|
|
359
673
|
const keyboard = buildInlineKeyboard(buttons);
|
|
360
674
|
try {
|
|
361
|
-
await
|
|
675
|
+
await editCallbackMessage(text, keyboard ? { reply_markup: keyboard } : undefined);
|
|
362
676
|
}
|
|
363
677
|
catch (editErr) {
|
|
364
678
|
const errStr = String(editErr);
|
|
365
|
-
if (
|
|
679
|
+
if (errStr.includes("no text in the message")) {
|
|
680
|
+
try {
|
|
681
|
+
await deleteCallbackMessage();
|
|
682
|
+
}
|
|
683
|
+
catch { }
|
|
684
|
+
await replyToCallbackChat(text, keyboard ? { reply_markup: keyboard } : undefined);
|
|
685
|
+
}
|
|
686
|
+
else if (!errStr.includes("message is not modified")) {
|
|
366
687
|
throw editErr;
|
|
367
688
|
}
|
|
368
689
|
}
|
|
@@ -384,6 +705,7 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
384
705
|
const { provider, page } = modelCallback;
|
|
385
706
|
const modelSet = byProvider.get(provider);
|
|
386
707
|
if (!modelSet || modelSet.size === 0) {
|
|
708
|
+
// Provider not found or no models - show providers list
|
|
387
709
|
const providerInfos = providers.map((p) => ({
|
|
388
710
|
id: p,
|
|
389
711
|
count: byProvider.get(p)?.size ?? 0,
|
|
@@ -396,6 +718,7 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
396
718
|
const pageSize = getModelsPageSize();
|
|
397
719
|
const totalPages = calculateTotalPages(models.length, pageSize);
|
|
398
720
|
const safePage = Math.max(1, Math.min(page, totalPages));
|
|
721
|
+
// Resolve current model from session (prefer overrides)
|
|
399
722
|
const currentModel = resolveTelegramSessionModel({
|
|
400
723
|
chatId,
|
|
401
724
|
isGroup,
|
|
@@ -417,16 +740,13 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
417
740
|
}
|
|
418
741
|
if (modelCallback.type === "select") {
|
|
419
742
|
const { provider, model } = modelCallback;
|
|
420
|
-
|
|
421
|
-
|
|
743
|
+
// Process model selection as a synthetic message with /model command
|
|
744
|
+
const syntheticMessage = buildSyntheticTextMessage({
|
|
745
|
+
base: callbackMessage,
|
|
422
746
|
from: callback.from,
|
|
423
747
|
text: `/model ${provider}/${model}`,
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
entities: undefined,
|
|
427
|
-
};
|
|
428
|
-
const getFile = typeof ctx.getFile === "function" ? ctx.getFile.bind(ctx) : async () => ({});
|
|
429
|
-
await processMessage({ message: syntheticMessage, me: ctx.me, getFile }, [], storeAllowFrom, {
|
|
748
|
+
});
|
|
749
|
+
await processMessage(buildSyntheticContext(ctx, syntheticMessage), [], storeAllowFrom, {
|
|
430
750
|
forceWasMentioned: true,
|
|
431
751
|
messageIdOverride: callback.id,
|
|
432
752
|
});
|
|
@@ -434,16 +754,12 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
434
754
|
}
|
|
435
755
|
return;
|
|
436
756
|
}
|
|
437
|
-
const syntheticMessage = {
|
|
438
|
-
|
|
757
|
+
const syntheticMessage = buildSyntheticTextMessage({
|
|
758
|
+
base: callbackMessage,
|
|
439
759
|
from: callback.from,
|
|
440
760
|
text: data,
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
entities: undefined,
|
|
444
|
-
};
|
|
445
|
-
const getFile = typeof ctx.getFile === "function" ? ctx.getFile.bind(ctx) : async () => ({});
|
|
446
|
-
await processMessage({ message: syntheticMessage, me: ctx.me, getFile }, [], storeAllowFrom, {
|
|
761
|
+
});
|
|
762
|
+
await processMessage(buildSyntheticContext(ctx, syntheticMessage), [], storeAllowFrom, {
|
|
447
763
|
forceWasMentioned: true,
|
|
448
764
|
messageIdOverride: callback.id,
|
|
449
765
|
});
|
|
@@ -456,10 +772,12 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
456
772
|
bot.on("message:migrate_to_chat_id", async (ctx) => {
|
|
457
773
|
try {
|
|
458
774
|
const msg = ctx.message;
|
|
459
|
-
if (!msg?.migrate_to_chat_id)
|
|
775
|
+
if (!msg?.migrate_to_chat_id) {
|
|
460
776
|
return;
|
|
461
|
-
|
|
777
|
+
}
|
|
778
|
+
if (shouldSkipUpdate(ctx)) {
|
|
462
779
|
return;
|
|
780
|
+
}
|
|
463
781
|
const oldChatId = String(msg.chat.id);
|
|
464
782
|
const newChatId = String(msg.migrate_to_chat_id);
|
|
465
783
|
const chatTitle = msg.chat.title ?? "Unknown";
|
|
@@ -496,224 +814,167 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
|
|
|
496
814
|
bot.on("message", async (ctx) => {
|
|
497
815
|
try {
|
|
498
816
|
const msg = ctx.message;
|
|
499
|
-
if (!msg)
|
|
817
|
+
if (!msg) {
|
|
500
818
|
return;
|
|
501
|
-
|
|
819
|
+
}
|
|
820
|
+
if (shouldSkipUpdate(ctx)) {
|
|
502
821
|
return;
|
|
822
|
+
}
|
|
503
823
|
const chatId = msg.chat.id;
|
|
504
824
|
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
|
505
825
|
const messageThreadId = msg.message_thread_id;
|
|
506
826
|
const isForum = msg.chat.is_forum === true;
|
|
507
|
-
const
|
|
827
|
+
const groupAllowContext = await resolveTelegramGroupAllowFromContext({
|
|
828
|
+
chatId,
|
|
829
|
+
accountId,
|
|
508
830
|
isForum,
|
|
509
831
|
messageThreadId,
|
|
832
|
+
groupAllowFrom,
|
|
833
|
+
resolveTelegramGroupConfig,
|
|
510
834
|
});
|
|
511
|
-
const storeAllowFrom
|
|
512
|
-
const
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
835
|
+
const { resolvedThreadId, storeAllowFrom, groupConfig, topicConfig, effectiveGroupAllow, hasGroupAllowOverride, } = groupAllowContext;
|
|
836
|
+
const senderId = msg.from?.id != null ? String(msg.from.id) : "";
|
|
837
|
+
const senderUsername = msg.from?.username ?? "";
|
|
838
|
+
if (shouldSkipGroupMessage({
|
|
839
|
+
isGroup,
|
|
840
|
+
chatId,
|
|
841
|
+
chatTitle: msg.chat.title,
|
|
842
|
+
resolvedThreadId,
|
|
843
|
+
senderId,
|
|
844
|
+
senderUsername,
|
|
845
|
+
effectiveGroupAllow,
|
|
846
|
+
hasGroupAllowOverride,
|
|
847
|
+
groupConfig,
|
|
848
|
+
topicConfig,
|
|
849
|
+
})) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
await processInboundMessage({
|
|
853
|
+
ctx,
|
|
854
|
+
msg,
|
|
855
|
+
chatId,
|
|
856
|
+
resolvedThreadId,
|
|
516
857
|
storeAllowFrom,
|
|
858
|
+
sendOversizeWarning: true,
|
|
859
|
+
oversizeLogMessage: "media exceeds size limit",
|
|
517
860
|
});
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
861
|
+
}
|
|
862
|
+
catch (err) {
|
|
863
|
+
runtime.error?.(danger(`handler failed: ${String(err)}`));
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
// Handle channel posts — enables bot-to-bot communication via Telegram channels.
|
|
867
|
+
// Telegram bots cannot see other bot messages in groups, but CAN in channels.
|
|
868
|
+
// This handler normalizes channel_post updates into the standard message pipeline.
|
|
869
|
+
bot.on("channel_post", async (ctx) => {
|
|
870
|
+
try {
|
|
871
|
+
const post = ctx.channelPost;
|
|
872
|
+
if (!post) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
// Deduplication check — same as the regular message handler
|
|
876
|
+
if (shouldSkipUpdate(ctx)) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const chatId = post.chat.id;
|
|
880
|
+
// Use the full group allow-from context for access control (same as message handler)
|
|
881
|
+
const groupAllowContext = await resolveTelegramGroupAllowFromContext({
|
|
882
|
+
chatId,
|
|
883
|
+
accountId,
|
|
884
|
+
isForum: false,
|
|
885
|
+
messageThreadId: undefined,
|
|
886
|
+
groupAllowFrom,
|
|
887
|
+
resolveTelegramGroupConfig,
|
|
888
|
+
});
|
|
889
|
+
const { storeAllowFrom, groupConfig, effectiveGroupAllow, hasGroupAllowOverride } = groupAllowContext;
|
|
890
|
+
// Check group allowlist (channels use the same groups config)
|
|
891
|
+
const groupAllowlist = resolveGroupPolicy(chatId);
|
|
892
|
+
if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (!groupConfig || groupConfig.enabled === false) {
|
|
896
|
+
logVerbose(`Blocked telegram channel ${chatId} (channel disabled)`);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
// Group policy filtering (same as message handler)
|
|
900
|
+
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
|
901
|
+
const groupPolicy = firstDefined(groupConfig?.groupPolicy, telegramCfg.groupPolicy, defaultGroupPolicy, "open");
|
|
902
|
+
if (groupPolicy === "disabled") {
|
|
903
|
+
logVerbose(`Blocked telegram channel message (groupPolicy: disabled)`);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (hasGroupAllowOverride) {
|
|
907
|
+
const senderId = post.sender_chat?.id ?? post.from?.id;
|
|
908
|
+
const senderUsername = post.sender_chat?.username ?? post.from?.username ?? "";
|
|
909
|
+
const allowed = senderId != null &&
|
|
910
|
+
isSenderAllowed({
|
|
565
911
|
allow: effectiveGroupAllow,
|
|
566
912
|
senderId: String(senderId),
|
|
567
913
|
senderUsername,
|
|
568
|
-
})
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
// Group allowlist based on configured group IDs.
|
|
574
|
-
const groupAllowlist = resolveGroupPolicy(chatId);
|
|
575
|
-
if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
|
|
576
|
-
logger.info({ chatId, title: msg.chat.title, reason: "not-allowed" }, "skipping group message");
|
|
914
|
+
});
|
|
915
|
+
if (!allowed) {
|
|
916
|
+
logVerbose(`Blocked telegram channel sender ${senderId ?? "unknown"} (group allowFrom override)`);
|
|
577
917
|
return;
|
|
578
918
|
}
|
|
579
919
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
if (text && !isCommandLike) {
|
|
585
|
-
const nowMs = Date.now();
|
|
586
|
-
const senderId = msg.from?.id != null ? String(msg.from.id) : "unknown";
|
|
587
|
-
const key = `text:${chatId}:${resolvedThreadId ?? "main"}:${senderId}`;
|
|
588
|
-
const existing = textFragmentBuffer.get(key);
|
|
589
|
-
if (existing) {
|
|
590
|
-
const last = existing.messages.at(-1);
|
|
591
|
-
const lastMsgId = last?.msg.message_id;
|
|
592
|
-
const lastReceivedAtMs = last?.receivedAtMs ?? nowMs;
|
|
593
|
-
const idGap = typeof lastMsgId === "number" ? msg.message_id - lastMsgId : Infinity;
|
|
594
|
-
const timeGapMs = nowMs - lastReceivedAtMs;
|
|
595
|
-
const canAppend = idGap > 0 &&
|
|
596
|
-
idGap <= TELEGRAM_TEXT_FRAGMENT_MAX_ID_GAP &&
|
|
597
|
-
timeGapMs >= 0 &&
|
|
598
|
-
timeGapMs <= TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS;
|
|
599
|
-
if (canAppend) {
|
|
600
|
-
const currentTotalChars = existing.messages.reduce((sum, m) => sum + (m.msg.text?.length ?? 0), 0);
|
|
601
|
-
const nextTotalChars = currentTotalChars + text.length;
|
|
602
|
-
if (existing.messages.length + 1 <= TELEGRAM_TEXT_FRAGMENT_MAX_PARTS &&
|
|
603
|
-
nextTotalChars <= TELEGRAM_TEXT_FRAGMENT_MAX_TOTAL_CHARS) {
|
|
604
|
-
existing.messages.push({ msg, ctx, receivedAtMs: nowMs });
|
|
605
|
-
scheduleTextFragmentFlush(existing);
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
// Not appendable (or limits exceeded): flush buffered entry first, then continue normally.
|
|
610
|
-
clearTimeout(existing.timer);
|
|
611
|
-
textFragmentBuffer.delete(key);
|
|
612
|
-
textFragmentProcessing = textFragmentProcessing
|
|
613
|
-
.then(async () => {
|
|
614
|
-
await flushTextFragments(existing);
|
|
615
|
-
})
|
|
616
|
-
.catch(() => undefined);
|
|
617
|
-
await textFragmentProcessing;
|
|
618
|
-
}
|
|
619
|
-
const shouldStart = text.length >= TELEGRAM_TEXT_FRAGMENT_START_THRESHOLD_CHARS;
|
|
620
|
-
if (shouldStart) {
|
|
621
|
-
const entry = {
|
|
622
|
-
key,
|
|
623
|
-
messages: [{ msg, ctx, receivedAtMs: nowMs }],
|
|
624
|
-
timer: setTimeout(() => { }, TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS),
|
|
625
|
-
};
|
|
626
|
-
textFragmentBuffer.set(key, entry);
|
|
627
|
-
scheduleTextFragmentFlush(entry);
|
|
920
|
+
if (groupPolicy === "allowlist") {
|
|
921
|
+
const senderId = post.sender_chat?.id ?? post.from?.id;
|
|
922
|
+
if (senderId == null) {
|
|
923
|
+
logVerbose(`Blocked telegram channel message (no sender ID, groupPolicy: allowlist)`);
|
|
628
924
|
return;
|
|
629
925
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if (mediaGroupId) {
|
|
634
|
-
const existing = mediaGroupBuffer.get(mediaGroupId);
|
|
635
|
-
if (existing) {
|
|
636
|
-
clearTimeout(existing.timer);
|
|
637
|
-
existing.messages.push({ msg, ctx });
|
|
638
|
-
existing.timer = setTimeout(async () => {
|
|
639
|
-
mediaGroupBuffer.delete(mediaGroupId);
|
|
640
|
-
mediaGroupProcessing = mediaGroupProcessing
|
|
641
|
-
.then(async () => {
|
|
642
|
-
await processMediaGroup(existing);
|
|
643
|
-
})
|
|
644
|
-
.catch(() => undefined);
|
|
645
|
-
await mediaGroupProcessing;
|
|
646
|
-
}, MEDIA_GROUP_TIMEOUT_MS);
|
|
647
|
-
}
|
|
648
|
-
else {
|
|
649
|
-
const entry = {
|
|
650
|
-
messages: [{ msg, ctx }],
|
|
651
|
-
timer: setTimeout(async () => {
|
|
652
|
-
mediaGroupBuffer.delete(mediaGroupId);
|
|
653
|
-
mediaGroupProcessing = mediaGroupProcessing
|
|
654
|
-
.then(async () => {
|
|
655
|
-
await processMediaGroup(entry);
|
|
656
|
-
})
|
|
657
|
-
.catch(() => undefined);
|
|
658
|
-
await mediaGroupProcessing;
|
|
659
|
-
}, MEDIA_GROUP_TIMEOUT_MS),
|
|
660
|
-
};
|
|
661
|
-
mediaGroupBuffer.set(mediaGroupId, entry);
|
|
926
|
+
if (!effectiveGroupAllow.hasEntries) {
|
|
927
|
+
logVerbose("Blocked telegram channel message (groupPolicy: allowlist, no allowlist entries)");
|
|
928
|
+
return;
|
|
662
929
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const errMsg = String(mediaErr);
|
|
671
|
-
if (errMsg.includes("exceeds") && errMsg.includes("MB limit")) {
|
|
672
|
-
const limitMb = Math.round(mediaMaxBytes / (1024 * 1024));
|
|
673
|
-
await withTelegramApiErrorLogging({
|
|
674
|
-
operation: "sendMessage",
|
|
675
|
-
runtime,
|
|
676
|
-
fn: () => bot.api.sendMessage(chatId, `⚠️ File too large. Maximum size is ${limitMb}MB.`, {
|
|
677
|
-
reply_to_message_id: msg.message_id,
|
|
678
|
-
}),
|
|
679
|
-
}).catch(() => { });
|
|
680
|
-
logger.warn({ chatId, error: errMsg }, "media exceeds size limit");
|
|
930
|
+
const senderUsername = post.sender_chat?.username ?? post.from?.username ?? "";
|
|
931
|
+
if (!isSenderAllowed({
|
|
932
|
+
allow: effectiveGroupAllow,
|
|
933
|
+
senderId: String(senderId),
|
|
934
|
+
senderUsername,
|
|
935
|
+
})) {
|
|
936
|
+
logVerbose(`Blocked telegram channel message from ${senderId} (groupPolicy: allowlist)`);
|
|
681
937
|
return;
|
|
682
938
|
}
|
|
683
|
-
throw mediaErr;
|
|
684
|
-
}
|
|
685
|
-
// Skip sticker-only messages where the sticker was skipped (animated/video)
|
|
686
|
-
// These have no media and no text content to process.
|
|
687
|
-
const hasText = Boolean((msg.text ?? msg.caption ?? "").trim());
|
|
688
|
-
if (msg.sticker && !media && !hasText) {
|
|
689
|
-
logVerbose("telegram: skipping sticker-only message (unsupported sticker type)");
|
|
690
|
-
return;
|
|
691
939
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
940
|
+
// Build a synthetic `from` field since channel posts may not have one.
|
|
941
|
+
// Use sender_chat (the bot/user that posted) if available.
|
|
942
|
+
const syntheticFrom = post.sender_chat
|
|
943
|
+
? {
|
|
944
|
+
id: post.sender_chat.id,
|
|
945
|
+
is_bot: true,
|
|
946
|
+
first_name: post.sender_chat.title || "Channel",
|
|
947
|
+
username: post.sender_chat.username,
|
|
948
|
+
}
|
|
949
|
+
: {
|
|
950
|
+
id: chatId,
|
|
951
|
+
is_bot: true,
|
|
952
|
+
first_name: post.chat.title || "Channel",
|
|
953
|
+
username: post.chat.username,
|
|
954
|
+
};
|
|
955
|
+
const syntheticMsg = {
|
|
956
|
+
...post,
|
|
957
|
+
from: post.from ?? syntheticFrom,
|
|
958
|
+
chat: {
|
|
959
|
+
...post.chat,
|
|
960
|
+
type: "supergroup",
|
|
961
|
+
},
|
|
962
|
+
};
|
|
963
|
+
const syntheticCtx = Object.create(ctx, {
|
|
964
|
+
message: { value: syntheticMsg, writable: true, enumerable: true },
|
|
965
|
+
});
|
|
966
|
+
await processInboundMessage({
|
|
967
|
+
ctx: syntheticCtx,
|
|
968
|
+
msg: syntheticMsg,
|
|
969
|
+
chatId,
|
|
970
|
+
resolvedThreadId: undefined,
|
|
710
971
|
storeAllowFrom,
|
|
711
|
-
|
|
712
|
-
|
|
972
|
+
sendOversizeWarning: false,
|
|
973
|
+
oversizeLogMessage: "channel post media exceeds size limit",
|
|
713
974
|
});
|
|
714
975
|
}
|
|
715
976
|
catch (err) {
|
|
716
|
-
runtime.error?.(danger(`handler failed: ${String(err)}`));
|
|
977
|
+
runtime.error?.(danger(`channel_post handler failed: ${String(err)}`));
|
|
717
978
|
}
|
|
718
979
|
});
|
|
719
980
|
};
|