pub mod broker;
pub mod completions;
pub mod config;
pub mod conn;
pub mod contacts;
pub mod daemon;
pub mod delete;
mod dispatch;
pub mod folders;
pub mod get;
pub mod hub;
pub mod list;
pub mod send;
pub mod spoke;
pub mod sync;
pub mod threads;
pub mod unsync;
use dispatch::{run_contacts, run_get, run_list, run_send, run_threads};
use std::path::PathBuf;
use anyhow::{Context, Result};
use crate::cli::{BrokerCmd, Cli, Command, ConfigCmd, SpokeCmd};
use crate::output;
use crate::progress::with_spinner;
pub async fn dispatch(cli: Cli) -> Result<()> {
let Cli { hub, device, config: config_path, command, .. } = cli;
let spoke = if hub {
Some(transport::iroh::bind_spoke().await.context("binding iroh spoke endpoint")?)
} else {
None
};
let result = run_command(command, spoke.as_ref(), device.as_deref(), config_path).await;
if let Some(ep) = &spoke {
ep.close().await;
}
(result?).map_or_else(|| Ok(()), |out| output::line(&out))
}
async fn run_command(
command: Command,
spoke: Option<&transport::iroh::Endpoint>,
device: Option<&str>,
config_path: Option<PathBuf>,
) -> Result<Option<String>> {
let out = match command {
Command::Config { cmd } => Some(run_config(cmd, config_path).await?),
Command::Hub => {
hub::run(&load(config_path)?).await?;
None
}
Command::Spoke { cmd } => Some(match cmd {
SpokeCmd::Add { key } => spoke::run_add(&key).await?,
}),
Command::Broker { cmd } => Some(match cmd {
BrokerCmd::Status => broker::run_status(&load(config_path)?, device, "broker").await?,
}),
Command::List { folder, unread, long, from, since, limit, offset } => {
let (cfg, db, bpath) = load_with_store(config_path).await?;
let opts = list::ListOpts { folder, unread, from, since, limit, offset, long };
Some(run_list(&cfg, spoke, device, opts, &db, bpath.as_deref()).await?)
}
Command::Folders => {
let bpath = config_path.clone();
let cfg = load(config_path)?;
let fut = folders::run(&cfg, spoke, device, bpath.as_deref());
Some(with_spinner("folders", fut).await?)
}
Command::Get { handle, folder: _, mark_read } => {
let (cfg, db, bpath) = load_with_store(config_path).await?;
Some(run_get(&cfg, spoke, device, handle, mark_read, &db, bpath.as_deref()).await?)
}
Command::Delete { handle, folder, undelete } => {
let (cfg, db, bpath) = load_with_store(config_path).await?;
let opts = delete::DeleteOpts { handle, folder, undelete };
let fut = delete::run(&cfg, spoke, device, opts, &db, bpath.as_deref());
Some(with_spinner("deleting", fut).await?)
}
Command::Send { number, message } => {
let (cfg, db, bpath) = load_with_store(config_path).await?;
Some(run_send(&cfg, spoke, device, number, message, &db, bpath.as_deref()).await?)
}
Command::Contacts { list, get, lookup, sync, path, raw, limit, page } => {
let (cfg, db, bpath) = load_with_store(config_path).await?;
let opts = contacts::ContactsOpts { list, get, lookup, sync, path, raw, limit, page };
Some(run_contacts(&cfg, spoke, device, opts, &db, bpath.as_deref()).await?)
}
Command::Threads => {
let (cfg, db, bpath) = load_with_store(config_path).await?;
Some(run_threads(&cfg, spoke, device, &db, bpath.as_deref()).await?)
}
Command::Sync { folder } => {
let (cfg, db, bpath) = load_with_store(config_path).await?;
let fut = sync::run(&cfg, spoke, device, &db, folder, bpath.as_deref());
Some(with_spinner("syncing", fut).await?)
}
Command::BrokerServe => {
let cfg = load(config_path)?;
let db = open_store(&cfg).await?;
imsg_broker::run(cfg, device.map(str::to_owned), db).await?;
None
}
Command::Unsync { purge } => Some(run_unsync(purge, config_path).await?),
Command::Daemon { cmd } => daemon::dispatch(cmd, device, config_path).await?,
Command::Completions { shell, install } => Some(completions::run(shell, install)?),
};
Ok(out)
}
async fn load_with_store(
config_path: Option<PathBuf>,
) -> Result<(::config::Config, store::Store, Option<PathBuf>)> {
let bpath = config_path.clone();
let cfg = load(config_path)?;
let db = open_store(&cfg).await?;
Ok((cfg, db, bpath))
}
async fn run_config(cmd: ConfigCmd, config_path: Option<PathBuf>) -> Result<String> {
match cmd {
ConfigCmd::Show => config::run_show(config_path),
ConfigCmd::SetDevice { address } => config::run_set_device(&address),
ConfigCmd::Setup => config::run_setup().await,
}
}
async fn run_unsync(purge: bool, config_path: Option<PathBuf>) -> Result<String> {
let cfg = load(config_path)?;
let db_path = cfg.store.resolve().context("no data directory available")?;
if purge {
unsync::purge(db_path)?;
return Ok("sync disabled; database deleted".to_owned());
}
let db = open_store(&cfg).await?;
unsync::disable(&db).await?;
Ok("sync disabled; database preserved (re-enable with imsg sync)".to_owned())
}
pub(crate) fn live_footer(mut out: String) -> String {
if !out.ends_with('\n') {
out.push('\n');
}
out.push_str("(live from device)");
out
}
pub(crate) fn freshness_line(last_sync_at: Option<i64>) -> String {
last_sync_at.map_or_else(
|| "(never synced \u{2014} run 'imsg sync' to populate the store)".to_owned(),
|ms| {
format!(
"(store as of {} \u{2014} run 'imsg sync' to refresh)",
session::sync::ms_to_display(ms)
)
},
)
}
pub(in crate::commands) fn load(path: Option<PathBuf>) -> Result<::config::Config> {
::config::load(path)
.context("loading config (run `imsg config set-device <ADDR>` if device.address is unset)")
}
pub(in crate::commands) async fn open_store(cfg: &::config::Config) -> Result<store::Store> {
let path =
cfg.store.resolve().context("no data directory available (set HOME or XDG_DATA_HOME)")?;
let ready = keyring::init_store().context("Secret Service store init failed")?;
let key = keyring::get_or_create_db_key(&ready).context("getting database encryption key")?;
store::Store::open(path, key).await.context("opening message store")
}