mixdog 0.9.1 → 0.9.3
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/package.json +9 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +355 -22
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +80 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +75 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { mkdirSync } from "fs";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { chunk } from "../lib/format.mjs";
|
|
4
|
+
import { readSection } from "../../shared/config.mjs";
|
|
5
|
+
import { toMarkdownV2, stripMdV2, isParseEntitiesError } from "../lib/telegram-format.mjs";
|
|
6
|
+
|
|
7
|
+
const MAX_TELEGRAM_MESSAGE = 4096;
|
|
8
|
+
const API_BASE = "https://api.telegram.org";
|
|
9
|
+
|
|
10
|
+
// Chunk raw text so that EACH chunk still fits Telegram's limit AFTER
|
|
11
|
+
// MarkdownV2 escaping. We first chunk normally (code-fence aware), then for any
|
|
12
|
+
// chunk whose converted (escaped) form exceeds `limit`, we split that raw chunk
|
|
13
|
+
// into smaller raw pieces until every piece converts to <= limit. Splitting the
|
|
14
|
+
// RAW text (not the converted text) keeps each piece a self-contained
|
|
15
|
+
// MarkdownV2 conversion (no entity/escape spans a piece boundary). A hard
|
|
16
|
+
// character-count floor prevents infinite recursion on pathological input.
|
|
17
|
+
const MDV2_MIN_RAW_SLICE = 256;
|
|
18
|
+
function chunkForMarkdownV2(text, limit) {
|
|
19
|
+
const raw = chunk(text, limit);
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const piece of raw) {
|
|
22
|
+
out.push(...splitRawUntilConvertedFits(piece, limit));
|
|
23
|
+
}
|
|
24
|
+
return out.length > 0 ? out : [""];
|
|
25
|
+
}
|
|
26
|
+
function splitRawUntilConvertedFits(piece, limit) {
|
|
27
|
+
if (!piece) return [piece];
|
|
28
|
+
if (toMarkdownV2(piece).length <= limit || piece.length <= MDV2_MIN_RAW_SLICE) {
|
|
29
|
+
return [piece];
|
|
30
|
+
}
|
|
31
|
+
// Prefer a newline boundary near the midpoint so we don't cut mid-line;
|
|
32
|
+
// fall back to the raw midpoint. Re-chunk each half through chunk() first so
|
|
33
|
+
// code fences stay balanced, then recurse for the escape-size guarantee.
|
|
34
|
+
const mid = Math.floor(piece.length / 2);
|
|
35
|
+
const nl = piece.lastIndexOf("\n", mid);
|
|
36
|
+
const cut = nl > MDV2_MIN_RAW_SLICE ? nl + 1 : mid;
|
|
37
|
+
const left = piece.slice(0, cut);
|
|
38
|
+
const right = piece.slice(cut);
|
|
39
|
+
const result = [];
|
|
40
|
+
for (const half of [left, right]) {
|
|
41
|
+
for (const sub of chunk(half, limit)) {
|
|
42
|
+
result.push(...splitRawUntilConvertedFits(sub, limit));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function defaultAccess() {
|
|
49
|
+
return {
|
|
50
|
+
dmPolicy: "allowlist",
|
|
51
|
+
allowFrom: [],
|
|
52
|
+
channels: {}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeAccess(parsed) {
|
|
57
|
+
const defaults = defaultAccess();
|
|
58
|
+
return {
|
|
59
|
+
dmPolicy: parsed?.dmPolicy === "pairing" ? "allowlist" : (parsed?.dmPolicy ?? defaults.dmPolicy),
|
|
60
|
+
allowFrom: parsed?.allowFrom ?? defaults.allowFrom,
|
|
61
|
+
channels: parsed?.channels ?? defaults.channels,
|
|
62
|
+
mentionPatterns: parsed?.mentionPatterns,
|
|
63
|
+
ackReaction: parsed?.ackReaction === true
|
|
64
|
+
? "\uD83D\uDC4D"
|
|
65
|
+
: (typeof parsed?.ackReaction === "string" && parsed.ackReaction ? parsed.ackReaction : undefined),
|
|
66
|
+
replyToMode: parsed?.replyToMode,
|
|
67
|
+
textChunkLimit: parsed?.textChunkLimit,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Telegram Bot API backend. Mirrors DiscordBackend's PUBLIC surface (the members
|
|
72
|
+
// index.mjs / output-forwarder rely on) so createBackend() can hand either one to
|
|
73
|
+
// the same runtime with no call-site changes. INBOUND via long-poll getUpdates;
|
|
74
|
+
// OUTBOUND as plain text (no parse_mode) so code fences survive verbatim.
|
|
75
|
+
class TelegramBackend {
|
|
76
|
+
name = "telegram";
|
|
77
|
+
MAX_MESSAGE_LENGTH = MAX_TELEGRAM_MESSAGE;
|
|
78
|
+
onMessage = null;
|
|
79
|
+
// Telegram has no Discord-style slash/component interaction parity. These
|
|
80
|
+
// hooks are kept present-but-inert so index.mjs assignments/reads don't throw.
|
|
81
|
+
onInteraction = null;
|
|
82
|
+
onModalRequest = null;
|
|
83
|
+
onCustomCommand = null;
|
|
84
|
+
token;
|
|
85
|
+
mainChannelId;
|
|
86
|
+
stateDir;
|
|
87
|
+
configFile;
|
|
88
|
+
isStatic;
|
|
89
|
+
initialAccess;
|
|
90
|
+
sendCount = 0;
|
|
91
|
+
// Long-poll state.
|
|
92
|
+
_polling = false;
|
|
93
|
+
_pollAbort = null;
|
|
94
|
+
_connectPromise = null;
|
|
95
|
+
_offset = 0;
|
|
96
|
+
_typingIntervals = /* @__PURE__ */ new Map();
|
|
97
|
+
constructor(config, stateDir) {
|
|
98
|
+
this.token = config.token;
|
|
99
|
+
this.mainChannelId = config.mainChannelId ?? "";
|
|
100
|
+
this.stateDir = stateDir;
|
|
101
|
+
this.configFile = config.configPath ?? "";
|
|
102
|
+
this.isStatic = config.accessMode === "static";
|
|
103
|
+
this.initialAccess = normalizeAccess(config.access);
|
|
104
|
+
try { mkdirSync(this.stateDir, { recursive: true }); } catch {}
|
|
105
|
+
}
|
|
106
|
+
// Passthrough by design. MarkdownV2 conversion happens PER-CHUNK inside
|
|
107
|
+
// sendMessage (after chunking), NOT here — converting the whole text here
|
|
108
|
+
// then chunking on raw length could split through the middle of a MarkdownV2
|
|
109
|
+
// entity and produce an unbalanced-entity 400. Keeping formatOutgoing a
|
|
110
|
+
// no-op also guarantees conversion runs exactly once (at send time) so there
|
|
111
|
+
// is no risk of double-escaping.
|
|
112
|
+
formatOutgoing(text) {
|
|
113
|
+
return text;
|
|
114
|
+
}
|
|
115
|
+
// Fold every channelsConfig entry's channelId into access.channels, mirroring
|
|
116
|
+
// DiscordBackend.readConfigAccess(). Read live so same-backend channel adds
|
|
117
|
+
// apply without a restart. Falls back to initialAccess if the read throws.
|
|
118
|
+
readConfigAccess() {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = readSection("channels");
|
|
121
|
+
const access = normalizeAccess(parsed.access ?? this.initialAccess);
|
|
122
|
+
if (parsed.channelsConfig) {
|
|
123
|
+
for (const entry of Object.values(parsed.channelsConfig)) {
|
|
124
|
+
if (typeof entry === "object" && entry !== null) {
|
|
125
|
+
const id = entry.channelId;
|
|
126
|
+
if (id && !(id in access.channels)) {
|
|
127
|
+
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return access;
|
|
133
|
+
} catch {
|
|
134
|
+
return this.initialAccess;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
loadAccess() {
|
|
138
|
+
// The main chat is auto-allowed (single-source channel setup, mirrors
|
|
139
|
+
// DiscordBackend.loadAccess) so a configured mainChannelId is enough for
|
|
140
|
+
// both inbound gating and outbound. Base off the live config so all
|
|
141
|
+
// configured channels (not just main) are seen without a restart.
|
|
142
|
+
const a = this.readConfigAccess();
|
|
143
|
+
if (this.mainChannelId && a && !(this.mainChannelId in (a.channels ?? {}))) {
|
|
144
|
+
return { ...a, channels: { ...(a.channels ?? {}), [this.mainChannelId]: { allowFrom: [], requireMention: false } } };
|
|
145
|
+
}
|
|
146
|
+
return a;
|
|
147
|
+
}
|
|
148
|
+
_isAllowedChat(chatId) {
|
|
149
|
+
const access = this.loadAccess();
|
|
150
|
+
if (!access || access.dmPolicy === "disabled") return false;
|
|
151
|
+
const key = String(chatId);
|
|
152
|
+
if (this.mainChannelId && key === String(this.mainChannelId)) return true;
|
|
153
|
+
if (key in (access.channels ?? {})) return true;
|
|
154
|
+
if ((access.allowFrom ?? []).includes(key)) return true;
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
// ── Bot API helper ─────────────────────────────────────────────────
|
|
158
|
+
// POST <method> with a JSON body; parse the JSON envelope and normalize
|
|
159
|
+
// Telegram's error shape into a thrown Error that carries `.status` and
|
|
160
|
+
// `.retryAfter` so sendMessage's retry loop can key off them exactly like
|
|
161
|
+
// the Discord backend keys off discord.js error fields.
|
|
162
|
+
async _api(method, body, { signal } = {}) {
|
|
163
|
+
const url = `${API_BASE}/bot${this.token}/${method}`;
|
|
164
|
+
let res;
|
|
165
|
+
try {
|
|
166
|
+
res = await fetch(url, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { "content-type": "application/json" },
|
|
169
|
+
body: JSON.stringify(body ?? {}),
|
|
170
|
+
signal: signal ?? AbortSignal.timeout(30_000)
|
|
171
|
+
});
|
|
172
|
+
} catch (err) {
|
|
173
|
+
// Network/timeout — surface as a transient error (no status).
|
|
174
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
175
|
+
throw e;
|
|
176
|
+
}
|
|
177
|
+
let json = null;
|
|
178
|
+
try { json = await res.json(); } catch {}
|
|
179
|
+
if (!res.ok || !json || json.ok !== true) {
|
|
180
|
+
const code = json?.error_code ?? res.status;
|
|
181
|
+
const retryAfter = json?.parameters?.retry_after;
|
|
182
|
+
const desc = json?.description ?? `HTTP ${res.status}`;
|
|
183
|
+
const e = new Error(`telegram ${method} failed: ${desc}`);
|
|
184
|
+
e.status = code;
|
|
185
|
+
if (retryAfter != null) e.retryAfter = retryAfter;
|
|
186
|
+
throw e;
|
|
187
|
+
}
|
|
188
|
+
return json.result;
|
|
189
|
+
}
|
|
190
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
191
|
+
async connect() {
|
|
192
|
+
// Re-entry guard mirrors DiscordBackend.connect(): a second connect() while
|
|
193
|
+
// one is in flight/settled returns the same promise (no duplicate loops).
|
|
194
|
+
if (this._connectPromise) return this._connectPromise;
|
|
195
|
+
this._connectPromise = (async () => {
|
|
196
|
+
// Validate the token once (also surfaces a bad token early). getMe is
|
|
197
|
+
// cheap and non-mutating; a failure here throws so startup can react.
|
|
198
|
+
try {
|
|
199
|
+
const me = await this._api("getMe", {});
|
|
200
|
+
process.stderr.write(`mixdog telegram: connected as @${me?.username ?? me?.id ?? "?"}\n`);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
this._connectPromise = null;
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
this._polling = true;
|
|
206
|
+
// Fire-and-forget the poll loop; it self-reschedules until _polling clears.
|
|
207
|
+
void this._pollLoop();
|
|
208
|
+
})();
|
|
209
|
+
return this._connectPromise;
|
|
210
|
+
}
|
|
211
|
+
async _pollLoop() {
|
|
212
|
+
while (this._polling) {
|
|
213
|
+
const ac = new AbortController();
|
|
214
|
+
this._pollAbort = ac;
|
|
215
|
+
// getUpdates long-poll: server holds up to `timeout`s; give the client
|
|
216
|
+
// a slightly longer abort budget so a normal empty poll isn't aborted.
|
|
217
|
+
const timer = setTimeout(() => ac.abort(), 30_000);
|
|
218
|
+
try {
|
|
219
|
+
const updates = await this._api("getUpdates", {
|
|
220
|
+
timeout: 25,
|
|
221
|
+
offset: this._offset > 0 ? this._offset : undefined,
|
|
222
|
+
allowed_updates: ["message"]
|
|
223
|
+
}, { signal: ac.signal });
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
if (Array.isArray(updates)) {
|
|
226
|
+
for (const u of updates) {
|
|
227
|
+
// Advance offset past every update we observe so none reprocesses,
|
|
228
|
+
// even ones we ultimately drop (non-message / not allowlisted).
|
|
229
|
+
if (typeof u.update_id === "number" && u.update_id >= this._offset) {
|
|
230
|
+
this._offset = u.update_id + 1;
|
|
231
|
+
}
|
|
232
|
+
try { this._handleUpdate(u); } catch (e) {
|
|
233
|
+
process.stderr.write(`mixdog telegram: handleUpdate failed: ${e}\n`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch (err) {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
if (!this._polling) break; // disconnect() aborted us — exit quietly.
|
|
240
|
+
// 409 = another getUpdates/webhook consumer is active; other errors are
|
|
241
|
+
// transient. Back off and continue rather than crashing the loop.
|
|
242
|
+
const status = err?.status;
|
|
243
|
+
const backoff = status === 409 ? 5_000 : 2_000;
|
|
244
|
+
process.stderr.write(`mixdog telegram: getUpdates error (${status ?? "net"}); retrying in ${backoff}ms\n`);
|
|
245
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
246
|
+
} finally {
|
|
247
|
+
this._pollAbort = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
_handleUpdate(u) {
|
|
252
|
+
const msg = u.message;
|
|
253
|
+
if (!msg) return; // non-message update (we only asked for messages).
|
|
254
|
+
const chatId = msg.chat?.id;
|
|
255
|
+
if (chatId == null) return;
|
|
256
|
+
if (!this._isAllowedChat(chatId)) return;
|
|
257
|
+
const from = msg.from ?? {};
|
|
258
|
+
if (from.is_bot) return; // ignore bot echoes / other bots.
|
|
259
|
+
const text = msg.text ?? msg.caption ?? "";
|
|
260
|
+
const receivedAtMs = Date.now();
|
|
261
|
+
// Mirror discord.mjs handleInbound's onMessage object shape so downstream
|
|
262
|
+
// routing (resolveInboundRoute etc.) is backend-agnostic. Telegram has no
|
|
263
|
+
// thread/parent concept here → parentChatId null; attachments unsupported
|
|
264
|
+
// in scope 1 → [].
|
|
265
|
+
if (this.onMessage) {
|
|
266
|
+
this.onMessage({
|
|
267
|
+
chatId: String(chatId),
|
|
268
|
+
parentChatId: null,
|
|
269
|
+
messageId: String(msg.message_id),
|
|
270
|
+
receivedAtMs,
|
|
271
|
+
discordCreatedAtMs: typeof msg.date === "number" ? msg.date * 1000 : null,
|
|
272
|
+
user: from.username ?? [from.first_name, from.last_name].filter(Boolean).join(" ") ?? "user",
|
|
273
|
+
userId: String(from.id ?? ""),
|
|
274
|
+
text,
|
|
275
|
+
ts: new Date((typeof msg.date === "number" ? msg.date * 1000 : receivedAtMs)).toISOString(),
|
|
276
|
+
attachments: []
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async disconnect() {
|
|
281
|
+
this._polling = false;
|
|
282
|
+
try { this._pollAbort?.abort(); } catch {}
|
|
283
|
+
this._pollAbort = null;
|
|
284
|
+
for (const interval of this._typingIntervals.values()) clearInterval(interval);
|
|
285
|
+
this._typingIntervals.clear();
|
|
286
|
+
this._connectPromise = null;
|
|
287
|
+
}
|
|
288
|
+
resetSendCount() {
|
|
289
|
+
this.sendCount = 0;
|
|
290
|
+
}
|
|
291
|
+
// ── Outbound ───────────────────────────────────────────────────────
|
|
292
|
+
// Mirrors DiscordBackend.sendMessage: OWNS chunking + per-chunk retry + the
|
|
293
|
+
// opaque resume-token contract so output-forwarder (which passes whole text
|
|
294
|
+
// and hands opts.resumeToken back on retry) works unchanged.
|
|
295
|
+
async sendMessage(chatId, text, opts) {
|
|
296
|
+
const limit = MAX_TELEGRAM_MESSAGE;
|
|
297
|
+
// Chunk the RAW text, then guarantee every chunk stays within Telegram's
|
|
298
|
+
// 4096 limit AFTER MarkdownV2 escaping. Escaping can up to ~double a chunk
|
|
299
|
+
// (every special char gains a leading backslash), so a raw 4096 chunk of
|
|
300
|
+
// specials would become ~8192 and Telegram rejects it with a 400 "message
|
|
301
|
+
// is too long" (NOT a parse-entities error, so the parse fallback would not
|
|
302
|
+
// catch it). We resplit any chunk whose converted form exceeds the limit,
|
|
303
|
+
// halving the raw slice until the converted output fits. This keeps the
|
|
304
|
+
// final `chunks` array authoritative for the resume-token index/length.
|
|
305
|
+
const chunks = chunkForMarkdownV2(text, limit);
|
|
306
|
+
// Opaque resume token. Shape parity with discord.mjs:
|
|
307
|
+
// { hash, nextChunkIdx, sentIds, prefixed, limit }. Telegram has no
|
|
308
|
+
// continuation-prefix requirement, so `prefixed` is always false — it is
|
|
309
|
+
// emitted only so the token shape matches across backends.
|
|
310
|
+
const contentHash = createHash("md5").update(String(text)).digest("hex");
|
|
311
|
+
const resumeToken = opts?.resumeToken;
|
|
312
|
+
let startIdx = 0;
|
|
313
|
+
const sentIds = [];
|
|
314
|
+
// Only honor the token when it pins to this exact text AND chunk limit;
|
|
315
|
+
// otherwise the chunk boundaries could differ → full resend from 0 (safe).
|
|
316
|
+
if (resumeToken && resumeToken.hash === contentHash && resumeToken.limit === limit) {
|
|
317
|
+
startIdx = Math.max(0, Math.min(resumeToken.nextChunkIdx ?? 0, chunks.length));
|
|
318
|
+
if (Array.isArray(resumeToken.sentIds)) sentIds.push(...resumeToken.sentIds);
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
for (let i = startIdx; i < chunks.length; i++) {
|
|
322
|
+
// Per-chunk send with retry. On 429 honour Telegram's
|
|
323
|
+
// parameters.retry_after (seconds → ms, clamp 60000); other transient
|
|
324
|
+
// errors get a short backoff. Cap at 3 attempts; after the cap throw an
|
|
325
|
+
// error carrying a resume token so the caller requeues the WHOLE item
|
|
326
|
+
// and the next sendMessage resumes at this chunk index (no duplicates).
|
|
327
|
+
let attempt = 0;
|
|
328
|
+
for (;;) {
|
|
329
|
+
try {
|
|
330
|
+
// Convert THIS chunk to MarkdownV2 independently. Chunking happens
|
|
331
|
+
// on raw text first (above), then each chunk is converted on its
|
|
332
|
+
// own, so a MarkdownV2 entity can never span a chunk boundary →
|
|
333
|
+
// every sent chunk is guaranteed to be independently valid.
|
|
334
|
+
const md = toMarkdownV2(chunks[i]);
|
|
335
|
+
let result;
|
|
336
|
+
// Defensive length guard: chunkForMarkdownV2() already keeps every
|
|
337
|
+
// chunk's converted form within limit, but a pathological slice at
|
|
338
|
+
// the recursion floor could still exceed it. Rather than let
|
|
339
|
+
// Telegram 400 with "message is too long" (which isParseEntitiesError
|
|
340
|
+
// does NOT match, so the parse fallback below would be skipped and
|
|
341
|
+
// the item would retry-loop forever), send this chunk as plain text.
|
|
342
|
+
if (md.length > MAX_TELEGRAM_MESSAGE) {
|
|
343
|
+
process.stderr.write(`mixdog telegram: converted chunk ${i} exceeds ${MAX_TELEGRAM_MESSAGE} after escaping; sending as plain text\n`);
|
|
344
|
+
const plain = stripMdV2(md).slice(0, MAX_TELEGRAM_MESSAGE);
|
|
345
|
+
result = await this._api("sendMessage", { chat_id: chatId, text: plain });
|
|
346
|
+
sentIds.push(String(result?.message_id ?? ""));
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
result = await this._api("sendMessage", {
|
|
351
|
+
chat_id: chatId,
|
|
352
|
+
text: md,
|
|
353
|
+
parse_mode: "MarkdownV2"
|
|
354
|
+
});
|
|
355
|
+
} catch (mdErr) {
|
|
356
|
+
// Safety net: if the converter still produced something Telegram
|
|
357
|
+
// rejects as unparseable, resend this chunk ONCE as plain text so
|
|
358
|
+
// the user gets the content instead of a hard failure. Strip the
|
|
359
|
+
// MarkdownV2 markers/escapes so no stray backslashes show.
|
|
360
|
+
if (isParseEntitiesError(mdErr)) {
|
|
361
|
+
process.stderr.write(`mixdog telegram: MarkdownV2 parse rejected, resending chunk ${i} as plain text\n`);
|
|
362
|
+
result = await this._api("sendMessage", {
|
|
363
|
+
chat_id: chatId,
|
|
364
|
+
text: stripMdV2(md)
|
|
365
|
+
});
|
|
366
|
+
} else {
|
|
367
|
+
throw mdErr;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
sentIds.push(String(result?.message_id ?? ""));
|
|
371
|
+
break;
|
|
372
|
+
} catch (err) {
|
|
373
|
+
attempt++;
|
|
374
|
+
const status = err?.status;
|
|
375
|
+
// Classify: PERMANENT = a 4xx client error (chat not found 400,
|
|
376
|
+
// unauthorized 401, blocked 403, 404 …) that will never succeed on
|
|
377
|
+
// retry. TRANSIENT = 429, any 5xx, or network/timeout (no status).
|
|
378
|
+
// A permanent error must NOT retry-loop and must NOT carry a resume
|
|
379
|
+
// token — throw it immediately flagged so the forwarder drops the
|
|
380
|
+
// item instead of requeuing forever. (MarkdownV2 parse-entity 400s
|
|
381
|
+
// are already handled by the plain-text fallback above and never
|
|
382
|
+
// reach here.)
|
|
383
|
+
const isPermanent = typeof status === "number" && status >= 400 && status < 500 && status !== 429;
|
|
384
|
+
if (isPermanent) {
|
|
385
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
386
|
+
e.permanent = true;
|
|
387
|
+
throw e;
|
|
388
|
+
}
|
|
389
|
+
if (attempt >= 3) {
|
|
390
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
391
|
+
e.resumeToken = { hash: contentHash, nextChunkIdx: i, sentIds: [...sentIds], prefixed: false, limit };
|
|
392
|
+
throw e;
|
|
393
|
+
}
|
|
394
|
+
if (status === 429) {
|
|
395
|
+
// Telegram's parameters.retry_after is ALWAYS in SECONDS, so
|
|
396
|
+
// convert seconds→ms. Clamp to [0, 60000]; fall back to 1s when
|
|
397
|
+
// the field is absent/invalid.
|
|
398
|
+
const retryAfterMs = Number(err?.retryAfter) * 1000;
|
|
399
|
+
const ms = Number.isFinite(retryAfterMs) && retryAfterMs > 0 ? Math.min(retryAfterMs, 60_000) : 1000;
|
|
400
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
401
|
+
} else {
|
|
402
|
+
// Other transient error (5xx / network): short backoff.
|
|
403
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
this.sendCount += sentIds.length;
|
|
409
|
+
} catch (err) {
|
|
410
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
411
|
+
const wrapped = new Error(`send failed after ${sentIds.length}/${chunks.length} chunk(s): ${msg}`);
|
|
412
|
+
// Preserve the opaque resume token across the rewrap so the caller can
|
|
413
|
+
// persist it on the queue item and resume on retry (matches discord.mjs).
|
|
414
|
+
if (err?.resumeToken) wrapped.resumeToken = err.resumeToken;
|
|
415
|
+
// Propagate the permanent flag so the forwarder drops (not requeues) it.
|
|
416
|
+
if (err?.permanent) wrapped.permanent = true;
|
|
417
|
+
throw wrapped;
|
|
418
|
+
}
|
|
419
|
+
return { sentIds };
|
|
420
|
+
}
|
|
421
|
+
// Telegram supports message reactions via setMessageReaction. Best-effort:
|
|
422
|
+
// an emoji Telegram doesn't allow as a reaction 400s → swallow (non-critical).
|
|
423
|
+
async react(chatId, messageId, emoji) {
|
|
424
|
+
try {
|
|
425
|
+
await this._api("setMessageReaction", {
|
|
426
|
+
chat_id: chatId,
|
|
427
|
+
message_id: Number(messageId),
|
|
428
|
+
reaction: [{ type: "emoji", emoji }]
|
|
429
|
+
});
|
|
430
|
+
} catch { /* reactions are non-critical */ }
|
|
431
|
+
}
|
|
432
|
+
async removeReaction(chatId, messageId, _emoji) {
|
|
433
|
+
try {
|
|
434
|
+
await this._api("setMessageReaction", {
|
|
435
|
+
chat_id: chatId,
|
|
436
|
+
message_id: Number(messageId),
|
|
437
|
+
reaction: []
|
|
438
|
+
});
|
|
439
|
+
} catch { /* best-effort */ }
|
|
440
|
+
}
|
|
441
|
+
// Bot API cannot read arbitrary chat history (no getChatHistory for bots);
|
|
442
|
+
// return [] so callers that expect a list degrade gracefully.
|
|
443
|
+
async fetchMessages() {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
// editMessageText exists; kept minimal for scope-1 parity so index.mjs's
|
|
447
|
+
// edit path doesn't throw. Sends the first chunk's worth only (no overflow
|
|
448
|
+
// management like Discord's editOverflow — a later scope can extend this).
|
|
449
|
+
async editMessage(chatId, messageId, text, _opts) {
|
|
450
|
+
try {
|
|
451
|
+
const result = await this._api("editMessageText", {
|
|
452
|
+
chat_id: chatId,
|
|
453
|
+
message_id: Number(messageId),
|
|
454
|
+
text: chunk(text, MAX_TELEGRAM_MESSAGE)[0] ?? ""
|
|
455
|
+
});
|
|
456
|
+
return String(result?.message_id ?? messageId);
|
|
457
|
+
} catch {
|
|
458
|
+
return String(messageId);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async deleteMessage(chatId, messageId) {
|
|
462
|
+
try {
|
|
463
|
+
await this._api("deleteMessage", { chat_id: chatId, message_id: Number(messageId) });
|
|
464
|
+
} catch { /* best-effort */ }
|
|
465
|
+
}
|
|
466
|
+
// Attachment download is unsupported in scope 1. getFile + a download step
|
|
467
|
+
// can be added later; returning [] keeps the call site safe meanwhile.
|
|
468
|
+
async downloadAttachment() {
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
async validateChannel(chatId) {
|
|
472
|
+
// getChat confirms the bot can see the chat; throws (like Discord's
|
|
473
|
+
// fetchAllowedChannel) when it can't.
|
|
474
|
+
await this._api("getChat", { chat_id: chatId });
|
|
475
|
+
}
|
|
476
|
+
// ── Typing indicator ───────────────────────────────────────────────
|
|
477
|
+
// sendChatAction "typing" lasts ~5s server-side, so refresh on an interval
|
|
478
|
+
// while active — mirrors DiscordBackend.startTyping's interval model.
|
|
479
|
+
startTyping(channelId) {
|
|
480
|
+
this.stopTyping(channelId);
|
|
481
|
+
const fire = () => {
|
|
482
|
+
void this._api("sendChatAction", { chat_id: channelId, action: "typing" }).catch(() => {});
|
|
483
|
+
};
|
|
484
|
+
fire();
|
|
485
|
+
const interval = setInterval(fire, 4_000);
|
|
486
|
+
this._typingIntervals.set(String(channelId), interval);
|
|
487
|
+
}
|
|
488
|
+
stopTyping(channelId) {
|
|
489
|
+
const key = String(channelId);
|
|
490
|
+
const interval = this._typingIntervals.get(key);
|
|
491
|
+
if (interval) {
|
|
492
|
+
clearInterval(interval);
|
|
493
|
+
this._typingIntervals.delete(key);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export {
|
|
499
|
+
TelegramBackend,
|
|
500
|
+
MAX_TELEGRAM_MESSAGE
|
|
501
|
+
};
|