1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
use super::ApiError;
use crate::keybase_cmd;
use hex;
use rand::prelude::*;
use std::{
  env, fmt, fs,
  io::{self, Read},
  path::{Path, PathBuf},
  process::{self, Command, Stdio},
  thread::JoinHandle,
};

pub struct Bot {
  pub username: String,
  pub keybase_path: PathBuf,
  pub home_dir: PathBuf,
  service_process: process::Child,
  pub listen_threads: Vec<JoinHandle<()>>,
}

impl fmt::Debug for Bot {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(
      f,
      "Bot {{ username: {}, home_dir: {:?}, listen_threads: {} }}",
      self.username,
      self.home_dir,
      self.listen_threads.len()
    )
  }
}

// TODO
// 1. Copy binary to working dir
// 2. Login

#[derive(Debug)]
pub enum BotError {
  IO(io::Error),
  ApiErr(ApiError),
}

impl From<io::Error> for BotError {
  fn from(err: io::Error) -> Self {
    BotError::IO(err)
  }
}

impl From<ApiError> for BotError {
  fn from(err: ApiError) -> Self {
    BotError::ApiErr(err)
  }
}

fn start_service(keybase_path: &Path, home_dir: &Path) -> Result<process::Child, io::Error> {
  let mut child = Command::new(keybase_path)
    .arg("--home")
    .arg(home_dir.to_str().unwrap())
    .arg("service")
    .stderr(Stdio::piped())
    .spawn()?;

  let stderr = child.stderr.as_mut().expect("No stderr on service");
  let mut buf = vec![0u8];
  stderr.read_exact(&mut buf)?;
  Ok(child)
}

impl Bot {
  pub fn new<S>(username: S, paperkey: S) -> Result<Bot, BotError>
  where
    S: Into<String>,
  {
    let username: String = username.into();
    let paperkey: String = paperkey.into();
    let mut working_dir = env::temp_dir();
    let mut rng = rand::thread_rng();
    let mut hex_id = vec![0u8; 16];
    rng.fill_bytes(&mut hex_id);
    let hex_id = hex::encode(hex_id);

    // Tempory folder for bot
    working_dir.push(format!("keybase_bot_{}", hex_id));
    fs::create_dir(&working_dir)?;

    // Copy keybase path
    let mut keybase_path = working_dir.clone();
    keybase_path.push("keybase");
    fs::copy(keybase_cmd::which_keybase(), &keybase_path)?;

    // Start KB service
    let mut service_process = start_service(&keybase_path, &working_dir)?;

    // Login
    if let Err(e) = keybase_cmd::login_oneshot(&keybase_path, &working_dir, &username, &paperkey) {
      println!("Login failed");
      service_process.kill()?;
      return Err(e.into());
    }

    Ok(Bot {
      username,
      keybase_path,
      home_dir: working_dir,
      service_process,
      listen_threads: vec![],
    })
  }
}

impl Drop for Bot {
  fn drop(&mut self) {
    self
      .service_process
      .kill()
      .expect("Failed to stop Keybase service");
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use crate::chat::{self, Chat};

  #[test]
  fn start_bot() {
    let bot = Bot::new(
      "pkt0",
      option_env!("PAPERKEY").expect("Missing PAPERKEY env"),
    )
    .unwrap();
    println!("Bot is {:?}", bot);

    let channel = chat::ChannelParams {
      name: format!("{},{}", "pkt0", "marcopolo"),
      ..Default::default()
    };
    let res = bot
      .send_msg(&channel, "hello world")
      .expect("Couldn't send message");
    println!("Result is {:?}", res);
  }

}