1use std::time::Duration;
13
14use crate::channels::{Channel, ChannelSettings, MediaKind, OutgoingMedia, OutgoingMessage};
15
16pub struct StepReport {
18 pub name: &'static str,
19 pub ok: bool,
20 pub detail: String,
21}
22
23impl StepReport {
24 fn pass(name: &'static str, detail: impl Into<String>) -> Self {
25 Self {
26 name,
27 ok: true,
28 detail: detail.into(),
29 }
30 }
31 fn fail(name: &'static str, detail: impl Into<String>) -> Self {
32 Self {
33 name,
34 ok: false,
35 detail: detail.into(),
36 }
37 }
38
39 fn print(&self) {
40 let mark = if self.ok { "ok" } else { "FAIL" };
41 println!(" [{mark}] {} — {}", self.name, self.detail);
42 }
43}
44
45pub struct CheckOptions {
47 pub chat_id: Option<String>,
48 pub wait: Duration,
49 pub media: bool,
50}
51
52pub async fn run(
55 channel: &mut dyn Channel,
56 settings: &ChannelSettings,
57 opts: &CheckOptions,
58) -> Vec<StepReport> {
59 let mut steps = Vec::new();
60 let name = channel.name().to_string();
61
62 let nonce = format!("apollo-check-{}", uuid::Uuid::new_v4().simple());
65
66 let rx = match channel.start().await {
67 Ok(rx) => {
68 steps.push(StepReport::pass("start", "receiver opened"));
69 Some(rx)
70 }
71 Err(e) => {
72 steps.push(StepReport::fail("start", e.to_string()));
73 None
74 }
75 };
76 steps.last().unwrap().print();
77
78 let chat_id = opts
79 .chat_id
80 .clone()
81 .or_else(|| settings.get("chat_id"))
82 .or_else(|| settings.get("channel_id"))
83 .unwrap_or_default();
84
85 let sent = channel
87 .send(OutgoingMessage {
88 chat_id: chat_id.clone(),
89 text: format!("{name} check — please reply with: {nonce}"),
90 reply_to: None,
91 })
92 .await;
93 steps.push(match &sent {
94 Ok(id) => StepReport::pass(
95 "send",
96 match id {
97 Some(id) => format!("delivered, id {id}"),
98 None => "delivered".to_string(),
99 },
100 ),
101 Err(e) => StepReport::fail("send", e.to_string()),
102 });
103 steps.last().unwrap().print();
104
105 if opts.media {
107 if channel.supports_media() {
108 let path = std::env::temp_dir().join("apollo-channel-check.txt");
109 let step = match tokio::fs::write(&path, b"apollo channel-check attachment\n").await {
110 Err(e) => StepReport::fail("media", format!("could not write temp file: {e}")),
111 Ok(()) => {
112 let media = OutgoingMedia::file(&chat_id, MediaKind::Document, &path)
113 .with_caption("apollo channel-check attachment");
114 match channel.send_media(media).await {
115 Ok(_) => StepReport::pass("media", "attachment uploaded"),
116 Err(e) => StepReport::fail("media", e.to_string()),
117 }
118 }
119 };
120 let _ = tokio::fs::remove_file(&path).await;
121 steps.push(step);
122 } else {
123 steps.push(StepReport::pass(
124 "media",
125 "channel reports no media support — skipped",
126 ));
127 }
128 steps.last().unwrap().print();
129 }
130
131 if let Some(mut rx) = rx {
133 println!(
134 " .... waiting {}s for you to reply with the nonce",
135 opts.wait.as_secs()
136 );
137 let deadline = tokio::time::Instant::now() + opts.wait;
138 let step = loop {
139 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
140 if remaining.is_zero() {
141 break StepReport::fail(
142 "receive",
143 format!("no message containing {nonce} within the timeout"),
144 );
145 }
146 match tokio::time::timeout(remaining, rx.recv()).await {
147 Ok(Some(msg)) if msg.text.contains(&nonce) => {
148 break StepReport::pass(
149 "receive",
150 format!("echo from {} in chat {}", msg.sender_id, msg.chat_id),
151 );
152 }
153 Ok(Some(_)) => continue,
155 Ok(None) => {
156 break StepReport::fail("receive", "receiver closed before the echo arrived")
157 }
158 Err(_) => {
159 break StepReport::fail(
160 "receive",
161 format!("no message containing {nonce} within the timeout"),
162 )
163 }
164 }
165 };
166 steps.push(step);
167 steps.last().unwrap().print();
168 }
169
170 let _ = channel.stop().await;
171 steps
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::channels::IncomingMessage;
178 use async_trait::async_trait;
179 use tokio::sync::mpsc;
180
181 struct EchoChannel {
184 tx: Option<mpsc::Sender<IncomingMessage>>,
185 }
186
187 #[async_trait]
188 impl Channel for EchoChannel {
189 fn name(&self) -> &str {
190 "echo"
191 }
192 async fn start(&mut self) -> anyhow::Result<mpsc::Receiver<IncomingMessage>> {
193 let (tx, rx) = mpsc::channel(4);
194 self.tx = Some(tx);
195 Ok(rx)
196 }
197 async fn send(&self, message: OutgoingMessage) -> anyhow::Result<Option<String>> {
198 if let Some(tx) = &self.tx {
199 let _ = tx
200 .send(IncomingMessage {
201 id: "1".into(),
202 sender_id: "u".into(),
203 sender_name: None,
204 chat_id: message.chat_id.clone(),
205 text: message.text.clone(),
206 is_group: false,
207 reply_to: None,
208 timestamp: chrono::Utc::now(),
209 })
210 .await;
211 }
212 Ok(Some("m1".into()))
213 }
214 async fn stop(&mut self) -> anyhow::Result<()> {
215 Ok(())
216 }
217 }
218
219 #[tokio::test]
220 async fn a_working_channel_passes_every_step() {
221 let mut channel = EchoChannel { tx: None };
222 let steps = run(
223 &mut channel,
224 &ChannelSettings::default(),
225 &CheckOptions {
226 chat_id: Some("c1".into()),
227 wait: Duration::from_secs(5),
228 media: false,
229 },
230 )
231 .await;
232
233 assert!(
234 steps.iter().all(|s| s.ok),
235 "{:?}",
236 steps
237 .iter()
238 .map(|s| (s.name, s.ok, &s.detail))
239 .collect::<Vec<_>>()
240 );
241 assert!(steps.iter().any(|s| s.name == "receive"));
242 }
243
244 struct SilentChannel;
247
248 #[async_trait]
249 impl Channel for SilentChannel {
250 fn name(&self) -> &str {
251 "silent"
252 }
253 async fn start(&mut self) -> anyhow::Result<mpsc::Receiver<IncomingMessage>> {
254 let (_tx, rx) = mpsc::channel(1);
255 Ok(rx)
256 }
257 async fn send(&self, _message: OutgoingMessage) -> anyhow::Result<Option<String>> {
258 Ok(None)
259 }
260 async fn stop(&mut self) -> anyhow::Result<()> {
261 Ok(())
262 }
263 }
264
265 #[tokio::test]
266 async fn a_deaf_channel_fails_the_receive_step() {
267 let mut channel = SilentChannel;
268 let steps = run(
269 &mut channel,
270 &ChannelSettings::default(),
271 &CheckOptions {
272 chat_id: Some("c1".into()),
273 wait: Duration::from_millis(200),
274 media: false,
275 },
276 )
277 .await;
278
279 let receive = steps.iter().find(|s| s.name == "receive").unwrap();
280 assert!(!receive.ok, "a deaf channel must not pass");
281 assert!(steps.iter().find(|s| s.name == "send").unwrap().ok);
282 }
283}