asterisk_agi/
lib.rs

1//! This crate helps to implement simple AGI programs.
2//! AGI is the Asterisk Gateway Interface
3//!
4//! When using this crate the first thing you probably have
5//! to do is calling `read_variables()` which will read the
6//! variables Asterisk provides to the AGI program. After that
7//! use the other functions to control what Asterisk is
8//! doing with the invocing channel.
9
10use 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/// Represents a result code from the AGI
21#[derive(Debug)]
22struct AgiResult {
23    code: u32,
24    result: i32,
25    data: String,
26}
27
28/// Represents an item presented to the caller in a voice menu.
29#[derive(Debug)]
30pub enum MenuItem {
31    File(String),
32    Digits(String),
33    Number(i32),
34    Date(Date<Tz>),
35    Time(NaiveTime),
36}
37
38/// Pause further execution for the given number of seconds.
39pub fn pause(seconds: u64) {
40    thread::sleep(time::Duration::from_secs(seconds));
41}
42
43/// Read the variables given by Asterisk when it calls an AGI program.
44///
45/// Typically this is the first call to this crate in an AGI program.
46pub 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
67/// Get the content of an Asterisk variable
68pub 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
74/// Set an Asterisk variable
75pub 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
81/// Parse the result of a command sent to Asterisk
82fn 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
103/// Send a command to Asterisk
104fn 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
116/// Answer a call
117pub fn answer() -> io::Result<()> {
118    send_command("ANSWER")?;
119    pause(2);
120    Ok(())
121}
122
123/// Play an audio file to the caller
124pub 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
134/// Wait for the caller to press a key
135pub 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
143/// Play an audio file and wait for digits to be pressed by the caller
144pub 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
152/// Play an audio file and wait for a phone number to be entered by the caller
153///
154/// Entering the phone number either ends by the given `digit_timeout_ms` or
155/// by the caller pressing the # key.
156pub 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
182/// Present a voice menu to the caller.
183///
184/// A voice menu may consist of multiple things to be played to
185/// the caller. In contrast to playing all the items with individual
186/// comands, the caller is able to press any of the keys in `answer_digits`
187/// during the menu is played.
188pub 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
212/// Get the optional second digit of the hour
213fn 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
222/// Plays a sound file to the caller and then waits for an hour to be
223/// chosen
224pub 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
245/// Plays the individual digits of a number
246pub 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
256/// Plays a number
257pub 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
267/// Plays the date
268pub 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
283/// Plays the time
284pub 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
297/// Executes a dialplan application
298pub 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}