@poolzin/pool-bot 2026.2.21 → 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 +17 -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-catalog.js +40 -9
- 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 -95
- 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/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/tmux/SKILL.md +111 -79
- package/skills/weather/SKILL.md +88 -25
package/dist/telegram/bot.js
CHANGED
|
@@ -1,30 +1,25 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import { sequentialize } from "@grammyjs/runner";
|
|
3
2
|
import { apiThrottler } from "@grammyjs/transformer-throttler";
|
|
4
3
|
import { Bot, webhookCallback } from "grammy";
|
|
5
4
|
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
|
6
|
-
import { isControlCommandMessage } from "../auto-reply/command-detection.js";
|
|
7
5
|
import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
|
6
|
+
import { isAbortRequestText } from "../auto-reply/reply/abort.js";
|
|
8
7
|
import { DEFAULT_GROUP_HISTORY_LIMIT } from "../auto-reply/reply/history.js";
|
|
9
8
|
import { isNativeCommandsExplicitlyDisabled, resolveNativeCommandsEnabled, resolveNativeSkillsEnabled, } from "../config/commands.js";
|
|
10
9
|
import { loadConfig } from "../config/config.js";
|
|
11
10
|
import { resolveChannelGroupPolicy, resolveChannelGroupRequireMention, } from "../config/group-policy.js";
|
|
12
11
|
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
|
|
13
12
|
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
|
14
|
-
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
15
13
|
import { formatUncaughtError } from "../infra/errors.js";
|
|
16
|
-
import { enqueueSystemEvent } from "../infra/system-events.js";
|
|
17
14
|
import { getChildLogger } from "../logging.js";
|
|
18
|
-
import {
|
|
19
|
-
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
|
15
|
+
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
20
16
|
import { resolveTelegramAccount } from "./accounts.js";
|
|
21
|
-
import { buildTelegramGroupPeerId, buildTelegramParentPeer, resolveTelegramForumThreadId, resolveTelegramStreamMode, } from "./bot/helpers.js";
|
|
22
17
|
import { registerTelegramHandlers } from "./bot-handlers.js";
|
|
23
18
|
import { createTelegramMessageProcessor } from "./bot-message.js";
|
|
24
19
|
import { registerTelegramNativeCommands } from "./bot-native-commands.js";
|
|
25
20
|
import { buildTelegramUpdateKey, createTelegramUpdateDedupe, resolveTelegramUpdateId, } from "./bot-updates.js";
|
|
21
|
+
import { buildTelegramGroupPeerId, resolveTelegramForumThreadId, resolveTelegramStreamMode, } from "./bot/helpers.js";
|
|
26
22
|
import { resolveTelegramFetch } from "./fetch.js";
|
|
27
|
-
import { wasSentByBot } from "./sent-message-cache.js";
|
|
28
23
|
export function getTelegramSequentialKey(ctx) {
|
|
29
24
|
// Handle reaction updates
|
|
30
25
|
const reaction = ctx.update?.message_reaction;
|
|
@@ -38,10 +33,10 @@ export function getTelegramSequentialKey(ctx) {
|
|
|
38
33
|
const chatId = msg?.chat?.id ?? ctx.chat?.id;
|
|
39
34
|
const rawText = msg?.text ?? msg?.caption;
|
|
40
35
|
const botUsername = ctx.me?.username;
|
|
41
|
-
if (rawText
|
|
42
|
-
|
|
43
|
-
if (typeof chatId === "number")
|
|
36
|
+
if (isAbortRequestText(rawText, botUsername ? { botUsername } : undefined)) {
|
|
37
|
+
if (typeof chatId === "number") {
|
|
44
38
|
return `telegram:${chatId}:control`;
|
|
39
|
+
}
|
|
45
40
|
return "telegram:control";
|
|
46
41
|
}
|
|
47
42
|
const isGroup = msg?.chat?.type === "group" || msg?.chat?.type === "supergroup";
|
|
@@ -88,30 +83,29 @@ export function createTelegramBot(opts) {
|
|
|
88
83
|
const bot = new Bot(opts.token, client ? { client } : undefined);
|
|
89
84
|
bot.api.config.use(apiThrottler());
|
|
90
85
|
bot.use(sequentialize(getTelegramSequentialKey));
|
|
91
|
-
bot.catch((err) => {
|
|
92
|
-
runtime.error?.(danger(`telegram bot error: ${formatUncaughtError(err)}`));
|
|
93
|
-
});
|
|
94
86
|
// Catch all errors from bot middleware to prevent unhandled rejections
|
|
95
87
|
bot.catch((err) => {
|
|
96
|
-
|
|
97
|
-
runtime.error?.(danger(`telegram bot error: ${message}`));
|
|
88
|
+
runtime.error?.(danger(`telegram bot error: ${formatUncaughtError(err)}`));
|
|
98
89
|
});
|
|
99
90
|
const recentUpdates = createTelegramUpdateDedupe();
|
|
100
91
|
let lastUpdateId = typeof opts.updateOffset?.lastUpdateId === "number" ? opts.updateOffset.lastUpdateId : null;
|
|
101
92
|
const recordUpdateId = (ctx) => {
|
|
102
93
|
const updateId = resolveTelegramUpdateId(ctx);
|
|
103
|
-
if (typeof updateId !== "number")
|
|
94
|
+
if (typeof updateId !== "number") {
|
|
104
95
|
return;
|
|
105
|
-
|
|
96
|
+
}
|
|
97
|
+
if (lastUpdateId !== null && updateId <= lastUpdateId) {
|
|
106
98
|
return;
|
|
99
|
+
}
|
|
107
100
|
lastUpdateId = updateId;
|
|
108
101
|
void opts.updateOffset?.onUpdateId?.(updateId);
|
|
109
102
|
};
|
|
110
103
|
const shouldSkipUpdate = (ctx) => {
|
|
111
104
|
const updateId = resolveTelegramUpdateId(ctx);
|
|
112
105
|
if (typeof updateId === "number" && lastUpdateId !== null) {
|
|
113
|
-
if (updateId <= lastUpdateId)
|
|
106
|
+
if (updateId <= lastUpdateId) {
|
|
114
107
|
return true;
|
|
108
|
+
}
|
|
115
109
|
}
|
|
116
110
|
const key = buildTelegramUpdateKey(ctx);
|
|
117
111
|
const skipped = recentUpdates.check(key);
|
|
@@ -137,10 +131,10 @@ export function createTelegramBot(opts) {
|
|
|
137
131
|
];
|
|
138
132
|
}
|
|
139
133
|
if (value && typeof value === "object") {
|
|
140
|
-
|
|
141
|
-
if (seen.has(obj))
|
|
134
|
+
if (seen.has(value)) {
|
|
142
135
|
return "[Circular]";
|
|
143
|
-
|
|
136
|
+
}
|
|
137
|
+
seen.add(value);
|
|
144
138
|
}
|
|
145
139
|
return value;
|
|
146
140
|
});
|
|
@@ -172,8 +166,7 @@ export function createTelegramBot(opts) {
|
|
|
172
166
|
? telegramCfg.allowFrom
|
|
173
167
|
: undefined) ??
|
|
174
168
|
(opts.allowFrom && opts.allowFrom.length > 0 ? opts.allowFrom : undefined);
|
|
175
|
-
const replyToMode = opts.replyToMode ?? telegramCfg.replyToMode ?? "
|
|
176
|
-
const streamMode = resolveTelegramStreamMode(telegramCfg);
|
|
169
|
+
const replyToMode = opts.replyToMode ?? telegramCfg.replyToMode ?? "off";
|
|
177
170
|
const nativeEnabled = resolveNativeCommandsEnabled({
|
|
178
171
|
providerId: "telegram",
|
|
179
172
|
providerSetting: telegramCfg.commands?.native,
|
|
@@ -192,32 +185,7 @@ export function createTelegramBot(opts) {
|
|
|
192
185
|
const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
|
193
186
|
const mediaMaxBytes = (opts.mediaMaxMb ?? telegramCfg.mediaMaxMb ?? 5) * 1024 * 1024;
|
|
194
187
|
const logger = getChildLogger({ module: "telegram-auto-reply" });
|
|
195
|
-
|
|
196
|
-
const resolveBotTopicsEnabled = async (ctx) => {
|
|
197
|
-
if (typeof ctx?.me?.has_topics_enabled === "boolean") {
|
|
198
|
-
botHasTopicsEnabled = ctx.me.has_topics_enabled;
|
|
199
|
-
return botHasTopicsEnabled;
|
|
200
|
-
}
|
|
201
|
-
if (typeof botHasTopicsEnabled === "boolean")
|
|
202
|
-
return botHasTopicsEnabled;
|
|
203
|
-
if (typeof bot.api.getMe !== "function") {
|
|
204
|
-
botHasTopicsEnabled = false;
|
|
205
|
-
return botHasTopicsEnabled;
|
|
206
|
-
}
|
|
207
|
-
try {
|
|
208
|
-
const me = await withTelegramApiErrorLogging({
|
|
209
|
-
operation: "getMe",
|
|
210
|
-
runtime,
|
|
211
|
-
fn: () => bot.api.getMe(),
|
|
212
|
-
});
|
|
213
|
-
botHasTopicsEnabled = Boolean(me?.has_topics_enabled);
|
|
214
|
-
}
|
|
215
|
-
catch (err) {
|
|
216
|
-
logVerbose(`telegram getMe failed: ${String(err)}`);
|
|
217
|
-
botHasTopicsEnabled = false;
|
|
218
|
-
}
|
|
219
|
-
return botHasTopicsEnabled;
|
|
220
|
-
};
|
|
188
|
+
const streamMode = resolveTelegramStreamMode(telegramCfg);
|
|
221
189
|
const resolveGroupPolicy = (chatId) => resolveChannelGroupPolicy({
|
|
222
190
|
cfg,
|
|
223
191
|
channel: "telegram",
|
|
@@ -232,10 +200,12 @@ export function createTelegramBot(opts) {
|
|
|
232
200
|
try {
|
|
233
201
|
const store = loadSessionStore(storePath);
|
|
234
202
|
const entry = store[sessionKey];
|
|
235
|
-
if (entry?.groupActivation === "always")
|
|
203
|
+
if (entry?.groupActivation === "always") {
|
|
236
204
|
return false;
|
|
237
|
-
|
|
205
|
+
}
|
|
206
|
+
if (entry?.groupActivation === "mention") {
|
|
238
207
|
return true;
|
|
208
|
+
}
|
|
239
209
|
}
|
|
240
210
|
catch (err) {
|
|
241
211
|
logVerbose(`Failed to load session for activation check: ${String(err)}`);
|
|
@@ -252,8 +222,9 @@ export function createTelegramBot(opts) {
|
|
|
252
222
|
});
|
|
253
223
|
const resolveTelegramGroupConfig = (chatId, messageThreadId) => {
|
|
254
224
|
const groups = telegramCfg.groups;
|
|
255
|
-
if (!groups)
|
|
225
|
+
if (!groups) {
|
|
256
226
|
return { groupConfig: undefined, topicConfig: undefined };
|
|
227
|
+
}
|
|
257
228
|
const groupKey = String(chatId);
|
|
258
229
|
const groupConfig = groups[groupKey] ?? groups["*"];
|
|
259
230
|
const topicConfig = messageThreadId != null ? groupConfig?.topics?.[String(messageThreadId)] : undefined;
|
|
@@ -279,7 +250,6 @@ export function createTelegramBot(opts) {
|
|
|
279
250
|
streamMode,
|
|
280
251
|
textLimit,
|
|
281
252
|
opts,
|
|
282
|
-
resolveBotTopicsEnabled,
|
|
283
253
|
});
|
|
284
254
|
registerTelegramNativeCommands({
|
|
285
255
|
bot,
|
|
@@ -300,84 +270,6 @@ export function createTelegramBot(opts) {
|
|
|
300
270
|
shouldSkipUpdate,
|
|
301
271
|
opts,
|
|
302
272
|
});
|
|
303
|
-
// Handle emoji reactions to messages
|
|
304
|
-
bot.on("message_reaction", async (ctx) => {
|
|
305
|
-
try {
|
|
306
|
-
const reaction = ctx.messageReaction;
|
|
307
|
-
if (!reaction)
|
|
308
|
-
return;
|
|
309
|
-
if (shouldSkipUpdate(ctx))
|
|
310
|
-
return;
|
|
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
|
-
if (user?.is_bot)
|
|
319
|
-
return;
|
|
320
|
-
if (reactionMode === "own" && !wasSentByBot(chatId, messageId))
|
|
321
|
-
return;
|
|
322
|
-
// Detect added reactions
|
|
323
|
-
const oldEmojis = new Set(reaction.old_reaction
|
|
324
|
-
.filter((r) => r.type === "emoji")
|
|
325
|
-
.map((r) => r.emoji));
|
|
326
|
-
const addedReactions = reaction.new_reaction
|
|
327
|
-
.filter((r) => r.type === "emoji")
|
|
328
|
-
.filter((r) => !oldEmojis.has(r.emoji));
|
|
329
|
-
if (addedReactions.length === 0)
|
|
330
|
-
return;
|
|
331
|
-
// Build sender label
|
|
332
|
-
const senderName = user
|
|
333
|
-
? [user.first_name, user.last_name].filter(Boolean).join(" ").trim() || user.username
|
|
334
|
-
: undefined;
|
|
335
|
-
const senderUsername = user?.username ? `@${user.username}` : undefined;
|
|
336
|
-
let senderLabel = senderName;
|
|
337
|
-
if (senderName && senderUsername) {
|
|
338
|
-
senderLabel = `${senderName} (${senderUsername})`;
|
|
339
|
-
}
|
|
340
|
-
else if (!senderName && senderUsername) {
|
|
341
|
-
senderLabel = senderUsername;
|
|
342
|
-
}
|
|
343
|
-
if (!senderLabel && user?.id) {
|
|
344
|
-
senderLabel = `id:${user.id}`;
|
|
345
|
-
}
|
|
346
|
-
senderLabel = senderLabel || "unknown";
|
|
347
|
-
// Reactions target a specific message_id; the Telegram Bot API does not include
|
|
348
|
-
// message_thread_id on MessageReactionUpdated, so we route to the chat-level
|
|
349
|
-
// session (forum topic routing is not available for reactions).
|
|
350
|
-
const isGroup = reaction.chat.type === "group" || reaction.chat.type === "supergroup";
|
|
351
|
-
const isForum = reaction.chat.is_forum === true;
|
|
352
|
-
const resolvedThreadId = isForum
|
|
353
|
-
? resolveTelegramForumThreadId({ isForum, messageThreadId: undefined })
|
|
354
|
-
: undefined;
|
|
355
|
-
const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
|
|
356
|
-
const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId });
|
|
357
|
-
// Fresh config for bindings lookup; other routing inputs are payload-derived.
|
|
358
|
-
const route = resolveAgentRoute({
|
|
359
|
-
cfg: loadConfig(),
|
|
360
|
-
channel: "telegram",
|
|
361
|
-
accountId: account.accountId,
|
|
362
|
-
peer: { kind: isGroup ? "group" : "dm", id: peerId },
|
|
363
|
-
parentPeer,
|
|
364
|
-
});
|
|
365
|
-
const sessionKey = route.sessionKey;
|
|
366
|
-
// Enqueue system event for each added reaction
|
|
367
|
-
for (const r of addedReactions) {
|
|
368
|
-
const emoji = r.emoji;
|
|
369
|
-
const text = `Telegram reaction added: ${emoji} by ${senderLabel} on msg ${messageId}`;
|
|
370
|
-
enqueueSystemEvent(text, {
|
|
371
|
-
sessionKey: sessionKey,
|
|
372
|
-
contextKey: `telegram:reaction:add:${chatId}:${messageId}:${user?.id ?? "anon"}:${emoji}`,
|
|
373
|
-
});
|
|
374
|
-
logVerbose(`telegram: reaction event enqueued: ${text}`);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
catch (err) {
|
|
378
|
-
runtime.error?.(danger(`telegram reaction handler failed: ${String(err)}`));
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
273
|
registerTelegramHandlers({
|
|
382
274
|
cfg,
|
|
383
275
|
accountId: account.accountId,
|
|
@@ -1,101 +1,110 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { createDraftStreamLoop } from "../channels/draft-stream-loop.js";
|
|
2
|
+
import { buildTelegramThreadParams } from "./bot/helpers.js";
|
|
3
|
+
const TELEGRAM_STREAM_MAX_CHARS = 4096;
|
|
4
|
+
const DEFAULT_THROTTLE_MS = 1000;
|
|
3
5
|
export function createTelegramDraftStream(params) {
|
|
4
|
-
const maxChars = Math.min(params.maxChars ??
|
|
5
|
-
const throttleMs = Math.max(
|
|
6
|
-
const
|
|
7
|
-
const draftId = rawDraftId === 0 ? 1 : Math.abs(rawDraftId);
|
|
6
|
+
const maxChars = Math.min(params.maxChars ?? TELEGRAM_STREAM_MAX_CHARS, TELEGRAM_STREAM_MAX_CHARS);
|
|
7
|
+
const throttleMs = Math.max(250, params.throttleMs ?? DEFAULT_THROTTLE_MS);
|
|
8
|
+
const minInitialChars = params.minInitialChars;
|
|
8
9
|
const chatId = params.chatId;
|
|
9
|
-
const threadParams =
|
|
10
|
-
|
|
11
|
-
:
|
|
10
|
+
const threadParams = buildTelegramThreadParams(params.thread);
|
|
11
|
+
const replyParams = params.replyToMessageId != null
|
|
12
|
+
? { ...threadParams, reply_to_message_id: params.replyToMessageId }
|
|
13
|
+
: threadParams;
|
|
14
|
+
let streamMessageId;
|
|
12
15
|
let lastSentText = "";
|
|
13
|
-
let lastSentAt = 0;
|
|
14
|
-
let pendingText = "";
|
|
15
|
-
let inFlight = false;
|
|
16
|
-
let timer;
|
|
17
16
|
let stopped = false;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
let isFinal = false;
|
|
18
|
+
const sendOrEditStreamMessage = async (text) => {
|
|
19
|
+
// Allow final flush even if stopped (e.g., after clear()).
|
|
20
|
+
if (stopped && !isFinal) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
21
23
|
const trimmed = text.trimEnd();
|
|
22
|
-
if (!trimmed)
|
|
23
|
-
return;
|
|
24
|
+
if (!trimmed) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
24
27
|
if (trimmed.length > maxChars) {
|
|
25
|
-
//
|
|
26
|
-
//
|
|
28
|
+
// Telegram text messages/edits cap at 4096 chars.
|
|
29
|
+
// Stop streaming once we exceed the cap to avoid repeated API failures.
|
|
27
30
|
stopped = true;
|
|
28
|
-
params.warn?.(`telegram
|
|
29
|
-
return;
|
|
31
|
+
params.warn?.(`telegram stream preview stopped (text length ${trimmed.length} > ${maxChars})`);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (trimmed === lastSentText) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
// Debounce first preview send for better push notification quality.
|
|
38
|
+
if (typeof streamMessageId !== "number" && minInitialChars != null && !isFinal) {
|
|
39
|
+
if (trimmed.length < minInitialChars) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
30
42
|
}
|
|
31
|
-
if (trimmed === lastSentText)
|
|
32
|
-
return;
|
|
33
43
|
lastSentText = trimmed;
|
|
34
|
-
lastSentAt = Date.now();
|
|
35
44
|
try {
|
|
36
|
-
|
|
45
|
+
if (typeof streamMessageId === "number") {
|
|
46
|
+
await params.api.editMessageText(chatId, streamMessageId, trimmed);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const sent = await params.api.sendMessage(chatId, trimmed, replyParams);
|
|
50
|
+
const sentMessageId = sent?.message_id;
|
|
51
|
+
if (typeof sentMessageId !== "number" || !Number.isFinite(sentMessageId)) {
|
|
52
|
+
stopped = true;
|
|
53
|
+
params.warn?.("telegram stream preview stopped (missing message id from sendMessage)");
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
streamMessageId = Math.trunc(sentMessageId);
|
|
57
|
+
return true;
|
|
37
58
|
}
|
|
38
59
|
catch (err) {
|
|
39
60
|
stopped = true;
|
|
40
|
-
params.warn?.(`telegram
|
|
61
|
+
params.warn?.(`telegram stream preview failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
|
+
return false;
|
|
41
63
|
}
|
|
42
64
|
};
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
const loop = createDraftStreamLoop({
|
|
66
|
+
throttleMs,
|
|
67
|
+
isStopped: () => stopped,
|
|
68
|
+
sendOrEditStreamMessage,
|
|
69
|
+
});
|
|
70
|
+
const update = (text) => {
|
|
71
|
+
if (stopped || isFinal) {
|
|
50
72
|
return;
|
|
51
73
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
loop.update(text);
|
|
75
|
+
};
|
|
76
|
+
const stop = async () => {
|
|
77
|
+
isFinal = true;
|
|
78
|
+
await loop.flush();
|
|
79
|
+
};
|
|
80
|
+
const clear = async () => {
|
|
81
|
+
stopped = true;
|
|
82
|
+
loop.stop();
|
|
83
|
+
await loop.waitForInFlight();
|
|
84
|
+
const messageId = streamMessageId;
|
|
85
|
+
streamMessageId = undefined;
|
|
86
|
+
if (typeof messageId !== "number") {
|
|
57
87
|
return;
|
|
58
88
|
}
|
|
59
|
-
inFlight = true;
|
|
60
89
|
try {
|
|
61
|
-
await
|
|
90
|
+
await params.api.deleteMessage(chatId, messageId);
|
|
62
91
|
}
|
|
63
|
-
|
|
64
|
-
|
|
92
|
+
catch (err) {
|
|
93
|
+
params.warn?.(`telegram stream preview cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
65
94
|
}
|
|
66
|
-
if (pendingText)
|
|
67
|
-
schedule();
|
|
68
|
-
};
|
|
69
|
-
const schedule = () => {
|
|
70
|
-
if (timer)
|
|
71
|
-
return;
|
|
72
|
-
const delay = Math.max(0, throttleMs - (Date.now() - lastSentAt));
|
|
73
|
-
timer = setTimeout(() => {
|
|
74
|
-
void flush();
|
|
75
|
-
}, delay);
|
|
76
95
|
};
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (inFlight) {
|
|
82
|
-
schedule();
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
if (!timer && Date.now() - lastSentAt >= throttleMs) {
|
|
86
|
-
void flush();
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
schedule();
|
|
96
|
+
const forceNewMessage = () => {
|
|
97
|
+
streamMessageId = undefined;
|
|
98
|
+
lastSentText = "";
|
|
99
|
+
loop.resetPending();
|
|
90
100
|
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
params.log?.(`telegram stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`);
|
|
102
|
+
return {
|
|
103
|
+
update,
|
|
104
|
+
flush: loop.flush,
|
|
105
|
+
messageId: () => streamMessageId,
|
|
106
|
+
clear,
|
|
107
|
+
stop,
|
|
108
|
+
forceNewMessage,
|
|
98
109
|
};
|
|
99
|
-
params.log?.(`telegram draft stream ready (draftId=${draftId}, maxChars=${maxChars}, throttleMs=${throttleMs})`);
|
|
100
|
-
return { update, flush, stop };
|
|
101
110
|
}
|
package/dist/telegram/format.js
CHANGED
|
@@ -6,12 +6,66 @@ function escapeHtml(text) {
|
|
|
6
6
|
function escapeHtmlAttr(text) {
|
|
7
7
|
return escapeHtml(text).replace(/"/g, """);
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* File extensions that share TLDs and commonly appear in code/documentation.
|
|
11
|
+
* These are wrapped in <code> tags to prevent Telegram from generating
|
|
12
|
+
* spurious domain registrar previews.
|
|
13
|
+
*
|
|
14
|
+
* Only includes extensions that are:
|
|
15
|
+
* 1. Commonly used as file extensions in code/docs
|
|
16
|
+
* 2. Rarely used as intentional domain references
|
|
17
|
+
*
|
|
18
|
+
* Excluded: .ai, .io, .tv, .fm (popular domain TLDs like x.ai, vercel.io, github.io)
|
|
19
|
+
*/
|
|
20
|
+
const FILE_EXTENSIONS_WITH_TLD = new Set([
|
|
21
|
+
"md", // Markdown (Moldova) - very common in repos
|
|
22
|
+
"go", // Go language - common in Go projects
|
|
23
|
+
"py", // Python (Paraguay) - common in Python projects
|
|
24
|
+
"pl", // Perl (Poland) - common in Perl projects
|
|
25
|
+
"sh", // Shell (Saint Helena) - common for scripts
|
|
26
|
+
"am", // Automake files (Armenia)
|
|
27
|
+
"at", // Assembly (Austria)
|
|
28
|
+
"be", // Backend files (Belgium)
|
|
29
|
+
"cc", // C++ source (Cocos Islands)
|
|
30
|
+
]);
|
|
31
|
+
/** Detects when markdown-it linkify auto-generated a link from a bare filename (e.g. README.md → http://README.md) */
|
|
32
|
+
function isAutoLinkedFileRef(href, label) {
|
|
33
|
+
const stripped = href.replace(/^https?:\/\//i, "");
|
|
34
|
+
if (stripped !== label) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const dotIndex = label.lastIndexOf(".");
|
|
38
|
+
if (dotIndex < 1) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const ext = label.slice(dotIndex + 1).toLowerCase();
|
|
42
|
+
if (!FILE_EXTENSIONS_WITH_TLD.has(ext)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
// Reject if any path segment before the filename contains a dot (looks like a domain)
|
|
46
|
+
const segments = label.split("/");
|
|
47
|
+
if (segments.length > 1) {
|
|
48
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
49
|
+
if (segments[i].includes(".")) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
function buildTelegramLink(link, text) {
|
|
10
57
|
const href = link.href.trim();
|
|
11
|
-
if (!href)
|
|
58
|
+
if (!href) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (link.start === link.end) {
|
|
12
62
|
return null;
|
|
13
|
-
|
|
63
|
+
}
|
|
64
|
+
// Suppress auto-linkified file references (e.g. README.md → http://README.md)
|
|
65
|
+
const label = text.slice(link.start, link.end);
|
|
66
|
+
if (isAutoLinkedFileRef(href, label)) {
|
|
14
67
|
return null;
|
|
68
|
+
}
|
|
15
69
|
const safeHref = escapeHtmlAttr(href);
|
|
16
70
|
return {
|
|
17
71
|
start: link.start,
|
|
@@ -28,6 +82,8 @@ function renderTelegramHtml(ir) {
|
|
|
28
82
|
strikethrough: { open: "<s>", close: "</s>" },
|
|
29
83
|
code: { open: "<code>", close: "</code>" },
|
|
30
84
|
code_block: { open: "<pre><code>", close: "</code></pre>" },
|
|
85
|
+
spoiler: { open: "<tg-spoiler>", close: "</tg-spoiler>" },
|
|
86
|
+
blockquote: { open: "<blockquote>", close: "</blockquote>" },
|
|
31
87
|
},
|
|
32
88
|
escapeText: escapeHtml,
|
|
33
89
|
buildLink: buildTelegramLink,
|
|
@@ -36,28 +92,116 @@ function renderTelegramHtml(ir) {
|
|
|
36
92
|
export function markdownToTelegramHtml(markdown, options = {}) {
|
|
37
93
|
const ir = markdownToIR(markdown ?? "", {
|
|
38
94
|
linkify: true,
|
|
95
|
+
enableSpoilers: true,
|
|
39
96
|
headingStyle: "none",
|
|
40
97
|
blockquotePrefix: "",
|
|
41
98
|
tableMode: options.tableMode,
|
|
42
99
|
});
|
|
43
|
-
|
|
100
|
+
const html = renderTelegramHtml(ir);
|
|
101
|
+
// Apply file reference wrapping if requested (for chunked rendering)
|
|
102
|
+
if (options.wrapFileRefs !== false) {
|
|
103
|
+
return wrapFileReferencesInHtml(html);
|
|
104
|
+
}
|
|
105
|
+
return html;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Wraps standalone file references (with TLD extensions) in <code> tags.
|
|
109
|
+
* This prevents Telegram from treating them as URLs and generating
|
|
110
|
+
* irrelevant domain registrar previews.
|
|
111
|
+
*
|
|
112
|
+
* Runs AFTER markdown→HTML conversion to avoid modifying HTML attributes.
|
|
113
|
+
* Skips content inside <code>, <pre>, and <a> tags to avoid nesting issues.
|
|
114
|
+
*/
|
|
115
|
+
/** Escape regex metacharacters in a string */
|
|
116
|
+
function escapeRegex(str) {
|
|
117
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
118
|
+
}
|
|
119
|
+
const FILE_EXTENSIONS_PATTERN = Array.from(FILE_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
|
|
120
|
+
const AUTO_LINKED_ANCHOR_PATTERN = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi;
|
|
121
|
+
const FILE_REFERENCE_PATTERN = new RegExp(`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=$|[^a-zA-Z0-9_\\-/])`, "gi");
|
|
122
|
+
const ORPHANED_TLD_PATTERN = new RegExp(`([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=[^a-zA-Z0-9/]|$)`, "g");
|
|
123
|
+
const HTML_TAG_PATTERN = /(<\/?)([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?>/gi;
|
|
124
|
+
function wrapStandaloneFileRef(match, prefix, filename) {
|
|
125
|
+
if (filename.startsWith("//")) {
|
|
126
|
+
return match;
|
|
127
|
+
}
|
|
128
|
+
if (/https?:\/\/$/i.test(prefix)) {
|
|
129
|
+
return match;
|
|
130
|
+
}
|
|
131
|
+
return `${prefix}<code>${escapeHtml(filename)}</code>`;
|
|
132
|
+
}
|
|
133
|
+
function wrapSegmentFileRefs(text, codeDepth, preDepth, anchorDepth) {
|
|
134
|
+
if (!text || codeDepth > 0 || preDepth > 0 || anchorDepth > 0) {
|
|
135
|
+
return text;
|
|
136
|
+
}
|
|
137
|
+
const wrappedStandalone = text.replace(FILE_REFERENCE_PATTERN, wrapStandaloneFileRef);
|
|
138
|
+
return wrappedStandalone.replace(ORPHANED_TLD_PATTERN, (match, prefix, tld) => prefix === ">" ? match : `${prefix}<code>${escapeHtml(tld)}</code>`);
|
|
139
|
+
}
|
|
140
|
+
export function wrapFileReferencesInHtml(html) {
|
|
141
|
+
// Safety-net: de-linkify auto-generated anchors where href="http://<label>" (defense in depth for textMode: "html")
|
|
142
|
+
AUTO_LINKED_ANCHOR_PATTERN.lastIndex = 0;
|
|
143
|
+
const deLinkified = html.replace(AUTO_LINKED_ANCHOR_PATTERN, (_match, label) => {
|
|
144
|
+
if (!isAutoLinkedFileRef(`http://${label}`, label)) {
|
|
145
|
+
return _match;
|
|
146
|
+
}
|
|
147
|
+
return `<code>${escapeHtml(label)}</code>`;
|
|
148
|
+
});
|
|
149
|
+
// Track nesting depth for tags that should not be modified
|
|
150
|
+
let codeDepth = 0;
|
|
151
|
+
let preDepth = 0;
|
|
152
|
+
let anchorDepth = 0;
|
|
153
|
+
let result = "";
|
|
154
|
+
let lastIndex = 0;
|
|
155
|
+
// Process tags token-by-token so we can skip protected regions while wrapping plain text.
|
|
156
|
+
HTML_TAG_PATTERN.lastIndex = 0;
|
|
157
|
+
let match;
|
|
158
|
+
while ((match = HTML_TAG_PATTERN.exec(deLinkified)) !== null) {
|
|
159
|
+
const tagStart = match.index;
|
|
160
|
+
const tagEnd = HTML_TAG_PATTERN.lastIndex;
|
|
161
|
+
const isClosing = match[1] === "</";
|
|
162
|
+
const tagName = match[2].toLowerCase();
|
|
163
|
+
// Process text before this tag
|
|
164
|
+
const textBefore = deLinkified.slice(lastIndex, tagStart);
|
|
165
|
+
result += wrapSegmentFileRefs(textBefore, codeDepth, preDepth, anchorDepth);
|
|
166
|
+
// Update tag depth (clamp at 0 for malformed HTML with stray closing tags)
|
|
167
|
+
if (tagName === "code") {
|
|
168
|
+
codeDepth = isClosing ? Math.max(0, codeDepth - 1) : codeDepth + 1;
|
|
169
|
+
}
|
|
170
|
+
else if (tagName === "pre") {
|
|
171
|
+
preDepth = isClosing ? Math.max(0, preDepth - 1) : preDepth + 1;
|
|
172
|
+
}
|
|
173
|
+
else if (tagName === "a") {
|
|
174
|
+
anchorDepth = isClosing ? Math.max(0, anchorDepth - 1) : anchorDepth + 1;
|
|
175
|
+
}
|
|
176
|
+
// Add the tag itself
|
|
177
|
+
result += deLinkified.slice(tagStart, tagEnd);
|
|
178
|
+
lastIndex = tagEnd;
|
|
179
|
+
}
|
|
180
|
+
// Process remaining text
|
|
181
|
+
const remainingText = deLinkified.slice(lastIndex);
|
|
182
|
+
result += wrapSegmentFileRefs(remainingText, codeDepth, preDepth, anchorDepth);
|
|
183
|
+
return result;
|
|
44
184
|
}
|
|
45
185
|
export function renderTelegramHtmlText(text, options = {}) {
|
|
46
186
|
const textMode = options.textMode ?? "markdown";
|
|
47
|
-
if (textMode === "html")
|
|
187
|
+
if (textMode === "html") {
|
|
188
|
+
// For HTML mode, trust caller markup - don't modify
|
|
48
189
|
return text;
|
|
190
|
+
}
|
|
191
|
+
// markdownToTelegramHtml already wraps file references by default
|
|
49
192
|
return markdownToTelegramHtml(text, { tableMode: options.tableMode });
|
|
50
193
|
}
|
|
51
194
|
export function markdownToTelegramChunks(markdown, limit, options = {}) {
|
|
52
195
|
const ir = markdownToIR(markdown ?? "", {
|
|
53
196
|
linkify: true,
|
|
197
|
+
enableSpoilers: true,
|
|
54
198
|
headingStyle: "none",
|
|
55
199
|
blockquotePrefix: "",
|
|
56
200
|
tableMode: options.tableMode,
|
|
57
201
|
});
|
|
58
202
|
const chunks = chunkMarkdownIR(ir, limit);
|
|
59
203
|
return chunks.map((chunk) => ({
|
|
60
|
-
html: renderTelegramHtml(chunk),
|
|
204
|
+
html: wrapFileReferencesInHtml(renderTelegramHtml(chunk)),
|
|
61
205
|
text: chunk.text,
|
|
62
206
|
}));
|
|
63
207
|
}
|