1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//! Telegram bot command handlers.
//!
//! Why: the four Telegram operations (pair, status, start, stop) live under one
//! discoverable group and benefit from a dedicated file.
//! What: `telegram` dispatcher, `telegram_pair`, `telegram_status`,
//! `telegram_stop`.
//! Test: `cli_parses_telegram_*` in `tests.rs`; handlers are exercised against
//! a live daemon.
use crate::cli::TelegramCmd;
/// `telegram` subcommand — manage the Telegram bot (pair, status, start, stop).
///
/// Why: every Telegram operation belongs under one discoverable command group;
/// this dispatcher routes the [`TelegramCmd`] variants to their handlers.
/// What: `Pair` requests a pairing code, `Status` queries the daemon's pairing
/// state, `Start` runs the bot process in the foreground, `Stop` kills it.
/// Test: variant routing is covered by the `cli_parses_telegram_*` tests; the
/// handlers themselves are exercised against a live daemon.
pub(crate) async fn telegram(url: &str, cmd: TelegramCmd) -> anyhow::Result<()> {
match cmd {
TelegramCmd::Pair => telegram_pair(url).await,
TelegramCmd::Status => telegram_status(url).await,
TelegramCmd::Start {
url: start_url,
token,
alert_chat_id,
allowed_user_id,
check,
} => {
// #2487: `start_url` is `Option<String>` (no clap `default_value`)
// — resolve through the shared `resolve_daemon_url` so an unset
// `--url`/`TRUSTY_MPM_URL` still finds a daemon on an ephemeral
// port via the lock file, matching every other command family.
let resolved_url = trusty_mpm::core::resolve_daemon_url(start_url.as_deref());
let token = token.or_else(|| trusty_mpm::telegram::resolve_token("TELEGRAM_BOT_TOKEN"));
let options = trusty_mpm::telegram::BotOptions {
allowed_user_id,
alert_chat_id,
};
trusty_mpm::telegram::run(resolved_url, token, check, options).await
}
TelegramCmd::Stop => telegram_stop(),
}
}
/// `telegram pair` — request a Telegram-bot pairing code.
///
/// Why: pairing the Telegram bot to this daemon needs an out-of-band shared
/// secret; `tm telegram pair` asks the local daemon for a short code and prints
/// both the `/pair` command and a `t.me` deep link the operator can use.
/// What: calls `POST /pair/request` via the shared [`CommandExecutor`] and
/// prints the code, its TTL, the `/pair <code>` command, and the deep link.
/// Test: covered by the executor's `pair_request_returns_code` test.
async fn telegram_pair(url: &str) -> anyhow::Result<()> {
use trusty_mpm::client::{CommandExecutor, CommandResult};
let executor = CommandExecutor::new(url.to_string());
match executor.pair_request().await {
CommandResult::PairCode {
code,
expires_in_seconds,
} => {
println!("Pairing code: {code}");
println!("Expires in: {} minutes", expires_in_seconds / 60);
println!();
println!("In Telegram, send to your bot:");
println!(" /pair {code}");
println!();
let username = trusty_mpm::telegram::bot_username();
println!("Or click: https://t.me/{username}?start={code}");
}
CommandResult::Error(msg) => eprintln!("pairing failed: {msg}"),
other => eprintln!("unexpected pairing result: {other:?}"),
}
Ok(())
}
/// `telegram status` — show the daemon's Telegram pairing state.
///
/// Why: operators need to know whether a Telegram chat is already paired with
/// the daemon — and which one — without digging through logs.
/// What: calls `GET /pair/status` via the shared [`DaemonClient`] and prints
/// `paired` plus the registered `chat_id`, or `unpaired` when no chat is bound.
/// Test: covered by the client's `pair_status_deserializes` test.
async fn telegram_status(url: &str) -> anyhow::Result<()> {
use trusty_mpm::client::DaemonClient;
let client = DaemonClient::new(url.to_string());
match client.pair_status().await {
Ok(status) if status.paired => match status.chat_id {
Some(chat_id) => println!("Telegram: paired (chat_id: {chat_id})"),
None => println!("Telegram: paired"),
},
Ok(_) => println!("Telegram: unpaired — run `tm telegram pair` to begin"),
Err(e) => eprintln!("daemon unreachable: {e}"),
}
Ok(())
}
/// `telegram stop` — kill the Telegram bot process if one is running.
///
/// Why: the bot may be running standalone (`tm telegram start`) or alongside
/// the daemon; `tm telegram stop` gives the operator a single way to take it
/// down without hunting for the PID.
/// What: runs `pkill -f "trusty-mpm telegram start"` to terminate any
/// foreground bot process; reports whether a process was killed.
/// Test: exercised manually — process management is not unit-testable here.
fn telegram_stop() -> anyhow::Result<()> {
let status = std::process::Command::new("pkill")
.args(["-f", "telegram start"])
.status();
match status {
Ok(s) if s.success() => println!("Telegram bot stopped"),
Ok(_) => println!("no Telegram bot process found"),
Err(e) => eprintln!("failed to stop Telegram bot: {e}"),
}
Ok(())
}