use map_core::{client::MapClient, folders::Folder, MapError};
use store::{Direction, NewMessage, OutboxStatus, OutgoingStatus, PhoneField, Store, STATUS_READ};
use tokio::io::{AsyncRead, AsyncWrite};
#[must_use]
pub const fn classify_push_error(e: &MapError) -> (OutboxStatus, OutgoingStatus) {
match e {
MapError::Transport(_) | MapError::UnexpectedEof => {
(OutboxStatus::Unknown, OutgoingStatus::Unknown)
}
MapError::InvalidInput(_) | MapError::ServerError(_) => {
(OutboxStatus::Failed, OutgoingStatus::FailedPermanent)
}
_ => (OutboxStatus::Failed, OutgoingStatus::FailedRetryable),
}
}
#[must_use]
pub const fn is_session_fatal(e: &MapError) -> bool {
matches!(e, MapError::Transport(_) | MapError::UnexpectedEof)
}
#[must_use]
pub fn is_fatal_anyhow(e: &anyhow::Error) -> bool {
e.chain().filter_map(|cause| cause.downcast_ref::<MapError>()).any(is_session_fatal)
}
pub async fn send_sms<T: AsyncRead + AsyncWrite + Unpin>(
client: &mut MapClient<T>,
store: &Store,
number: &str,
message: &str,
now: i64,
) -> anyhow::Result<String> {
let params = format!("{number}\x1F{message}");
let (_, outbox_id) = store
.enqueue_send(
NewMessage {
map_handle: String::new(),
timestamp_ms: now,
folder: Folder::Sent.as_str().to_owned(),
direction: Direction::Sent,
address: PhoneField::new(number, None),
status: STATUS_READ,
synced_at: now,
text: message.to_owned(),
outgoing_status: Some(OutgoingStatus::Queued),
},
"send_sms",
¶ms,
now,
)
.await?;
let placeholder = format!("local:{outbox_id}");
client.set_folder(Folder::Outbox).await?;
store.resolve(outbox_id, OutboxStatus::Sending, now, None).await?;
match client.push_message(number, message).await {
Ok(handle) => {
store.complete_send(outbox_id, &placeholder, &handle, now).await?;
Ok(format!("sent to {number} (handle {handle})"))
}
Err(e) => {
let (outbox_status, outgoing_status) = classify_push_error(&e);
if let Err(se) = store.resolve(outbox_id, outbox_status, now, Some(e.to_string())).await
{
tracing::warn!("resolve outbox {outbox_id}: {se}");
}
if let Err(se) = store.update_outgoing_status(&placeholder, outgoing_status).await {
tracing::warn!("update outgoing status {placeholder}: {se}");
}
Err(anyhow::Error::from(e))
}
}
}
pub async fn push_sms<T: AsyncRead + AsyncWrite + Unpin>(
client: &mut MapClient<T>,
number: &str,
message: &str,
) -> anyhow::Result<String> {
client.set_folder(Folder::Outbox).await?;
let handle = client.push_message(number, message).await?;
Ok(format!("sent to {number} (handle {handle})"))
}
pub async fn drain_outbox<T: AsyncRead + AsyncWrite + Unpin>(
client: &mut MapClient<T>,
store: &Store,
now: i64,
) -> anyhow::Result<()> {
let pending = store.pending().await?;
if pending.is_empty() {
return Ok(());
}
client.set_folder(Folder::Outbox).await?;
for entry in pending {
process_entry(client, store, entry, now).await;
}
Ok(())
}
async fn process_entry<T: AsyncRead + AsyncWrite + Unpin>(
client: &mut MapClient<T>,
store: &Store,
entry: store::OutboxRow,
now: i64,
) {
let Some((number, message)) = entry.payload.split_once('\x1F') else {
tracing::warn!("drain_outbox: entry {} has unparseable payload — skipped", entry.id);
return;
};
let placeholder = format!("local:{}", entry.id);
match client.push_message(number, message).await {
Ok(handle) => record_send_ok(store, entry.id, &placeholder, &handle, now).await,
Err(e) => record_send_err(store, entry.id, &placeholder, &e, now).await,
}
}
async fn record_send_ok(store: &Store, entry_id: i64, placeholder: &str, handle: &str, now: i64) {
store.complete_send(entry_id, placeholder, handle, now).await.unwrap_or_else(|e| {
tracing::warn!("drain_outbox: store update failed for entry {entry_id}: {e:#}");
});
}
async fn record_send_err(
store: &Store,
entry_id: i64,
placeholder: &str,
e: &map_core::MapError,
now: i64,
) {
let (outbox_status, outgoing_status) = classify_push_error(e);
tracing::warn!("drain_outbox: push failed for entry {entry_id}: {e}");
let err_str = e.to_string();
store.resolve(entry_id, outbox_status, now, Some(err_str)).await.unwrap_or_else(|se| {
tracing::warn!("drain_outbox: resolve failed for entry {entry_id}: {se:#}");
});
store.update_outgoing_status(placeholder, outgoing_status).await.unwrap_or_else(|se| {
tracing::warn!("drain_outbox: status update failed for entry {entry_id}: {se:#}");
});
}