Skip to main content

oxios_telegram/
format.rs

1//! Telegram response formatter.
2
3use oxios_gateway::format::ChannelFormatter;
4use oxios_gateway::message::{ErrorKind, OutgoingMessage};
5
6/// Telegram-specific response formatter.
7///
8/// Formats outgoing messages for Telegram with Markdown-compatible
9/// metadata footer and emoji indicators.
10pub struct TelegramFormatter;
11
12impl ChannelFormatter for TelegramFormatter {
13    fn format_success(&self, msg: &OutgoingMessage) -> String {
14        let mut out = msg.content.clone();
15
16        if let Some(meta) = &msg.meta {
17            let mut footer_parts = Vec::new();
18            if !meta.phase.is_empty() {
19                let eval = if meta.evaluation_passed {
20                    "βœ…"
21                } else {
22                    "⚠️"
23                };
24                footer_parts.push(format!("{} {}", eval, meta.phase));
25            }
26            if let Some(tag) = &meta.project_tag {
27                footer_parts.push(tag.clone());
28            }
29            if let Some(dur) = meta.duration_ms {
30                footer_parts.push(format!("{:.1}s", dur as f64 / 1000.0));
31            }
32            if !footer_parts.is_empty() {
33                out.push_str(&format!("\n\n_{}_", footer_parts.join(" Β· ")));
34            }
35        }
36
37        out
38    }
39
40    fn format_error(&self, msg: &OutgoingMessage) -> String {
41        let meta = msg.meta.as_ref();
42        let kind = meta.and_then(|m| m.error.as_ref()).map(|e| e.kind);
43
44        let icon = match kind {
45            Some(ErrorKind::ProviderError) => "πŸ”Œ",
46            Some(ErrorKind::Timeout) => "⏱️",
47            _ => "❌",
48        };
49
50        let mut out = format!("{} {}", icon, msg.content);
51
52        if let Some(err) = meta.and_then(|m| m.error.as_ref()) {
53            if let Some(s) = &err.suggestion {
54                out.push_str(&format!("\n\nπŸ’‘ _{s}_"));
55            }
56        }
57
58        out
59    }
60
61    fn format_progress(&self, phase: &str) -> String {
62        match phase {
63            "Interview" => "πŸ” 뢄석 쀑...",
64            "Seed" => "πŸ“‹ κ³„νš 수립 쀑...",
65            "Execute" => "⚑ μ‹€ν–‰ 쀑...",
66            "Evaluate" => "πŸ“Š 평가 쀑...",
67            "Evolve" => "πŸ”„ κ°œμ„  쀑...",
68            _ => "⏳ 처리 쀑...",
69        }
70        .into()
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use oxios_gateway::message::{ResponseMeta, UserFacingError};
78    use std::collections::HashMap;
79
80    fn make_msg(content: &str, meta: Option<ResponseMeta>) -> OutgoingMessage {
81        OutgoingMessage {
82            id: uuid::Uuid::new_v4(),
83            channel: "telegram".to_string(),
84            user_id: "123".to_string(),
85            content: content.to_string(),
86            timestamp: chrono::Utc::now(),
87            metadata: HashMap::new(),
88            meta,
89        }
90    }
91
92    #[test]
93    fn format_success_no_meta() {
94        let msg = make_msg("Hello", None);
95        let fmt = TelegramFormatter;
96        assert_eq!(fmt.format_success(&msg), "Hello");
97    }
98
99    #[test]
100    fn format_success_with_phase() {
101        let meta = ResponseMeta {
102            session_id: None,
103            project_id: None,
104            project_tag: Some("[πŸ”§ Test]".to_string()),
105            seed_id: None,
106            phase: "Execute".to_string(),
107            evaluation_passed: true,
108            duration_ms: Some(3500),
109            error: None,
110        };
111        let msg = make_msg("Done!", Some(meta));
112        let fmt = TelegramFormatter;
113        let result = fmt.format_success(&msg);
114        assert!(result.contains("Done!"));
115        assert!(result.contains("βœ… Execute"));
116        assert!(result.contains("[πŸ”§ Test]"));
117        assert!(result.contains("3.5s"));
118    }
119
120    #[test]
121    fn format_error_internal() {
122        let meta = ResponseMeta {
123            session_id: None,
124            project_id: None,
125            project_tag: None,
126            seed_id: None,
127            phase: String::new(),
128            evaluation_passed: false,
129            duration_ms: None,
130            error: Some(UserFacingError {
131                message: "λ‚΄λΆ€ 였λ₯˜".to_string(),
132                kind: ErrorKind::Internal,
133                suggestion: None,
134            }),
135        };
136        let msg = make_msg("λ‚΄λΆ€ 였λ₯˜", Some(meta));
137        let fmt = TelegramFormatter;
138        let result = fmt.format_error(&msg);
139        assert!(result.starts_with("❌"));
140    }
141
142    #[test]
143    fn format_error_provider_with_suggestion() {
144        let meta = ResponseMeta {
145            session_id: None,
146            project_id: None,
147            project_tag: None,
148            seed_id: None,
149            phase: String::new(),
150            evaluation_passed: false,
151            duration_ms: None,
152            error: Some(UserFacingError {
153                message: "AI μ„œλΉ„μŠ€ 였λ₯˜".to_string(),
154                kind: ErrorKind::ProviderError,
155                suggestion: Some("1-2λΆ„ ν›„ λ‹€μ‹œ μ‹œλ„ν•˜μ„Έμš”.".to_string()),
156            }),
157        };
158        let msg = make_msg("AI μ„œλΉ„μŠ€ 였λ₯˜", Some(meta));
159        let fmt = TelegramFormatter;
160        let result = fmt.format_error(&msg);
161        assert!(result.starts_with("πŸ”Œ"));
162        assert!(result.contains("πŸ’‘"));
163    }
164
165    #[test]
166    fn format_progress_known_phases() {
167        let fmt = TelegramFormatter;
168        assert_eq!(fmt.format_progress("Interview"), "πŸ” 뢄석 쀑...");
169        assert_eq!(fmt.format_progress("Execute"), "⚑ μ‹€ν–‰ 쀑...");
170        assert_eq!(fmt.format_progress("Evolve"), "πŸ”„ κ°œμ„  쀑...");
171    }
172
173    #[test]
174    fn format_progress_unknown() {
175        let fmt = TelegramFormatter;
176        assert_eq!(fmt.format_progress("Unknown"), "⏳ 처리 쀑...");
177    }
178}