|
|
|
|
|
|
|
|
|
|
|
use core::time; |
|
|
use std::collections::*; |
|
|
use std::collections::*; |
|
|
use std::path::PathBuf; |
|
|
use std::path::PathBuf; |
|
|
use std::sync::Arc; |
|
|
use std::sync::Arc; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
use tokio::fs as tokiofs; |
|
|
use tokio::fs as tokiofs; |
|
|
use tokio::prelude::*; |
|
|
use tokio::prelude::*; |
|
|
|
|
|
use tokio::sync::mpsc; |
|
|
|
|
|
|
|
|
use tokio_inotify; |
|
|
use tokio_inotify; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
use ruma_events::{self, room::message::*}; |
|
|
use ruma_events::{self, room::message::*}; |
|
|
use ruma_identifiers::RoomId; |
|
|
use ruma_identifiers::RoomId; |
|
|
|
|
|
|
|
|
use crate::config::*; |
|
|
|
|
|
|
|
|
use crate::config::{self, *}; |
|
|
|
|
|
use crate::sender; |
|
|
|
|
|
|
|
|
type MatrixClient = Client<HttpsConnector<HttpConnector<GaiResolver>>>; |
|
|
type MatrixClient = Client<HttpsConnector<HttpConnector<GaiResolver>>>; |
|
|
type MessageRequest = rumamessage::create_message_event::Request; |
|
|
type MessageRequest = rumamessage::create_message_event::Request; |
|
|
|
|
|
|
|
|
pub enum Error { |
|
|
pub enum Error { |
|
|
FileFormatMismatch, |
|
|
FileFormatMismatch, |
|
|
BadUrl, |
|
|
BadUrl, |
|
|
|
|
|
UnspecifiedClient(String), |
|
|
Io(io::Error), |
|
|
Io(io::Error), |
|
|
MtxClient(ruma_client::Error), |
|
|
MtxClient(ruma_client::Error), |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
watch_path: PathBuf, |
|
|
watch_path: PathBuf, |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
pub async fn start_spooling(conf: Config) -> Result<(), Error> { |
|
|
|
|
|
let ain = tokio_inotify::AsyncINotify::init().expect("inotify init"); |
|
|
|
|
|
|
|
|
|
|
|
let mut clients = HashMap::new(); |
|
|
|
|
|
let mut watch_map = HashMap::new(); |
|
|
|
|
|
|
|
|
|
|
|
for s in conf.spool_dirs { |
|
|
|
|
|
let label = s.sender_acct_label; |
|
|
|
|
|
let w = ain.add_watch(&s.path, IN_CLOSE_WRITE | IN_MOVED_TO)?; |
|
|
|
|
|
println!("Added watch: {}", s.path.to_str().unwrap_or("[non-UTF-8]")); |
|
|
|
|
|
|
|
|
|
|
|
// TODO Make this better. |
|
|
|
|
|
let cli = if !clients.contains_key(&label) { |
|
|
|
|
|
let acc = conf.accounts.get(&label).expect("missing account"); |
|
|
|
|
|
let cli = create_and_auth_client(acc.clone()).await?; |
|
|
|
|
|
clients.insert(label, cli.clone()); |
|
|
|
|
|
cli |
|
|
|
|
|
} else { |
|
|
|
|
|
clients.get(&label).expect("missing account").clone() |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
let sa = SpoolAction { |
|
|
|
|
|
client: cli, |
|
|
|
|
|
room: s.dest_room_id, |
|
|
|
|
|
delay_secs: s.send_delay_sec, |
|
|
|
|
|
watch_path: s.path.clone(), |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
watch_map.insert(w, sa); |
|
|
|
|
|
|
|
|
pub async fn start_spoolers( |
|
|
|
|
|
conf: Config, |
|
|
|
|
|
client_chans: HashMap<String, mpsc::Sender<sender::Message>>, |
|
|
|
|
|
) -> Result<(), Error> { |
|
|
|
|
|
for sd in conf.spool_dirs { |
|
|
|
|
|
let chan = client_chans |
|
|
|
|
|
.get(&sd.sender_acct_label) |
|
|
|
|
|
.ok_or_else(|| Error::UnspecifiedClient(sd.sender_acct_label.clone()))?; |
|
|
|
|
|
|
|
|
|
|
|
let ain = tokio_inotify::AsyncINotify::init().expect("spool: inotify init"); |
|
|
|
|
|
|
|
|
|
|
|
let w = ain.add_watch(&sd.path, IN_CLOSE_WRITE | IN_MOVED_TO)?; |
|
|
|
|
|
println!("[spool] added watch: {:?}", sd.path); |
|
|
|
|
|
|
|
|
|
|
|
tokio::spawn(do_watch_dir(ain, sd, chan.clone())); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let watch_map = Arc::new(watch_map); |
|
|
|
|
|
|
|
|
|
|
|
// This is horrible. |
|
|
|
|
|
let _ = ain |
|
|
|
|
|
.compat() |
|
|
|
|
|
.map_err(Error::from) |
|
|
|
|
|
.try_for_each({ |
|
|
|
|
|
|e| { |
|
|
|
|
|
// Not sure why we have to do this clone here outside. |
|
|
|
|
|
let wm = watch_map.clone(); |
|
|
|
|
|
async move { |
|
|
|
|
|
// I don't like these clones but idk any better way. |
|
|
|
|
|
let act = match wm.as_ref().get(&e.wd) { |
|
|
|
|
|
Some(a) => a.clone(), |
|
|
|
|
|
None => { |
|
|
|
|
|
println!( |
|
|
|
|
|
"got a wd that was not from a watch we added, ignoring: {:?}", |
|
|
|
|
|
e.wd |
|
|
|
|
|
); |
|
|
|
|
|
return Ok(()); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
//let pb = e.name.clone(); |
|
|
|
|
|
|
|
|
|
|
|
// TODO This should be spawning a new task. |
|
|
|
|
|
//tokio::spawn(async move { |
|
|
|
|
|
// TODO Respect delay. |
|
|
|
|
|
match process_file(&e.name, &act).await { |
|
|
|
|
|
Ok(()) => {} // ok I guess? |
|
|
|
|
|
Err(e) => println!("Error processing file: {:?}", e), |
|
|
|
|
|
} |
|
|
|
|
|
//}); |
|
|
|
|
|
|
|
|
Ok(()) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
Ok(()) |
|
|
|
|
|
|
|
|
async fn do_watch_dir( |
|
|
|
|
|
inot: tokio_inotify::AsyncINotify, |
|
|
|
|
|
sdc: config::SpoolDir, |
|
|
|
|
|
mut dest: mpsc::Sender<sender::Message>, |
|
|
|
|
|
) { |
|
|
|
|
|
let mut iter = inot.compat(); |
|
|
|
|
|
while let Some(ent) = iter.next().await { |
|
|
|
|
|
match ent { |
|
|
|
|
|
Ok(ent) => { |
|
|
|
|
|
// Just succ up the file and send it over. We'll do the |
|
|
|
|
|
// formatting later. |
|
|
|
|
|
let mut real_path = sdc.path.clone(); |
|
|
|
|
|
real_path.push(ent.name); |
|
|
|
|
|
let s = match file_as_string(&real_path).await { |
|
|
|
|
|
Ok(s) => s, |
|
|
|
|
|
Err(e) => { |
|
|
|
|
|
eprintln!( |
|
|
|
|
|
"[spool] warning, could not read file, ignoring: {:?}", |
|
|
|
|
|
real_path |
|
|
|
|
|
); |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
let msg = |
|
|
|
|
|
sender::Message::new_delay(sdc.dest_room_id.clone(), s, sdc.send_delay_sec); |
|
|
|
|
|
let tout = time::Duration::from_secs(30); |
|
|
|
|
|
dest.send_timeout(msg, tout) |
|
|
|
|
|
.map_err(|_| ()) |
|
|
|
|
|
.await |
|
|
|
|
|
.expect("spool: relay channel send timeout"); |
|
|
|
|
|
|
|
|
|
|
|
if let Err(e) = tokiofs::remove_file(&real_path).await { |
|
|
|
|
|
eprintln!( |
|
|
|
|
|
"[spool] warning: could not remove sent file, ignoring: {:?}", |
|
|
|
|
|
real_path |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}) |
|
|
|
|
|
.await?; |
|
|
|
|
|
|
|
|
|
|
|
Ok(()) |
|
|
|
|
|
|
|
|
Err(e) => panic!("spool: error reading watch {:?}", e), |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* |
|
|
async fn process_file(p: &PathBuf, sa: &SpoolAction) -> Result<(), Error> { |
|
|
async fn process_file(p: &PathBuf, sa: &SpoolAction) -> Result<(), Error> { |
|
|
let ext = match p.extension().map(|e| e.to_str()).flatten() { |
|
|
let ext = match p.extension().map(|e| e.to_str()).flatten() { |
|
|
Some(v) => v, |
|
|
Some(v) => v, |
|
|
None => { |
|
|
None => { |
|
|
println!("Found weird file {:?}, ignoring", p); |
|
|
|
|
|
|
|
|
eprintln!("Found weird file {:?}, ignoring", p); |
|
|
return Ok(()); |
|
|
return Ok(()); |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
match sa.client.as_ref().request(req).await { |
|
|
match sa.client.as_ref().request(req).await { |
|
|
Ok(_) => { |
|
|
Ok(_) => { |
|
|
// Now delete it if it passed. |
|
|
// Now delete it if it passed. |
|
|
tokiofs::remove_file(real_path).await?; |
|
|
|
|
|
} |
|
|
} |
|
|
Err(e) => println!("Error processing {}: {:?}", name, e), |
|
|
Err(e) => println!("Error processing {}: {:?}", name, e), |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
Ok(()) |
|
|
Ok(()) |
|
|
} |
|
|
|
|
|
|
|
|
}*/ |
|
|
|
|
|
|
|
|
async fn file_as_string(p: &PathBuf) -> Result<String, Error> { |
|
|
async fn file_as_string(p: &PathBuf) -> Result<String, Error> { |
|
|
let mut buf = Vec::new(); |
|
|
let mut buf = Vec::new(); |
|
|
|
|
|
|
|
|
f.read_to_end(&mut buf).await?; |
|
|
f.read_to_end(&mut buf).await?; |
|
|
String::from_utf8(buf).map_err(|_| Error::FileFormatMismatch) |
|
|
String::from_utf8(buf).map_err(|_| Error::FileFormatMismatch) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async fn create_and_auth_client(acct: Account) -> Result<Arc<MatrixClient>, Error> { |
|
|
|
|
|
let hs_url = Url::parse(&acct.homeserver).map_err(|_| Error::BadUrl)?; |
|
|
|
|
|
let c = MatrixClient::https(hs_url, None); |
|
|
|
|
|
match acct.auth { |
|
|
|
|
|
Auth::UsernamePass(un, pw) => c.log_in(un, pw, acct.device_id, acct.display).await?, |
|
|
|
|
|
}; |
|
|
|
|
|
Ok(Arc::new(c)) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn make_text_request<R: rand::Rng>(room_id: RoomId, msg: &str, rng: &mut R) -> MessageRequest { |
|
|
|
|
|
let inner = TextMessageEventContent { |
|
|
|
|
|
body: String::from(msg), |
|
|
|
|
|
format: None, |
|
|
|
|
|
formatted_body: None, |
|
|
|
|
|
relates_to: None, |
|
|
|
|
|
}; |
|
|
|
|
|
let mec = MessageEventContent::Text(inner); |
|
|
|
|
|
MessageRequest { |
|
|
|
|
|
room_id: room_id, |
|
|
|
|
|
event_type: ruma_events::EventType::RoomMessage, |
|
|
|
|
|
txn_id: make_txn_id(rng), |
|
|
|
|
|
data: mec, |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const TXN_ID_LEN: usize = 20; |
|
|
|
|
|
|
|
|
|
|
|
fn make_txn_id<R: rand::Rng>(rng: &mut R) -> String { |
|
|
|
|
|
let mut buf = String::with_capacity(TXN_ID_LEN); |
|
|
|
|
|
for _ in 0..TXN_ID_LEN { |
|
|
|
|
|
buf.push((rng.gen_range(0, 26) + ('a' as u8)) as char); |
|
|
|
|
|
} |
|
|
|
|
|
buf |
|
|
|
|
|
} |
|
|
|