use std::fmt::Write as _;
use std::path::Path;
use anyhow::Result;
use config::Config;
use ipc::{BodyDto, BrokerRequest, BrokerResponse};
use map_core::MessageStatus;
use session::live::models::{Direction as LiveDirection, LiveBody};
use store::{Direction, MessageRow, Store, STATUS_READ, STATUS_UNREAD};
use transport::iroh::Endpoint;
use crate::commands::{broker, conn, live_footer};
pub(crate) async fn run(
cfg: &Config,
endpoint: Option<&Endpoint>,
device: Option<&str>,
handle: String,
mark_read: bool,
config_path: Option<&Path>,
) -> Result<String> {
if endpoint.is_none() {
let body = get_via_broker(cfg, device, config_path, handle.clone()).await?;
let out = render_row(&BodyView::from_dto(&body));
if mark_read && !body.read {
broker::call(cfg, device, config_path, BrokerRequest::MarkReadDevice { handle })
.await
.ok();
}
return Ok(live_footer(out));
}
let body = get_via_map(cfg, endpoint, device, handle, mark_read).await?;
Ok(live_footer(render_row(&BodyView::from_live(&body))))
}
async fn get_via_map(
cfg: &Config,
endpoint: Option<&Endpoint>,
device: Option<&str>,
handle: String,
mark_read: bool,
) -> Result<LiveBody> {
let mut client = conn::connect_map(cfg, endpoint, device).await?;
let result = session::live::get(&mut client, handle.clone()).await;
if let Ok(body) = &result {
if mark_read && !body.read {
if let Err(e) = client.set_message_status_read(&handle, MessageStatus::Read).await {
tracing::warn!("failed to mark {handle} read on device: {e}");
}
}
}
if let Err(e) = client.disconnect().await {
tracing::warn!("MAP disconnect failed: {e}");
}
result
}
pub(crate) async fn run_store(handle: String, mark_read: bool, store: &Store) -> Result<String> {
let row = store
.get_by_handle(&handle)
.await?
.ok_or_else(|| anyhow::anyhow!("message {handle} not found in local store"))?;
if mark_read && row.status == STATUS_UNREAD {
if let Err(e) = store.update_status(&handle, STATUS_READ).await {
tracing::warn!("failed to update read status in store for {handle}: {e}");
}
}
let mut out = render_row(&BodyView::from_row(&row));
if !out.ends_with('\n') {
out.push('\n');
}
out.push_str(&crate::commands::freshness_line(store.last_sync_at().await?));
Ok(out)
}
async fn get_via_broker(
cfg: &Config,
device: Option<&str>,
config_path: Option<&Path>,
handle: String,
) -> Result<BodyDto> {
match broker::call(cfg, device, config_path, BrokerRequest::GetMessage { handle }).await? {
BrokerResponse::Body(body) => Ok(body),
BrokerResponse::Failed(reason) => Err(anyhow::anyhow!("{reason}")),
BrokerResponse::Error(e) => Err(anyhow::anyhow!("{e}")),
other => Err(anyhow::anyhow!("unexpected broker response: {other:?}")),
}
}
struct BodyView<'a> {
label: &'static str,
address: &'a str,
date: Option<String>,
folder: &'a str,
read: bool,
text: &'a str,
}
impl<'a> BodyView<'a> {
fn from_row(row: &'a MessageRow) -> Self {
Self {
label: dir_label(matches!(row.direction, Direction::Sent)),
address: &row.address,
date: Some(session::sync::ms_to_display(row.timestamp_ms)),
folder: &row.folder,
read: row.status == STATUS_READ,
text: &row.text,
}
}
fn from_dto(b: &'a BodyDto) -> Self {
Self {
label: dir_label(matches!(b.direction, ipc::Direction::Sent)),
address: &b.address,
date: None,
folder: &b.folder,
read: b.read,
text: &b.text,
}
}
fn from_live(b: &'a LiveBody) -> Self {
Self {
label: dir_label(matches!(b.direction, LiveDirection::Sent)),
address: &b.address,
date: None,
folder: &b.folder,
read: b.read,
text: &b.text,
}
}
}
const fn dir_label(sent: bool) -> &'static str {
if sent {
"To"
} else {
"From"
}
}
fn render_row(v: &BodyView) -> String {
let status = if v.read { "read" } else { "unread" };
let mut out = String::with_capacity(v.text.len().saturating_add(64));
let _ = writeln!(out, "{}: {}", v.label, v.address);
if let Some(dt) = &v.date {
let _ = writeln!(out, "Date: {dt}");
}
let _ = writeln!(out, "Folder: {}", v.folder);
let _ = writeln!(out, "Status: {status}");
let _ = writeln!(out);
let _ = write!(out, "{}", v.text);
out
}