//! This is a CLI program and a little testing frame. This file must not be //! included when using Delta Chat Core as a library. //! //! Usage: cargo run --example repl --release -- //! All further options can be set using the set-command (type ? for help). #[macro_use] extern crate deltachat; #[macro_use] extern crate failure; #[macro_use] extern crate lazy_static; use std::borrow::Cow::{self, Borrowed, Owned}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock}; use deltachat::constants::*; use deltachat::context::*; use deltachat::dc_configure::*; use deltachat::dc_job::*; use deltachat::dc_securejoin::*; use deltachat::dc_tools::*; use deltachat::oauth2::*; use deltachat::types::*; use deltachat::x::*; use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::config::OutputStreamType; use rustyline::error::ReadlineError; use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; use rustyline::hint::{Hinter, HistoryHinter}; use rustyline::{ Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyPress, }; mod cmdline; use self::cmdline::*; // Event Handler unsafe extern "C" fn receive_event( _context: &Context, event: Event, data1: uintptr_t, data2: uintptr_t, ) -> uintptr_t { match event { Event::GET_STRING => {} Event::INFO => { /* do not show the event as this would fill the screen */ println!("{}", to_string(data2 as *const _),); } Event::SMTP_CONNECTED => { println!("[DC_EVENT_SMTP_CONNECTED] {}", to_string(data2 as *const _)); } Event::IMAP_CONNECTED => { println!("[DC_EVENT_IMAP_CONNECTED] {}", to_string(data2 as *const _),); } Event::SMTP_MESSAGE_SENT => { println!( "[DC_EVENT_SMTP_MESSAGE_SENT] {}", to_string(data2 as *const _), ); } Event::WARNING => { println!("[Warning] {}", to_string(data2 as *const _),); } Event::ERROR => { println!( "\x1b[31m[DC_EVENT_ERROR] {}\x1b[0m", to_string(data2 as *const _), ); } Event::ERROR_NETWORK => { println!( "\x1b[31m[DC_EVENT_ERROR_NETWORK] first={}, msg={}\x1b[0m", data1 as usize, to_string(data2 as *const _), ); } Event::ERROR_SELF_NOT_IN_GROUP => { println!( "\x1b[31m[DC_EVENT_ERROR_SELF_NOT_IN_GROUP] {}\x1b[0m", to_string(data2 as *const _), ); } Event::MSGS_CHANGED => { print!( "\x1b[33m{{Received DC_EVENT_MSGS_CHANGED({}, {})}}\n\x1b[0m", data1 as usize, data2 as usize, ); } Event::CONTACTS_CHANGED => { print!("\x1b[33m{{Received DC_EVENT_CONTACTS_CHANGED()}}\n\x1b[0m"); } Event::LOCATION_CHANGED => { print!( "\x1b[33m{{Received DC_EVENT_LOCATION_CHANGED(contact={})}}\n\x1b[0m", data1 as usize, ); } Event::CONFIGURE_PROGRESS => { print!( "\x1b[33m{{Received DC_EVENT_CONFIGURE_PROGRESS({} ‰)}}\n\x1b[0m", data1 as usize, ); } Event::IMEX_PROGRESS => { print!( "\x1b[33m{{Received DC_EVENT_IMEX_PROGRESS({} ‰)}}\n\x1b[0m", data1 as usize, ); } Event::IMEX_FILE_WRITTEN => { print!( "\x1b[33m{{Received DC_EVENT_IMEX_FILE_WRITTEN({})}}\n\x1b[0m", to_string(data1 as *const _) ); } Event::CHAT_MODIFIED => { print!( "\x1b[33m{{Received DC_EVENT_CHAT_MODIFIED({})}}\n\x1b[0m", data1 as usize, ); } _ => { print!( "\x1b[33m{{Received {:?}({}, {})}}\n\x1b[0m", event, data1 as usize, data2 as usize, ); } } 0 } // Threads for waiting for messages and for jobs lazy_static! { static ref HANDLE: Arc>> = Arc::new(Mutex::new(None)); static ref IS_RUNNING: AtomicBool = AtomicBool::new(true); } struct Handle { handle_imap: Option>, handle_mvbox: Option>, handle_sentbox: Option>, handle_smtp: Option>, } macro_rules! while_running { ($code:block) => { if IS_RUNNING.load(Ordering::Relaxed) { $code } else { break; } }; } fn start_threads(c: Arc>) { if HANDLE.clone().lock().unwrap().is_some() { return; } println!("Starting threads"); IS_RUNNING.store(true, Ordering::Relaxed); let ctx = c.clone(); let handle_imap = std::thread::spawn(move || loop { while_running!({ unsafe { dc_perform_imap_jobs(&ctx.read().unwrap()); dc_perform_imap_fetch(&ctx.read().unwrap()); } while_running!({ let context = ctx.read().unwrap(); dc_perform_imap_idle(&context); }); }); }); let ctx = c.clone(); let handle_mvbox = std::thread::spawn(move || loop { while_running!({ unsafe { dc_perform_mvbox_fetch(&ctx.read().unwrap()) }; while_running!({ unsafe { dc_perform_mvbox_idle(&ctx.read().unwrap()) }; }); }); }); let ctx = c.clone(); let handle_sentbox = std::thread::spawn(move || loop { while_running!({ unsafe { dc_perform_sentbox_fetch(&ctx.read().unwrap()) }; while_running!({ unsafe { dc_perform_sentbox_idle(&ctx.read().unwrap()) }; }); }); }); let ctx = c; let handle_smtp = std::thread::spawn(move || loop { while_running!({ unsafe { dc_perform_smtp_jobs(&ctx.read().unwrap()) }; while_running!({ unsafe { dc_perform_smtp_idle(&ctx.read().unwrap()) }; }); }); }); *HANDLE.clone().lock().unwrap() = Some(Handle { handle_imap: Some(handle_imap), handle_mvbox: Some(handle_mvbox), handle_sentbox: Some(handle_sentbox), handle_smtp: Some(handle_smtp), }); } fn stop_threads(context: &Context) { if let Some(ref mut handle) = *HANDLE.clone().lock().unwrap() { println!("Stopping threads"); IS_RUNNING.store(false, Ordering::Relaxed); unsafe { dc_interrupt_imap_idle(context); dc_interrupt_mvbox_idle(context); dc_interrupt_sentbox_idle(context); dc_interrupt_smtp_idle(context); } handle.handle_imap.take().unwrap().join().unwrap(); handle.handle_mvbox.take().unwrap().join().unwrap(); handle.handle_sentbox.take().unwrap().join().unwrap(); handle.handle_smtp.take().unwrap().join().unwrap(); } } // === The main loop struct DcHelper { completer: FilenameCompleter, highlighter: MatchingBracketHighlighter, hinter: HistoryHinter, } impl Completer for DcHelper { type Candidate = Pair; fn complete( &self, line: &str, pos: usize, ctx: &RustyContext<'_>, ) -> Result<(usize, Vec), ReadlineError> { self.completer.complete(line, pos, ctx) } } const IMEX_COMMANDS: [&'static str; 12] = [ "initiate-key-transfer", "get-setupcodebegin", "continue-key-transfer", "has-backup", "export-backup", "import-backup", "export-keys", "import-keys", "export-setup", "poke", "reset", "stop", ]; const DB_COMMANDS: [&'static str; 11] = [ "info", "open", "close", "set", "get", "oauth2", "configure", "connect", "disconnect", "maybenetwork", "housekeeping", ]; const CHAT_COMMANDS: [&'static str; 24] = [ "listchats", "listarchived", "chat", "createchat", "createchatbymsg", "creategroup", "createverified", "addmember", "removemember", "groupname", "groupimage", "chatinfo", "sendlocations", "setlocation", "dellocations", "getlocations", "send", "sendimage", "sendfile", "draft", "listmedia", "archive", "unarchive", "delchat", ]; const MESSAGE_COMMANDS: [&'static str; 8] = [ "listmsgs", "msginfo", "listfresh", "forward", "markseen", "star", "unstar", "delmsg", ]; const CONTACT_COMMANDS: [&'static str; 6] = [ "listcontacts", "listverified", "addcontact", "contactinfo", "delcontact", "cleanupcontacts", ]; const MISC_COMMANDS: [&'static str; 8] = [ "getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "help", ]; impl Hinter for DcHelper { fn hint(&self, line: &str, pos: usize, ctx: &RustyContext<'_>) -> Option { if !line.is_empty() { for &cmds in &[ &IMEX_COMMANDS[..], &DB_COMMANDS[..], &CHAT_COMMANDS[..], &MESSAGE_COMMANDS[..], &CONTACT_COMMANDS[..], &MISC_COMMANDS[..], ] { if let Some(entry) = cmds.iter().find(|el| el.starts_with(&line[..pos])) { if *entry != line && *entry != &line[..pos] { return Some(entry[pos..].to_owned()); } } } } self.hinter.hint(line, pos, ctx) } } static COLORED_PROMPT: &'static str = "\x1b[1;32m> \x1b[0m"; static PROMPT: &'static str = "> "; impl Highlighter for DcHelper { fn highlight_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> { if prompt == PROMPT { Borrowed(COLORED_PROMPT) } else { Borrowed(prompt) } } fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { Owned("\x1b[1m".to_owned() + hint + "\x1b[m") } fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { self.highlighter.highlight(line, pos) } fn highlight_char(&self, line: &str, pos: usize) -> bool { self.highlighter.highlight_char(line, pos) } } impl Helper for DcHelper {} fn main_0(args: Vec) -> Result<(), failure::Error> { let mut context = dc_context_new( receive_event, 0 as *mut libc::c_void, b"CLI\x00" as *const u8 as *const libc::c_char, ); unsafe { dc_cmdline_skip_auth() }; if args.len() == 2 { if 0 == unsafe { dc_open( &mut context, to_cstring(&args[1]).as_ptr(), 0 as *const libc::c_char, ) } { println!("Error: Cannot open {}.", args[0],); } } else if args.len() != 1 { println!("Error: Bad arguments, expected [db-name]."); } println!("Delta Chat Core is awaiting your commands."); let ctx = Arc::new(RwLock::new(context)); let config = Config::builder() .history_ignore_space(true) .completion_type(CompletionType::List) .edit_mode(EditMode::Emacs) .output_stream(OutputStreamType::Stdout) .build(); let h = DcHelper { completer: FilenameCompleter::new(), highlighter: MatchingBracketHighlighter::new(), hinter: HistoryHinter {}, }; let mut rl = Editor::with_config(config); rl.set_helper(Some(h)); rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward); rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward); if rl.load_history(".dc-history.txt").is_err() { println!("No previous history."); } loop { let p = "> "; let readline = rl.readline(&p); match readline { Ok(line) => { // TODO: ignore "set mail_pw" rl.add_history_entry(line.as_str()); let ctx = ctx.clone(); match unsafe { handle_cmd(line.trim(), ctx) } { Ok(ExitResult::Continue) => {} Ok(ExitResult::Exit) => break, Err(err) => println!("Error: {}", err), } } Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => { println!("Exiting..."); break; } Err(err) => { println!("Error: {}", err); break; } } } rl.save_history(".dc-history.txt")?; println!("history saved"); { stop_threads(&ctx.read().unwrap()); unsafe { let mut ctx = ctx.write().unwrap(); dc_close(&mut ctx); dc_context_unref(&mut ctx); } } Ok(()) } #[derive(Debug)] enum ExitResult { Continue, Exit, } unsafe fn handle_cmd(line: &str, ctx: Arc>) -> Result { let mut args = line.splitn(2, ' '); let arg0 = args.next().unwrap_or_default(); let arg1 = args.next().unwrap_or_default(); let arg1_c = to_cstring(arg1); let arg1_c_ptr = if arg1.is_empty() { std::ptr::null() } else { arg1_c.as_ptr() }; match arg0 { "connect" => { start_threads(ctx); } "disconnect" => { stop_threads(&ctx.read().unwrap()); } "smtp-jobs" => { if HANDLE.clone().lock().unwrap().is_some() { println!("smtp-jobs are already running in a thread.",); } else { dc_perform_smtp_jobs(&ctx.read().unwrap()); } } "imap-jobs" => { if HANDLE.clone().lock().unwrap().is_some() { println!("imap-jobs are already running in a thread."); } else { dc_perform_imap_jobs(&ctx.read().unwrap()); } } "configure" => { start_threads(ctx.clone()); dc_configure(&ctx.read().unwrap()); } "oauth2" => { let addr = dc_get_config( &ctx.read().unwrap(), b"addr\x00" as *const u8 as *const libc::c_char, ); if addr.is_null() || *addr.offset(0isize) as libc::c_int == 0i32 { println!("oauth2: set addr first."); } else { let oauth2_url = dc_get_oauth2_url( &ctx.read().unwrap(), to_str(addr), "chat.delta:/com.b44t.messenger", ); if oauth2_url.is_none() { println!("OAuth2 not available for {}.", to_string(addr)); } else { println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{}", oauth2_url.unwrap()); } } free(addr as *mut libc::c_void); } "clear" => { println!("\n\n\n"); print!("\x1b[1;1H\x1b[2J"); } "getqr" | "getbadqr" => { start_threads(ctx.clone()); let qrstr = dc_get_securejoin_qr(&ctx.read().unwrap(), arg1.parse().unwrap_or_default()); if !qrstr.is_null() && 0 != *qrstr.offset(0isize) as libc::c_int { if arg0 == "getbadqr" && strlen(qrstr) > 40 { let mut i: libc::c_int = 12i32; while i < 22i32 { *qrstr.offset(i as isize) = '0' as i32 as libc::c_char; i += 1 } } println!("{}", to_string(qrstr as *const _)); let syscmd = dc_mprintf( b"qrencode -t ansiutf8 \"%s\" -o -\x00" as *const u8 as *const libc::c_char, qrstr, ); system(syscmd); free(syscmd as *mut libc::c_void); } free(qrstr as *mut libc::c_void); } "joinqr" => { start_threads(ctx.clone()); if !arg0.is_empty() { dc_join_securejoin(&ctx.read().unwrap(), arg1_c_ptr); } } "exit" => return Ok(ExitResult::Exit), _ => dc_cmdline(&ctx.read().unwrap(), line)?, } Ok(ExitResult::Continue) } pub fn main() -> Result<(), failure::Error> { let _ = pretty_env_logger::try_init(); let args: Vec = std::env::args().collect(); main_0(args)?; Ok(()) }