Skip to content

Instantly share code, notes, and snippets.

@rtsuk
Created October 31, 2020 17:43
Show Gist options
  • Select an option

  • Save rtsuk/39e7ded637326a5605c6c08ec0ff4d30 to your computer and use it in GitHub Desktop.

Select an option

Save rtsuk/39e7ded637326a5605c6c08ec0ff4d30 to your computer and use it in GitHub Desktop.

Revisions

  1. rtsuk created this gist Oct 31, 2020.
    105 changes: 105 additions & 0 deletions asterisk.rs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,105 @@
    use anyhow::{bail, ensure, format_err, Error};
    use itertools::Itertools;
    use std::{collections::HashMap, time::Duration};
    use telnet::{Telnet, TelnetEvent};
    use uuid::Uuid;

    #[derive(Debug)]
    pub struct Response {
    pub raw_response: String,
    action_id: String,
    }

    impl Response {
    fn new(raw_response: String, fields: HashMap<String, String>) -> Result<Response, Error> {
    let action_id = fields
    .get("ActionID")
    .ok_or_else(|| format_err!("Response missing ActionID field"))?;
    Ok(Self {
    raw_response,
    action_id: action_id.to_string(),
    })
    }
    }

    pub struct Ami {
    connection: Telnet,
    }

    const BUFFER_SIZE: usize = 32 * 1024;

    impl Ami {
    fn read_data(connection: &mut Telnet) -> Result<String, Error> {
    let mut result = String::new();
    loop {
    let event = connection.read_timeout(Duration::from_millis(100))?;
    match event {
    TelnetEvent::Data(data) => {
    result.push_str(&String::from_utf8(data.to_vec())?);
    }
    TelnetEvent::Error(err) => {
    bail!("telnet error: {}", err);
    }
    TelnetEvent::TimedOut => {
    return Ok(result);
    }
    _ => println!("{:?}", event),
    }
    }
    }

    pub fn new(host: &str, port: u16) -> Result<Ami, Error> {
    let mut connection = Telnet::connect((host, port), BUFFER_SIZE)?;
    let data = Self::read_data(&mut connection)?.trim().to_string();
    ensure!(
    data == "Asterisk Call Manager/1.3",
    "Unexpected response from AMI"
    );

    Ok(Self { connection })
    }

    fn execute(&mut self, command: &str, options: &[(&str, &str)]) -> Result<Response, Error> {
    let my_uuid = Uuid::new_v4();
    let commands: Vec<String> = [
    format!("Action: {}", command),
    format!("ActionID: {}", my_uuid),
    ]
    .iter()
    .map(|s| s.clone())
    .chain(options.iter().map(|(k, v)| format!("{}: {}", k, v)))
    .chain(std::iter::once(String::from("")))
    .collect();
    let mut command_string = commands.join("\r\n");
    command_string.push_str("\r\n");
    self.connection.write(command_string.as_bytes())?;
    let result_text = Self::read_data(&mut self.connection)?;
    let result: HashMap<String, String> = result_text
    .split("\r\n")
    .map(|s| s.split(": ").next_tuple().unwrap_or(("", "")))
    .map(|(k, v)| {
    if k.len() > 0 {
    Some((k.to_string(), v.to_string()))
    } else {
    None
    }
    })
    .filter_map(|t| t)
    .collect();
    Response::new(result_text, result)
    }

    pub fn login(&mut self, username: &str, password: &str) -> Result<Response, Error> {
    let options = [
    ("Username", username),
    ("Secret", password),
    ("Event", "Off"),
    ];
    self.execute("Login", &options)
    }

    pub fn command(&mut self, command: &str) -> Result<Response, Error> {
    self.execute("Command", &[("Command", command)])
    }
    }
    // execute 'Login', {'Username' => username, 'Secret' => password, 'Event' => 'On'}