1use chrono::naive::NaiveTime;
11use chrono::{Date, Timelike};
12use chrono_tz::Tz;
13use lazy_static::lazy_static;
14use log::info;
15use regex::Regex;
16use std::collections::HashMap;
17use std::io;
18use std::{thread, time};
19
20#[derive(Debug)]
22struct AgiResult {
23 code: u32,
24 result: i32,
25 data: String,
26}
27
28#[derive(Debug)]
30pub enum MenuItem {
31 File(String),
32 Digits(String),
33 Number(i32),
34 Date(Date<Tz>),
35 Time(NaiveTime),
36}
37
38pub fn pause(seconds: u64) {
40 thread::sleep(time::Duration::from_secs(seconds));
41}
42
43pub fn read_variables() -> io::Result<HashMap<String, String>> {
47 lazy_static! {
48 static ref VAR_RE: Regex = Regex::new("^([^:]+): (.*)$").unwrap();
49 }
50
51 let mut variables: HashMap<String, String> = HashMap::new();
52 loop {
53 let mut line = String::new();
54 let bytes = io::stdin().read_line(&mut line)?;
55 if let Some(capture) = VAR_RE.captures(line.trim()) {
56 variables.insert(
57 capture.get(1).unwrap().as_str().to_string(),
58 capture.get(2).unwrap().as_str().to_string(),
59 );
60 }
61 if bytes <= 2 {
62 return Ok(variables);
63 }
64 }
65}
66
67pub fn get_variable(name: &str) -> io::Result<Option<String>> {
69 let cmd = format!("GET VARIABLE {}", name);
70 let res = send_command(&cmd)?;
71 Ok(Some(res.data))
72}
73
74pub fn set_variable(name: &str, value: &str) -> io::Result<()> {
76 let cmd = format!("SET VARIABLE {} {}", name, value);
77 send_command(&cmd)?;
78 Ok(())
79}
80
81fn parse_result(res: &str) -> Option<AgiResult> {
83 lazy_static! {
84 static ref RES_RE: Regex = Regex::new("^(\\d{3}) result=([^ ]+)( .*)?$").unwrap();
85 }
86
87 RES_RE.captures(res).map(|c| AgiResult {
88 code: c
89 .get(1)
90 .map(|code| code.as_str().parse().unwrap_or(555))
91 .unwrap_or(555),
92 result: c
93 .get(2)
94 .map(|result| result.as_str().parse().unwrap_or(-1))
95 .unwrap_or(-1),
96 data: c
97 .get(3)
98 .map(|data| data.as_str().trim().to_string())
99 .unwrap_or(String::new()),
100 })
101}
102
103fn send_command(cmd: &str) -> io::Result<AgiResult> {
105 println!("{}", cmd);
106
107 let mut result = String::new();
108 io::stdin().read_line(&mut result)?;
109 Ok(parse_result(result.trim()).unwrap_or(AgiResult {
110 code: 556,
111 result: -1,
112 data: String::new(),
113 }))
114}
115
116pub fn answer() -> io::Result<()> {
118 send_command("ANSWER")?;
119 pause(2);
120 Ok(())
121}
122
123pub fn stream_file(file: &str, escape_digits: &str) -> io::Result<Option<char>> {
125 let cmd = format!("STREAM FILE {} \"{}\"", file, escape_digits);
126 let result = send_command(&cmd)?;
127 Ok(if result.result == 0 {
128 None
129 } else {
130 Some(result.result as u8 as char)
131 })
132}
133
134pub fn wait_for_digit(timeout_ms: i32) -> io::Result<Option<char>> {
136 let wait_cmd = format!("WAIT FOR DIGIT {}", timeout_ms);
137 let res = send_command(&wait_cmd)?;
138 Ok(Some(res.result as u32)
139 .filter(|&c| c == 0x2A || c == 0x23 || 0x30 <= c && c <= 0x39)
140 .map(|d| d as u8 as char))
141}
142
143pub fn prompt(msg_file: &str, answer_digits: &str, timeout_ms: i32) -> io::Result<Option<char>> {
145 let res = stream_file(msg_file, answer_digits)?;
146 if res.is_some() {
147 return Ok(res);
148 }
149 Ok(wait_for_digit(timeout_ms)?.filter(|&key| answer_digits.contains(key)))
150}
151
152pub fn prompt_phonenumber(
157 msg_file: &str,
158 timeout_ms: i32,
159 digit_timeout_ms: i32,
160) -> io::Result<Option<String>> {
161 let mut phonenumber = String::new();
162
163 let first_digit = prompt(msg_file, "1234567890", timeout_ms)?;
164 if let Some(digit) = first_digit {
165 phonenumber.push(digit);
166 } else {
167 return Ok(None);
168 }
169
170 'digits: loop {
171 let next_digit = wait_for_digit(digit_timeout_ms)?;
172 match next_digit {
173 None => break 'digits,
174 Some('#') => break 'digits,
175 Some(digit) => phonenumber.push(digit),
176 }
177 }
178
179 Ok(Some(phonenumber))
180}
181
182pub fn menu(
189 messages: &[MenuItem],
190 answer_digits: &str,
191 timeout_ms: i32,
192) -> io::Result<Option<char>> {
193 for message in messages {
194 if let Some(res) = match message {
195 MenuItem::File(f) => stream_file(f, answer_digits)?,
196 MenuItem::Digits(d) => say_digits(&d, answer_digits)?,
197 MenuItem::Number(n) => say_number(*n, answer_digits)?,
198 MenuItem::Date(d) => say_date(d, answer_digits)?,
199 MenuItem::Time(t) => say_time(t, answer_digits)?,
200 } {
201 info!("Menu Result: {}", res);
202 return Ok(Some(res));
203 }
204 }
205 Ok(if timeout_ms != 0 {
206 wait_for_digit(timeout_ms)?.filter(|&key| answer_digits.contains(key))
207 } else {
208 None
209 })
210}
211
212fn get_second_hour_digit(first_digit: u32) -> io::Result<Option<u32>> {
214 let second_digit = wait_for_digit(5000)?
215 .filter(|&c| '0' <= c && c <= '9')
216 .map(|d| (d as u8 - 0x30) as u32);
217 Ok(second_digit
218 .map(|d| first_digit * 10 + d)
219 .filter(|&h| h < 24))
220}
221
222pub fn prompt_hour(msg_file: &str) -> io::Result<Option<u32>> {
225 let res = prompt(msg_file, "1234567890", 5000)?;
226 if let Some(digit) = res {
227 return match digit {
228 '0' => get_second_hour_digit(0),
229 '1' => get_second_hour_digit(1),
230 '2' => get_second_hour_digit(2),
231 '3' => Ok(Some(3)),
232 '4' => Ok(Some(4)),
233 '5' => Ok(Some(5)),
234 '6' => Ok(Some(6)),
235 '7' => Ok(Some(7)),
236 '8' => Ok(Some(8)),
237 '9' => Ok(Some(9)),
238 _ => Ok(None),
239 };
240 } else {
241 return Ok(None);
242 }
243}
244
245pub fn say_digits(digits: &str, escape_digits: &str) -> io::Result<Option<char>> {
247 let cmd = format!("SAY DIGITS {} \"{}\"", digits, escape_digits);
248 let result = send_command(&cmd)?;
249 Ok(if result.result == 0 {
250 None
251 } else {
252 Some(result.result as u8 as char)
253 })
254}
255
256pub fn say_number(number: i32, escape_digits: &str) -> io::Result<Option<char>> {
258 let cmd = format!("SAY NUMBER {} \"{}\"", number, escape_digits);
259 let result = send_command(&cmd)?;
260 Ok(if result.result == 0 {
261 None
262 } else {
263 Some(result.result as u8 as char)
264 })
265}
266
267pub fn say_date(date: &Date<Tz>, escape_digits: &str) -> io::Result<Option<char>> {
269 let seconds_since_1970 = date.and_hms(6, 0, 0).timestamp();
270 info!("{:?} is {} seconds since 1970", date, seconds_since_1970);
271 let cmd = format!(
272 "SAY DATETIME {} \"{}\" dBY Europe/Berlin",
273 seconds_since_1970, escape_digits
274 );
275 let result = send_command(&cmd)?;
276 Ok(if result.result == 0 {
277 None
278 } else {
279 Some(result.result as u8 as char)
280 })
281}
282
283pub fn say_time(time: &NaiveTime, escape_digits: &str) -> io::Result<Option<char>> {
285 let hour = time.hour();
286 let minute = time.minute();
287
288 Ok(say_number(hour as i32, escape_digits)?
289 .or(stream_file("digits/oclock", escape_digits)?)
290 .or(if minute == 0 {
291 None
292 } else {
293 say_number(minute as i32, escape_digits)?
294 }))
295}
296
297pub fn exec(app: &str, options: &str) -> io::Result<()> {
299 let cmd = format!("EXEC {} \"{}\"", app, options);
300 send_command(&cmd)?;
301 Ok(())
302}
303
304#[cfg(test)]
305mod tests {
306 #[test]
307 fn it_works() {
308 assert_eq!(2 + 2, 4);
309 }
310}