switchroom 0.7.15 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -59
- package/bin/run-hook.sh +27 -11
- package/bin/timezone-hook.sh +9 -7
- package/dist/agent-scheduler/index.js +410 -133
- package/dist/auth-broker/index.js +13932 -0
- package/dist/cli/switchroom.js +26937 -5601
- package/dist/host-control/main.js +12702 -0
- package/dist/vault/approvals/kernel-server.js +467 -184
- package/dist/vault/broker/server.js +1430 -724
- package/examples/minimal.yaml +63 -0
- package/examples/personal-google-workspace-mcp/.env.example +34 -0
- package/examples/personal-google-workspace-mcp/README.md +194 -0
- package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
- package/examples/switchroom.yaml +220 -0
- package/package.json +7 -4
- package/profiles/_base/settings.json.hbs +20 -5
- package/profiles/_base/start.sh.hbs +16 -3
- package/profiles/_shared/agent-self-service.md.hbs +126 -0
- package/profiles/_shared/telegram-style.md.hbs +20 -90
- package/profiles/_shared/vault-protocol.md.hbs +68 -0
- package/profiles/default/CLAUDE.md +50 -96
- package/profiles/default/CLAUDE.md.hbs +36 -6
- package/profiles/default/workspace/SOUL.md.hbs +12 -5
- package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
- package/skills/buildkite-agent-runtime/SKILL.md +44 -11
- package/skills/buildkite-api/SKILL.md +31 -8
- package/skills/buildkite-cli/SKILL.md +27 -9
- package/skills/buildkite-migration/SKILL.md +22 -9
- package/skills/buildkite-pipelines/SKILL.md +26 -9
- package/skills/buildkite-secure-delivery/SKILL.md +23 -9
- package/skills/buildkite-test-engine/SKILL.md +25 -8
- package/skills/docx/SKILL.md +1 -1
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/file-bug/SKILL.md +34 -6
- package/skills/humanizer/SKILL.md +15 -0
- package/skills/humanizer-calibrate/SKILL.md +7 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/pdf/SKILL.md +1 -1
- package/skills/pptx/SKILL.md +1 -1
- package/skills/skill-creator/SKILL.md +21 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/skills/switchroom-cli/SKILL.md +63 -64
- package/skills/switchroom-health/SKILL.md +23 -10
- package/skills/switchroom-install/SKILL.md +3 -3
- package/skills/switchroom-manage/SKILL.md +26 -19
- package/skills/switchroom-runtime/SKILL.md +191 -0
- package/skills/switchroom-status/SKILL.md +27 -2
- package/skills/telegram-test-harness/SKILL.md +3 -0
- package/skills/token-helpers/SKILL.md +24 -1
- package/skills/webapp-testing/SKILL.md +31 -1
- package/skills/xlsx/SKILL.md +1 -1
- package/telegram-plugin/admin-commands/index.ts +7 -5
- package/telegram-plugin/analytics-posthog.ts +191 -0
- package/telegram-plugin/bridge/bridge.ts +69 -0
- package/telegram-plugin/bridge/ipc-client.ts +4 -1
- package/telegram-plugin/dist/bridge/bridge.js +194 -119
- package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
- package/telegram-plugin/dist/server.js +245 -189
- package/telegram-plugin/first-paint.ts +3 -24
- package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
- package/telegram-plugin/gateway/auth-command.ts +794 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/boot-card.ts +169 -40
- package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
- package/telegram-plugin/gateway/boot-probes.ts +166 -123
- package/telegram-plugin/gateway/boot-reason.ts +41 -7
- package/telegram-plugin/gateway/boot-version.ts +66 -0
- package/telegram-plugin/gateway/gateway.ts +3499 -1885
- package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
- package/telegram-plugin/gateway/quarantine.ts +69 -0
- package/telegram-plugin/gateway/quota-cache.ts +9 -4
- package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
- package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
- package/telegram-plugin/gateway/recent-denials.ts +77 -0
- package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
- package/telegram-plugin/history.ts +91 -0
- package/telegram-plugin/hooks/hooks.json +10 -0
- package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/inbound-classifier.ts +50 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +4 -2
- package/telegram-plugin/permission-rule.ts +51 -0
- package/telegram-plugin/permission-title.ts +56 -0
- package/telegram-plugin/quota-check.ts +19 -41
- package/telegram-plugin/registry/reaper.ts +223 -0
- package/telegram-plugin/retry-api-call.ts +80 -0
- package/telegram-plugin/runtime-metrics.ts +177 -0
- package/telegram-plugin/scripts/build.mjs +0 -1
- package/telegram-plugin/secret-detect/index.ts +24 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
- package/telegram-plugin/secret-detect/vault-error.ts +78 -11
- package/telegram-plugin/secret-detect/vault-write.ts +14 -2
- package/telegram-plugin/server.js +41795 -0
- package/telegram-plugin/session-tail.ts +6 -1
- package/telegram-plugin/shared/bot-runtime.ts +5 -4
- package/telegram-plugin/silence-poke.ts +420 -0
- package/telegram-plugin/silent-end.ts +174 -0
- package/telegram-plugin/stream-controller.ts +13 -0
- package/telegram-plugin/stream-reply-handler.ts +7 -0
- package/telegram-plugin/subagent-watcher.ts +213 -4
- package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
- package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
- package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
- package/telegram-plugin/tests/boot-probes.test.ts +216 -10
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
- package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
- package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
- package/telegram-plugin/tests/history-reaper.test.ts +378 -0
- package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
- package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
- package/telegram-plugin/tests/issues-card.test.ts +49 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
- package/telegram-plugin/tests/permission-rule.test.ts +80 -1
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/tests/races.test.ts +179 -0
- package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
- package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
- package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
- package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
- package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
- package/telegram-plugin/tests/silence-poke.test.ts +493 -0
- package/telegram-plugin/tests/silent-end.test.ts +206 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
- package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
- package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
- package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
- package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
- package/telegram-plugin/turn-signal-tracker.ts +100 -24
- package/telegram-plugin/uat/SETUP.md +210 -35
- package/telegram-plugin/uat/assertions.ts +264 -37
- package/telegram-plugin/uat/driver-info.ts +57 -0
- package/telegram-plugin/uat/driver.ts +590 -51
- package/telegram-plugin/uat/harness.ts +140 -94
- package/telegram-plugin/uat/load-env.test.ts +72 -0
- package/telegram-plugin/uat/load-env.ts +48 -0
- package/telegram-plugin/uat/login.ts +96 -53
- package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
- package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
- package/telegram-plugin/uat/runners/report.ts +150 -0
- package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
- package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
- package/telegram-plugin/uat/runners/scorer.ts +106 -0
- package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
- package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
- package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
- package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
- package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
- package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
- package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
- package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
- package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
- package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
- package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
- package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
- package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
- package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
- package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
- package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
- package/telegram-plugin/vault-approval-posture.ts +42 -0
- package/telegram-plugin/welcome-text.ts +1 -0
- package/telegram-plugin/active-pins-sweep.ts +0 -204
- package/telegram-plugin/active-pins.ts +0 -146
- package/telegram-plugin/auth-dashboard.ts +0 -1104
- package/telegram-plugin/auth-slot-parser.ts +0 -497
- package/telegram-plugin/card-event-log.ts +0 -138
- package/telegram-plugin/dist/foreman/foreman.js +0 -31106
- package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
- package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
- package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
- package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
- package/telegram-plugin/foreman/foreman.ts +0 -1165
- package/telegram-plugin/foreman/setup-flow.ts +0 -345
- package/telegram-plugin/foreman/setup-state.ts +0 -239
- package/telegram-plugin/foreman/state.ts +0 -203
- package/telegram-plugin/pin-event-log.ts +0 -76
- package/telegram-plugin/progress-card-driver.ts +0 -2886
- package/telegram-plugin/progress-card-pin-manager.ts +0 -589
- package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
- package/telegram-plugin/progress-card.ts +0 -1409
- package/telegram-plugin/tests/HARNESS.md +0 -340
- package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
- package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
- package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
- package/telegram-plugin/tests/active-pins.test.ts +0 -187
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
- package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
- package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
- package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
- package/telegram-plugin/tests/card-event-log.test.ts +0 -145
- package/telegram-plugin/tests/first-paint.test.ts +0 -257
- package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
- package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
- package/telegram-plugin/tests/foreman-state.test.ts +0 -164
- package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
- package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
- package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
- package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
- package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
- package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
- package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
- package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
- package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
- package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
- package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
- package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
- package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
- package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
- package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
- package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
- package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
- package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
- package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
- package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
- package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
- package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
- package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
- package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
- package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
- package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
- package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
- package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
- package/telegram-plugin/tests/setup-flow.test.ts +0 -510
- package/telegram-plugin/tests/setup-state.test.ts +0 -146
- package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
- package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
- package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
- package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
- package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
- package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
- package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
- package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
- package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
- package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
- package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
- package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
- package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
- package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
- package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
- package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
- package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
- package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
- package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
- package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
- package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
- package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
- package/telegram-plugin/two-zone-card.ts +0 -269
- package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
|
@@ -8,17 +8,23 @@
|
|
|
8
8
|
* and admin rights — see Telegram Bots FAQ. The driver sends fixture
|
|
9
9
|
* inbounds and observes everything the test bot emits.
|
|
10
10
|
*
|
|
11
|
-
* Phase
|
|
12
|
-
* `
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
11
|
+
* Phase 2 wires real mtcute lifecycle: `MemoryStorage` so no SQLite
|
|
12
|
+
* is touched, `importSession` to load the bearer string, real
|
|
13
|
+
* `connect`/`disconnect`, real `sendText` (with forum-topic routing),
|
|
14
|
+
* and `observeMessages` backed by `onNewMessage`/`onEditMessage`.
|
|
15
15
|
*
|
|
16
16
|
* Security: never log session strings, never log message bodies that
|
|
17
17
|
* might contain auth codes (see `auth-code-redact.ts` for the
|
|
18
18
|
* production pattern).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
MemoryStorage,
|
|
23
|
+
TelegramClient,
|
|
24
|
+
getMarkedPeerId,
|
|
25
|
+
InputMedia,
|
|
26
|
+
} from "@mtcute/node";
|
|
27
|
+
import type { Message } from "@mtcute/node";
|
|
22
28
|
|
|
23
29
|
export interface DriverOptions {
|
|
24
30
|
/** Telegram developer credential — `api_id` from my.telegram.org. */
|
|
@@ -34,7 +40,12 @@ export interface DriverOptions {
|
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export interface SendTextOptions {
|
|
37
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Forum topic id. For supergroups with topics enabled this is the
|
|
45
|
+
* `message_thread_id` from the Bot API. mtcute maps it to the
|
|
46
|
+
* `replyTo` parameter on send — the topic's "top message id" is
|
|
47
|
+
* what the server expects.
|
|
48
|
+
*/
|
|
38
49
|
messageThreadId?: number;
|
|
39
50
|
/** Reply-quote a specific earlier message id. */
|
|
40
51
|
replyTo?: number;
|
|
@@ -45,12 +56,48 @@ export interface ObservedMessage {
|
|
|
45
56
|
messageId: number;
|
|
46
57
|
threadId?: number;
|
|
47
58
|
text: string;
|
|
48
|
-
/**
|
|
49
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Sender's user_id (or channel_id, for posts in a channel). Used by
|
|
61
|
+
* `expectMessage` to filter `from: "bot"` vs `from: "driver"`.
|
|
62
|
+
*/
|
|
63
|
+
senderUserId: number;
|
|
50
64
|
fromBot: boolean;
|
|
51
65
|
date: Date;
|
|
66
|
+
/** `true` when this observation is an edit of an earlier message. */
|
|
67
|
+
edited: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* `true` when this message was delivered as a silent push (Telegram
|
|
70
|
+
* `silent` flag, set by the sender's `disable_notification: true`).
|
|
71
|
+
* Used to verify the conversational-pacing contract: mid-turn updates
|
|
72
|
+
* must be silent, only the final answer should ping.
|
|
73
|
+
*/
|
|
74
|
+
silent: boolean;
|
|
52
75
|
}
|
|
53
76
|
|
|
77
|
+
export interface ObservedButton {
|
|
78
|
+
/** Visible label on the button. */
|
|
79
|
+
text: string;
|
|
80
|
+
/**
|
|
81
|
+
* Inline-callback button payload (Bot API `callback_data`). UTF-8
|
|
82
|
+
* decoded from the raw `Uint8Array` mtcute exposes. Undefined for
|
|
83
|
+
* URL buttons / web-app buttons / other non-callback button kinds.
|
|
84
|
+
*/
|
|
85
|
+
callbackData?: string;
|
|
86
|
+
/** URL for `url` buttons; undefined for callback buttons. */
|
|
87
|
+
url?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 2-D matrix of inline buttons, matching Bot API's
|
|
92
|
+
* `inline_keyboard: [[{text, callback_data}, ...], ...]`.
|
|
93
|
+
*
|
|
94
|
+
* Only `type === "inline"` keyboards are returned by `getKeyboard` —
|
|
95
|
+
* reply-keyboard / force-reply / hide markups aren't used by the
|
|
96
|
+
* gateway for vault UX flows and would require a separate driver
|
|
97
|
+
* surface (typing the reply instead of tapping).
|
|
98
|
+
*/
|
|
99
|
+
export type ObservedKeyboard = ObservedButton[][];
|
|
100
|
+
|
|
54
101
|
export interface ObservedReaction {
|
|
55
102
|
chatId: number;
|
|
56
103
|
messageId: number;
|
|
@@ -77,17 +124,30 @@ export class Driver {
|
|
|
77
124
|
constructor(private readonly opts: DriverOptions) {}
|
|
78
125
|
|
|
79
126
|
async connect(): Promise<void> {
|
|
80
|
-
//
|
|
81
|
-
// string
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// and feed `this.opts.session` through it.
|
|
127
|
+
// MemoryStorage keeps all session state in memory — the session
|
|
128
|
+
// string we hold in `opts.session` is the only durable source of
|
|
129
|
+
// truth. This sidesteps SQLite entirely (native bindings, file
|
|
130
|
+
// locking, ephemeral STATE_DIR cleanup) and makes per-scenario
|
|
131
|
+
// teardown a no-op for the driver's storage layer.
|
|
86
132
|
this.client = new TelegramClient({
|
|
87
133
|
apiId: this.opts.apiId,
|
|
88
134
|
apiHash: this.opts.apiHash,
|
|
135
|
+
storage: new MemoryStorage(),
|
|
89
136
|
});
|
|
90
|
-
|
|
137
|
+
|
|
138
|
+
// `force: true` because MemoryStorage is always empty at construct
|
|
139
|
+
// time — without force, mtcute treats the (non-existent) prior
|
|
140
|
+
// session as authoritative and silently ignores ours.
|
|
141
|
+
await this.client.importSession(this.opts.session, true);
|
|
142
|
+
await this.client.connect();
|
|
143
|
+
// `connect()` opens the transport but does NOT start the updates
|
|
144
|
+
// dispatch loop — that's `start()`'s job. For a returning session
|
|
145
|
+
// (no interactive login) we have to call `startUpdatesLoop()`
|
|
146
|
+
// ourselves, otherwise `onNewMessage` / `onEditMessage` never
|
|
147
|
+
// fire and `observeMessages` silently waits forever. Symptom:
|
|
148
|
+
// `expectMessage` timing out even though the bot's reply has
|
|
149
|
+
// arrived in the chat (visible in Telegram).
|
|
150
|
+
await this.client.startUpdatesLoop();
|
|
91
151
|
}
|
|
92
152
|
|
|
93
153
|
async disconnect(): Promise<void> {
|
|
@@ -102,67 +162,532 @@ export class Driver {
|
|
|
102
162
|
opts?: SendTextOptions,
|
|
103
163
|
): Promise<{ messageId: number }> {
|
|
104
164
|
const c = this.requireClient();
|
|
105
|
-
// mtcute
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
165
|
+
// mtcute's CommonSendParams.replyTo doubles as the forum-topic
|
|
166
|
+
// target — passing the topic's top-message id routes the new
|
|
167
|
+
// message into that topic. Explicit `opts.replyTo` (quote-reply)
|
|
168
|
+
// takes precedence if both are set; this matches Bot API
|
|
169
|
+
// behaviour where `reply_to_message_id` overrides
|
|
170
|
+
// `message_thread_id`.
|
|
171
|
+
const replyTo = opts?.replyTo ?? opts?.messageThreadId;
|
|
172
|
+
const sent = await c.sendText(chatId, text, replyTo ? { replyTo } : undefined);
|
|
112
173
|
return { messageId: sent.id };
|
|
113
174
|
}
|
|
114
175
|
|
|
115
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Return the driver's own Telegram user_id. Cached after first call.
|
|
178
|
+
*/
|
|
179
|
+
async getMyUserId(): Promise<number> {
|
|
180
|
+
const c = this.requireClient();
|
|
181
|
+
const me = await c.getMe();
|
|
182
|
+
return me.id;
|
|
183
|
+
}
|
|
116
184
|
|
|
117
185
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* `
|
|
186
|
+
* Unpin every pinned message in `chatId`. Used by the harness's
|
|
187
|
+
* settle phase: a stale pin from the previous scenario's turn would
|
|
188
|
+
* otherwise be reused by the gateway via edit (no new pin event),
|
|
189
|
+
* making `expectPinnedCard` time out. Best-effort — logs and swallows
|
|
190
|
+
* Telegram errors so an unrelated network drop / flood-wait doesn't
|
|
191
|
+
* abort spinUp before the scenario runs. The next assertion (e.g.
|
|
192
|
+
* `expectPinnedCard`) will fail loudly with its own deadline if the
|
|
193
|
+
* unpin actually mattered, so the warning is enough to root-cause
|
|
194
|
+
* post-hoc without a silent failure mode.
|
|
122
195
|
*/
|
|
123
|
-
async
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
196
|
+
async unpinAllMessages(chatId: number): Promise<void> {
|
|
197
|
+
const c = this.requireClient();
|
|
198
|
+
await c.unpinAllMessages(chatId).catch((err: unknown) => {
|
|
199
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
200
|
+
// eslint-disable-next-line no-console -- harness diagnostic
|
|
201
|
+
console.warn(`[uat/driver] unpinAllMessages(${chatId}) failed: ${msg}`);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolve a bot username (with or without `@`) to its user_id. The
|
|
207
|
+
* resulting id doubles as the chat_id for DMing the bot from the
|
|
208
|
+
* driver — Telegram DMs use the peer's user_id as the chat_id.
|
|
209
|
+
*/
|
|
210
|
+
async resolveBotUserId(username: string): Promise<number> {
|
|
211
|
+
const c = this.requireClient();
|
|
212
|
+
const handle = username.startsWith("@") ? username : `@${username}`;
|
|
213
|
+
const peer = await c.resolvePeer(handle);
|
|
214
|
+
// For a bot/user the resolved peer is `inputPeerUser` carrying the
|
|
215
|
+
// numeric `userId` we need.
|
|
216
|
+
const u = peer as { userId?: number; channelId?: number };
|
|
217
|
+
if (typeof u.userId === "number") return u.userId;
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Driver.resolveBotUserId: '${handle}' did not resolve to a user (got ${JSON.stringify(peer)})`,
|
|
220
|
+
);
|
|
129
221
|
}
|
|
130
222
|
|
|
131
223
|
/**
|
|
132
|
-
*
|
|
133
|
-
* Returns an async iterable so scenarios
|
|
134
|
-
* predicate matches.
|
|
135
|
-
*
|
|
224
|
+
* Subscribe to new + edited messages in `chatId` (optionally
|
|
225
|
+
* filtered to a forum topic). Returns an async iterable so scenarios
|
|
226
|
+
* can `for await` until a predicate matches. Each yielded value
|
|
227
|
+
* carries an `edited` flag so the scenario can distinguish initial
|
|
228
|
+
* sends from progress-card edits.
|
|
229
|
+
*
|
|
230
|
+
* Backfill: mtcute's emitters fire only for live updates, not
|
|
231
|
+
* history. Scenarios that need to observe a message sent before
|
|
232
|
+
* the observer started should poll `getHistory` directly (Phase 3
|
|
233
|
+
* helper). The smoke test sends *after* `observeMessages` starts,
|
|
234
|
+
* so no backfill needed.
|
|
136
235
|
*/
|
|
137
236
|
observeMessages(
|
|
138
|
-
|
|
139
|
-
|
|
237
|
+
chatId: number,
|
|
238
|
+
opts?: { threadId?: number },
|
|
140
239
|
): AsyncIterable<ObservedMessage> {
|
|
141
|
-
|
|
240
|
+
const c = this.requireClient();
|
|
241
|
+
const targetThread = opts?.threadId;
|
|
242
|
+
const queue: ObservedMessage[] = [];
|
|
243
|
+
const waiters: Array<(m: IteratorResult<ObservedMessage>) => void> = [];
|
|
244
|
+
let closed = false;
|
|
245
|
+
|
|
246
|
+
const dispatch = (m: ObservedMessage): void => {
|
|
247
|
+
const w = waiters.shift();
|
|
248
|
+
if (w) w({ value: m, done: false });
|
|
249
|
+
else queue.push(m);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const onNew = (msg: Message): void => {
|
|
253
|
+
const observed = toObserved(msg, false);
|
|
254
|
+
if (observed.chatId !== chatId) return;
|
|
255
|
+
if (targetThread !== undefined && observed.threadId !== targetThread) return;
|
|
256
|
+
dispatch(observed);
|
|
257
|
+
};
|
|
258
|
+
const onEdit = (msg: Message): void => {
|
|
259
|
+
const observed = toObserved(msg, true);
|
|
260
|
+
if (observed.chatId !== chatId) return;
|
|
261
|
+
if (targetThread !== undefined && observed.threadId !== targetThread) return;
|
|
262
|
+
dispatch(observed);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
c.onNewMessage.add(onNew);
|
|
266
|
+
c.onEditMessage.add(onEdit);
|
|
267
|
+
|
|
268
|
+
const close = (): void => {
|
|
269
|
+
if (closed) return;
|
|
270
|
+
closed = true;
|
|
271
|
+
c.onNewMessage.remove(onNew);
|
|
272
|
+
c.onEditMessage.remove(onEdit);
|
|
273
|
+
while (waiters.length > 0) {
|
|
274
|
+
waiters.shift()?.({ value: undefined as never, done: true });
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
[Symbol.asyncIterator](): AsyncIterator<ObservedMessage> {
|
|
280
|
+
return {
|
|
281
|
+
next(): Promise<IteratorResult<ObservedMessage>> {
|
|
282
|
+
if (queue.length > 0) {
|
|
283
|
+
return Promise.resolve({ value: queue.shift()!, done: false });
|
|
284
|
+
}
|
|
285
|
+
if (closed) {
|
|
286
|
+
return Promise.resolve({ value: undefined as never, done: true });
|
|
287
|
+
}
|
|
288
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
289
|
+
},
|
|
290
|
+
return(): Promise<IteratorResult<ObservedMessage>> {
|
|
291
|
+
close();
|
|
292
|
+
return Promise.resolve({ value: undefined as never, done: true });
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
};
|
|
142
297
|
}
|
|
143
298
|
|
|
144
299
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
300
|
+
* Subscribe to message-reaction add/remove ops in `chatId` (and
|
|
301
|
+
* optionally on a specific `messageId`).
|
|
302
|
+
*
|
|
303
|
+
* Implementation notes:
|
|
304
|
+
*
|
|
305
|
+
* - mtcute parses `updateBotMessageReaction` for bot accounts; for
|
|
306
|
+
* USER accounts (the driver) we have to handle the raw
|
|
307
|
+
* `updateMessageReactions` ourselves via `onRawUpdate`. The TL
|
|
308
|
+
* carries the full new reaction set, not a delta — we diff
|
|
309
|
+
* against the prior set we've cached for the same `msgId` to
|
|
310
|
+
* emit add (`+`) / remove (`-`) ops.
|
|
311
|
+
*
|
|
312
|
+
* - DM / group / supergroup all supported. `chatId` follows the
|
|
313
|
+
* Bot API marked-id convention (positive for users, negative
|
|
314
|
+
* for groups, -100… for supergroups/channels). Internally we
|
|
315
|
+
* normalize the raw `peer` field with mtcute's `getMarkedPeerId`
|
|
316
|
+
* so callers never see raw TL peer types.
|
|
317
|
+
*
|
|
318
|
+
* - `threadId` filters to a forum-topic id (the raw update's
|
|
319
|
+
* `topMsgId`). Useful for supergroup-with-topics scenarios; a
|
|
320
|
+
* no-op for DMs/basic groups.
|
|
321
|
+
*
|
|
322
|
+
* - Reactions are emitted only when they CHANGE. The initial
|
|
323
|
+
* reaction-add fires as `op: "+"`; a follow-up
|
|
324
|
+
* `setMessageReaction` that REPLACES the prior emoji emits `-`
|
|
325
|
+
* for the old + `+` for the new.
|
|
326
|
+
*
|
|
327
|
+
* - Custom emojis (`reactionCustomEmoji`) are skipped — scenarios
|
|
328
|
+
* that need them aren't in scope and parsing them would require
|
|
329
|
+
* resolving the document id to an alias.
|
|
149
330
|
*/
|
|
150
331
|
observeReactions(
|
|
151
|
-
|
|
152
|
-
|
|
332
|
+
chatId: number,
|
|
333
|
+
opts?: { messageId?: number; threadId?: number },
|
|
153
334
|
): AsyncIterable<ObservedReaction> {
|
|
154
|
-
|
|
335
|
+
const c = this.requireClient();
|
|
336
|
+
const targetMsgId = opts?.messageId;
|
|
337
|
+
const targetThread = opts?.threadId;
|
|
338
|
+
const queue: ObservedReaction[] = [];
|
|
339
|
+
const waiters: Array<(m: IteratorResult<ObservedReaction>) => void> = [];
|
|
340
|
+
let closed = false;
|
|
341
|
+
const prior = new Map<number, Set<string>>();
|
|
342
|
+
|
|
343
|
+
const dispatch = (r: ObservedReaction): void => {
|
|
344
|
+
const w = waiters.shift();
|
|
345
|
+
if (w) w({ value: r, done: false });
|
|
346
|
+
else queue.push(r);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const onRaw = (info: { update: unknown }): void => {
|
|
350
|
+
const u = info.update as {
|
|
351
|
+
_: string;
|
|
352
|
+
peer?: unknown;
|
|
353
|
+
msgId?: number;
|
|
354
|
+
topMsgId?: number;
|
|
355
|
+
reactions?: {
|
|
356
|
+
results?: Array<{
|
|
357
|
+
reaction: { _: string; emoticon?: string };
|
|
358
|
+
}>;
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
if (u._ !== "updateMessageReactions") return;
|
|
362
|
+
if (!u.peer) return;
|
|
363
|
+
// mtcute's getMarkedPeerId handles peerUser / peerChat / peerChannel
|
|
364
|
+
// uniformly — normalizes to Bot API marked-id form (-100... for
|
|
365
|
+
// supergroups, -... for basic groups, positive for users).
|
|
366
|
+
let peerId: number;
|
|
367
|
+
try {
|
|
368
|
+
peerId = getMarkedPeerId(u.peer as Parameters<typeof getMarkedPeerId>[0]);
|
|
369
|
+
} catch {
|
|
370
|
+
return; // unrecognized peer shape
|
|
371
|
+
}
|
|
372
|
+
if (peerId !== chatId) return;
|
|
373
|
+
const msgId = u.msgId;
|
|
374
|
+
if (typeof msgId !== "number") return;
|
|
375
|
+
if (targetMsgId !== undefined && msgId !== targetMsgId) return;
|
|
376
|
+
if (targetThread !== undefined && u.topMsgId !== targetThread) return;
|
|
377
|
+
|
|
378
|
+
const now = new Set<string>();
|
|
379
|
+
for (const rc of u.reactions?.results ?? []) {
|
|
380
|
+
if (
|
|
381
|
+
rc.reaction?._ === "reactionEmoji" &&
|
|
382
|
+
typeof rc.reaction.emoticon === "string"
|
|
383
|
+
) {
|
|
384
|
+
now.add(rc.reaction.emoticon);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const before = prior.get(msgId) ?? new Set<string>();
|
|
388
|
+
const date = new Date();
|
|
389
|
+
for (const e of now) {
|
|
390
|
+
if (!before.has(e)) {
|
|
391
|
+
dispatch({ chatId, messageId: msgId, emoji: e, op: "+", date });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const e of before) {
|
|
395
|
+
if (!now.has(e)) {
|
|
396
|
+
dispatch({ chatId, messageId: msgId, emoji: e, op: "-", date });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
prior.set(msgId, now);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
c.onRawUpdate.add(onRaw);
|
|
403
|
+
|
|
404
|
+
const close = (): void => {
|
|
405
|
+
if (closed) return;
|
|
406
|
+
closed = true;
|
|
407
|
+
c.onRawUpdate.remove(onRaw);
|
|
408
|
+
while (waiters.length > 0) {
|
|
409
|
+
waiters.shift()?.({ value: undefined as never, done: true });
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
[Symbol.asyncIterator](): AsyncIterator<ObservedReaction> {
|
|
415
|
+
return {
|
|
416
|
+
next(): Promise<IteratorResult<ObservedReaction>> {
|
|
417
|
+
if (queue.length > 0) {
|
|
418
|
+
return Promise.resolve({ value: queue.shift()!, done: false });
|
|
419
|
+
}
|
|
420
|
+
if (closed) {
|
|
421
|
+
return Promise.resolve({ value: undefined as never, done: true });
|
|
422
|
+
}
|
|
423
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
424
|
+
},
|
|
425
|
+
return(): Promise<IteratorResult<ObservedReaction>> {
|
|
426
|
+
close();
|
|
427
|
+
return Promise.resolve({ value: undefined as never, done: true });
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
},
|
|
431
|
+
};
|
|
155
432
|
}
|
|
156
433
|
|
|
157
434
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
435
|
+
* Subscribe to pin/unpin events on `chatId`. Used for
|
|
436
|
+
* progress-card-lifecycle assertions.
|
|
437
|
+
*
|
|
438
|
+
* mtcute doesn't expose a parsed event for `updatePinnedMessages`
|
|
439
|
+
* — it comes through raw. Same shape as `observeReactions`. The
|
|
440
|
+
* raw update carries `messages: number[]` (one or many msg ids
|
|
441
|
+
* pinned/unpinned in one batch) plus a `pinned?: boolean` flag.
|
|
442
|
+
*
|
|
443
|
+
* DM / group / supergroup all supported. `chatId` follows the Bot
|
|
444
|
+
* API marked-id convention; internally we normalize the raw `peer`
|
|
445
|
+
* field with mtcute's `getMarkedPeerId`.
|
|
446
|
+
*
|
|
447
|
+
* Forum-topic filtering (the `threadId` opt) is currently unused
|
|
448
|
+
* here — `updatePinnedMessages` doesn't carry `topMsgId`, only
|
|
449
|
+
* the chat-level peer + message ids. Scenarios that need per-topic
|
|
450
|
+
* pin scoping should filter consumer-side via `driver.getMessage`
|
|
451
|
+
* to look up the pinned message's thread context.
|
|
160
452
|
*/
|
|
161
453
|
observePins(
|
|
162
|
-
|
|
454
|
+
chatId: number,
|
|
163
455
|
_opts?: { threadId?: number },
|
|
164
456
|
): AsyncIterable<ObservedPin> {
|
|
165
|
-
|
|
457
|
+
const c = this.requireClient();
|
|
458
|
+
const queue: ObservedPin[] = [];
|
|
459
|
+
const waiters: Array<(p: IteratorResult<ObservedPin>) => void> = [];
|
|
460
|
+
let closed = false;
|
|
461
|
+
|
|
462
|
+
const dispatch = (p: ObservedPin): void => {
|
|
463
|
+
const w = waiters.shift();
|
|
464
|
+
if (w) w({ value: p, done: false });
|
|
465
|
+
else queue.push(p);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const onRaw = (info: { update: unknown }): void => {
|
|
469
|
+
const u = info.update as {
|
|
470
|
+
_: string;
|
|
471
|
+
pinned?: boolean;
|
|
472
|
+
peer?: unknown;
|
|
473
|
+
messages?: number[];
|
|
474
|
+
};
|
|
475
|
+
if (u._ !== "updatePinnedMessages") return;
|
|
476
|
+
if (!u.peer) return;
|
|
477
|
+
let peerId: number;
|
|
478
|
+
try {
|
|
479
|
+
peerId = getMarkedPeerId(u.peer as Parameters<typeof getMarkedPeerId>[0]);
|
|
480
|
+
} catch {
|
|
481
|
+
return; // unrecognized peer shape
|
|
482
|
+
}
|
|
483
|
+
if (peerId !== chatId) return;
|
|
484
|
+
const ids = u.messages ?? [];
|
|
485
|
+
const pinned = u.pinned !== false; // default-true per TL (`pinned` omitted = pin)
|
|
486
|
+
const date = new Date();
|
|
487
|
+
for (const messageId of ids) {
|
|
488
|
+
dispatch({ chatId, messageId, pinned, date });
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
c.onRawUpdate.add(onRaw);
|
|
493
|
+
|
|
494
|
+
const close = (): void => {
|
|
495
|
+
if (closed) return;
|
|
496
|
+
closed = true;
|
|
497
|
+
c.onRawUpdate.remove(onRaw);
|
|
498
|
+
while (waiters.length > 0) {
|
|
499
|
+
waiters.shift()?.({ value: undefined as never, done: true });
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
[Symbol.asyncIterator](): AsyncIterator<ObservedPin> {
|
|
505
|
+
return {
|
|
506
|
+
next(): Promise<IteratorResult<ObservedPin>> {
|
|
507
|
+
if (queue.length > 0) {
|
|
508
|
+
return Promise.resolve({ value: queue.shift()!, done: false });
|
|
509
|
+
}
|
|
510
|
+
if (closed) {
|
|
511
|
+
return Promise.resolve({ value: undefined as never, done: true });
|
|
512
|
+
}
|
|
513
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
514
|
+
},
|
|
515
|
+
return(): Promise<IteratorResult<ObservedPin>> {
|
|
516
|
+
close();
|
|
517
|
+
return Promise.resolve({ value: undefined as never, done: true });
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Fetch a single message by id. Used by `expectPinnedCard` to grab
|
|
526
|
+
* the card text once a pin event fires (the pin update carries
|
|
527
|
+
* just the id — content has to be looked up separately).
|
|
528
|
+
*
|
|
529
|
+
* Returns `null` when the message doesn't exist or has been
|
|
530
|
+
* deleted between the pin event and this lookup.
|
|
531
|
+
*/
|
|
532
|
+
async getMessage(
|
|
533
|
+
chatId: number,
|
|
534
|
+
messageId: number,
|
|
535
|
+
): Promise<ObservedMessage | null> {
|
|
536
|
+
const c = this.requireClient();
|
|
537
|
+
const results = await c.getMessages(chatId, [messageId]);
|
|
538
|
+
const msg = results[0];
|
|
539
|
+
if (!msg) return null;
|
|
540
|
+
return toObserved(msg, false);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Fetch the inline keyboard attached to a bot message, if any.
|
|
545
|
+
* Returns `null` for messages without an inline_keyboard (or with
|
|
546
|
+
* a non-inline markup like force-reply).
|
|
547
|
+
*
|
|
548
|
+
* Used by vault UX scenarios that need to:
|
|
549
|
+
* 1. Drive the gateway-published vault save card, audit/allow flow,
|
|
550
|
+
* or grant approval card (epic #1012).
|
|
551
|
+
* 2. Find the `[Allow]` / `[Save]` / `[Approve]` button to press by
|
|
552
|
+
* its label, then pass `button.callbackData` to `pressButton`.
|
|
553
|
+
*
|
|
554
|
+
* Callback `data` is decoded from the raw `Uint8Array` to UTF-8
|
|
555
|
+
* string; the gateway always encodes callback_data as ASCII/UTF-8,
|
|
556
|
+
* so this matches Bot API consumers' view. URL buttons surface as
|
|
557
|
+
* `{ text, url }` with `callbackData: undefined`.
|
|
558
|
+
*/
|
|
559
|
+
async getKeyboard(
|
|
560
|
+
chatId: number,
|
|
561
|
+
messageId: number,
|
|
562
|
+
): Promise<ObservedKeyboard | null> {
|
|
563
|
+
const c = this.requireClient();
|
|
564
|
+
const results = await c.getMessages(chatId, [messageId]);
|
|
565
|
+
const msg = results[0] as { markup?: { type: string; buttons: unknown[][] } } | null;
|
|
566
|
+
if (!msg) return null;
|
|
567
|
+
const markup = msg.markup;
|
|
568
|
+
if (!markup || markup.type !== "inline") return null;
|
|
569
|
+
const decoder = new TextDecoder();
|
|
570
|
+
return markup.buttons.map((row) =>
|
|
571
|
+
row.map((b) => {
|
|
572
|
+
const btn = b as {
|
|
573
|
+
_: string;
|
|
574
|
+
text: string;
|
|
575
|
+
data?: Uint8Array;
|
|
576
|
+
url?: string;
|
|
577
|
+
};
|
|
578
|
+
const out: ObservedButton = { text: btn.text };
|
|
579
|
+
if (btn._ === "keyboardButtonCallback" && btn.data) {
|
|
580
|
+
out.callbackData = decoder.decode(btn.data);
|
|
581
|
+
}
|
|
582
|
+
if (btn._ === "keyboardButtonUrl" && btn.url) {
|
|
583
|
+
out.url = btn.url;
|
|
584
|
+
}
|
|
585
|
+
return out;
|
|
586
|
+
}),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Press an inline-keyboard callback button — the MTProto path that
|
|
592
|
+
* mirrors what tapping the button in the Telegram client does.
|
|
593
|
+
*
|
|
594
|
+
* The bot receives a `callback_query` update. Bot-side handlers
|
|
595
|
+
* (e.g. the gateway's vault-audit one-tap allow handler from
|
|
596
|
+
* #969 P2b, or the agent-grant-request flow proposed in #1012)
|
|
597
|
+
* fire as if a real operator tapped.
|
|
598
|
+
*
|
|
599
|
+
* Note: the driver user must be in the bot's admin allowlist for
|
|
600
|
+
* any admin-gated button (most `/vault` callbacks are admin-gated).
|
|
601
|
+
* The harness's `test-harness` agent already includes the driver
|
|
602
|
+
* via `--allow-from` at agent-add time, so admin actions work
|
|
603
|
+
* end-to-end out of the box.
|
|
604
|
+
*/
|
|
605
|
+
async pressButton(
|
|
606
|
+
chatId: number,
|
|
607
|
+
messageId: number,
|
|
608
|
+
callbackData: string,
|
|
609
|
+
): Promise<void> {
|
|
610
|
+
const c = this.requireClient();
|
|
611
|
+
await c.getCallbackAnswer({
|
|
612
|
+
chatId,
|
|
613
|
+
message: messageId,
|
|
614
|
+
data: callbackData,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Send a voice note. Wraps mtcute's `sendMedia` + `InputMedia.voice`
|
|
620
|
+
* factory. The `oggPath` must be a path to an OGG/Opus audio file
|
|
621
|
+
* (Telegram only accepts that codec for voice notes); other audio
|
|
622
|
+
* formats render as a generic audio attachment and `voice_in`
|
|
623
|
+
* transcription on the bot side will skip them.
|
|
624
|
+
*
|
|
625
|
+
* Generating a fixture locally:
|
|
626
|
+
* ffmpeg -f lavfi -i anullsrc=r=48000:cl=mono -t 1 \
|
|
627
|
+
* -c:a libopus -b:a 32k tests/fixtures/voice/silence-1s.opus
|
|
628
|
+
*
|
|
629
|
+
* The scenario at `scenarios/voice-inbound-dm.test.ts` references
|
|
630
|
+
* a fixture path but is `describe.skip`'d until the fixture is
|
|
631
|
+
* committed (kept out of git to keep the repo small until needed).
|
|
632
|
+
*/
|
|
633
|
+
async sendVoice(
|
|
634
|
+
chatId: number,
|
|
635
|
+
oggPath: string,
|
|
636
|
+
opts?: SendTextOptions,
|
|
637
|
+
): Promise<{ messageId: number }> {
|
|
638
|
+
const c = this.requireClient();
|
|
639
|
+
const replyTo = opts?.replyTo ?? opts?.messageThreadId;
|
|
640
|
+
const media = InputMedia.voice(oggPath);
|
|
641
|
+
const sent = await c.sendMedia(
|
|
642
|
+
chatId,
|
|
643
|
+
media,
|
|
644
|
+
replyTo ? { replyTo } : undefined,
|
|
645
|
+
);
|
|
646
|
+
return { messageId: sent.id };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Send or remove an emoji reaction on a target message. Used by the
|
|
651
|
+
* UAT reaction-trigger scenario (#1074) to exercise the gateway's
|
|
652
|
+
* MessageReactionUpdated handler — the driver reacts to a bot reply,
|
|
653
|
+
* the bot's reaction-trigger pipeline synthesizes a new inbound turn
|
|
654
|
+
* to the agent.
|
|
655
|
+
*
|
|
656
|
+
* Pass `emoji: null` to remove the existing reaction (mtcute's
|
|
657
|
+
* `sendReaction` collapses send + remove into one method).
|
|
658
|
+
*/
|
|
659
|
+
async sendReaction(
|
|
660
|
+
chatId: number,
|
|
661
|
+
messageId: number,
|
|
662
|
+
emoji: string | null,
|
|
663
|
+
): Promise<void> {
|
|
664
|
+
const c = this.requireClient();
|
|
665
|
+
await c.sendReaction({
|
|
666
|
+
chatId,
|
|
667
|
+
message: messageId,
|
|
668
|
+
emoji: emoji === null ? null : emoji,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Send a geolocation point. Used by the UAT location-inbound scenario
|
|
674
|
+
* to exercise the gateway's `message:location` handler (#1077).
|
|
675
|
+
*/
|
|
676
|
+
async sendLocation(
|
|
677
|
+
chatId: number,
|
|
678
|
+
latitude: number,
|
|
679
|
+
longitude: number,
|
|
680
|
+
opts?: SendTextOptions,
|
|
681
|
+
): Promise<{ messageId: number }> {
|
|
682
|
+
const c = this.requireClient();
|
|
683
|
+
const replyTo = opts?.replyTo ?? opts?.messageThreadId;
|
|
684
|
+
const media = InputMedia.geo(latitude, longitude);
|
|
685
|
+
const sent = await c.sendMedia(
|
|
686
|
+
chatId,
|
|
687
|
+
media,
|
|
688
|
+
replyTo ? { replyTo } : undefined,
|
|
689
|
+
);
|
|
690
|
+
return { messageId: sent.id };
|
|
166
691
|
}
|
|
167
692
|
|
|
168
693
|
private requireClient(): TelegramClient {
|
|
@@ -172,3 +697,17 @@ export class Driver {
|
|
|
172
697
|
return this.client;
|
|
173
698
|
}
|
|
174
699
|
}
|
|
700
|
+
|
|
701
|
+
function toObserved(msg: Message, edited: boolean): ObservedMessage {
|
|
702
|
+
return {
|
|
703
|
+
chatId: msg.chat.id,
|
|
704
|
+
messageId: msg.id,
|
|
705
|
+
threadId: msg.replyToMessage?.threadId ?? undefined,
|
|
706
|
+
text: msg.text ?? "",
|
|
707
|
+
senderUserId: msg.sender.id,
|
|
708
|
+
fromBot: msg.sender.type === "user" && msg.sender.isBot === true,
|
|
709
|
+
date: msg.date,
|
|
710
|
+
edited,
|
|
711
|
+
silent: msg.isSilent,
|
|
712
|
+
};
|
|
713
|
+
}
|