Skip to main content

imsg_session/watch/
mod.rs

1//! Live MNS event processing and the watch loop.
2
3use map_core::client::MapClient;
4use map_core::folders::Folder;
5use map_core::MessageStatus;
6use store::{Direction, NewMessage, OutgoingStatus, PhoneField, Store};
7use tokio::io::{AsyncRead, AsyncWrite};
8use tokio::sync::{mpsc, watch};
9
10use crate::outbox::drain_outbox;
11use crate::sync::backfill_catch_up;
12use crate::util::now_ms;
13use crate::{EventType, MnsEvent};
14
15// matches on the last /-delimited segment, case-insensitively — real devices report uppercase
16// paths (e.g. TELECOM/MSG/INBOX). Returns None for unknown folder names
17fn parse_folder(s: &str) -> Option<Folder> {
18    // rsplit('/').next() always yields Some on any &str; unwrap_or(s) is a no-op fallback.
19    let leaf = s.rsplit('/').next().unwrap_or(s);
20    match leaf.to_ascii_lowercase().as_str() {
21        "inbox" => Some(Folder::Inbox),
22        "sent" => Some(Folder::Sent),
23        "outbox" => Some(Folder::Outbox),
24        "deleted" => Some(Folder::Deleted),
25        _ => None,
26    }
27}
28
29/// Processes a single MNS event against the store.
30///
31/// `NewMessage` — navigates to the event folder, fetches the body, and upserts.
32/// `MessageDeleted` — deletes by handle. `MessageShift` — updates folder. `ReadStatusChanged` —
33/// re-fetches the message and writes its true read/unread flag (the event itself carries no
34/// directionality). `DeliverySuccess`/`SendingSuccess` — confirms the outbound
35/// `outgoing_status`; `DeliveryFailure`/`SendingFailure` — marks it permanently failed (same
36/// column [`Store::reconcile_outgoing`] resolves via Sent-folder backfill, so this is the
37/// event-driven fast path for the same outcome). `MemoryFull`/`MemoryAvailable` are logged only —
38/// they carry no `handle` and have no corresponding store row. Missing handle or unknown folder
39/// on `NewMessage`/`ReadStatusChanged` are logged and skipped.
40///
41/// # Errors
42///
43/// Returns an error if the store write fails. MAP fetch errors on `NewMessage`/
44/// `ReadStatusChanged` are propagated.
45pub async fn handle_mns_event<T: AsyncRead + AsyncWrite + Unpin>(
46    event: &MnsEvent,
47    client: &mut MapClient<T>,
48    store: &Store,
49    now: i64,
50) -> anyhow::Result<()> {
51    match event.event_type() {
52        EventType::NewMessage => handle_new_message(event, client, store, now).await?,
53        EventType::MessageDeleted => {
54            if let Some(handle) = event.handle() {
55                store.delete_by_handle(handle).await?;
56            }
57        }
58        EventType::MessageShift => {
59            if let (Some(handle), Some(folder)) = (event.handle(), event.folder()) {
60                store.update_folder(handle, folder).await?;
61            }
62        }
63        EventType::ReadStatusChanged => handle_read_status_changed(event, client, store).await?,
64        EventType::DeliverySuccess | EventType::SendingSuccess => {
65            mark_outgoing(event, store, OutgoingStatus::SentConfirmed).await?;
66        }
67        EventType::DeliveryFailure | EventType::SendingFailure => {
68            mark_outgoing(event, store, OutgoingStatus::FailedPermanent).await?;
69        }
70        EventType::MemoryFull => {
71            tracing::warn!(
72                "device message store is full — new messages may be rejected until freed"
73            );
74        }
75        EventType::MemoryAvailable => {
76            tracing::info!("device message store has space available again");
77        }
78    }
79    Ok(())
80}
81
82// missing handle/folder or an unparseable folder are logged and skipped, not errors — only a
83// MAP transport/protocol failure or a store write failure propagates
84async fn handle_new_message<T: AsyncRead + AsyncWrite + Unpin>(
85    event: &MnsEvent,
86    client: &mut MapClient<T>,
87    store: &Store,
88    now: i64,
89) -> anyhow::Result<()> {
90    let (Some(handle), Some(folder_raw)) = (event.handle(), event.folder()) else {
91        tracing::warn!("NewMessage event missing handle or folder — skipped");
92        return Ok(());
93    };
94    let Some(folder) = parse_folder(folder_raw) else {
95        tracing::warn!("NewMessage event unknown folder {folder_raw} — skipped");
96        return Ok(());
97    };
98    client.set_folder(folder).await?;
99    let bmsg = client.get_message(handle).await?;
100    let address = bmsg.originator().map(|o| o.tel.clone()).unwrap_or_default();
101    let status = i32::from(matches!(bmsg.status(), MessageStatus::Read));
102    let msg = NewMessage {
103        map_handle: handle.to_owned(),
104        timestamp_ms: now,
105        folder: folder_raw.to_owned(),
106        direction: Direction::Received,
107        address: PhoneField::new(&address, None),
108        status,
109        synced_at: now,
110        text: bmsg.envelope().body.text.clone(),
111        outgoing_status: None,
112    };
113    store.upsert(msg).await?;
114    Ok(())
115}
116
117// MAP's ReadStatusChanged event carries no directionality — it only signals that the flag
118// changed, not which way — so the previous value can't be trusted to mean "became read"; this
119// must re-fetch and check. Missing handle/folder or an unparseable folder are logged and
120// skipped, not errors — only a MAP transport/protocol failure or a store write failure
121// propagates
122async fn handle_read_status_changed<T: AsyncRead + AsyncWrite + Unpin>(
123    event: &MnsEvent,
124    client: &mut MapClient<T>,
125    store: &Store,
126) -> anyhow::Result<()> {
127    let (Some(handle), Some(folder_raw)) = (event.handle(), event.folder()) else {
128        tracing::warn!("ReadStatusChanged event missing handle or folder — skipped");
129        return Ok(());
130    };
131    let Some(folder) = parse_folder(folder_raw) else {
132        tracing::warn!("ReadStatusChanged event unknown folder {folder_raw} — skipped");
133        return Ok(());
134    };
135    client.set_folder(folder).await?;
136    let bmsg = client.get_message(handle).await?;
137    let status = i32::from(matches!(bmsg.status(), MessageStatus::Read));
138    store.update_status(handle, status).await?;
139    Ok(())
140}
141
142// no-op when the event carries no handle
143async fn mark_outgoing(
144    event: &MnsEvent,
145    store: &Store,
146    status: OutgoingStatus,
147) -> anyhow::Result<()> {
148    if let Some(handle) = event.handle() {
149        store.update_outgoing_status(handle, status).await?;
150    }
151    Ok(())
152}
153
154/// Runs a catch-up backfill, drains queued outbox entries, then processes live MNS events
155/// until cancellation.
156///
157/// Catch-up uses per-folder cursors — each folder pulls only what it missed since its
158/// last successful sync. Queued outbox entries are flushed after backfill so that any
159/// message pending from a prior failed send is delivered before the event loop begins.
160/// Each `NewMessage` event fetches the body from `client`; other event types only touch
161/// the store. Returns when `cancel_rx` is set to `true` or `event_rx` closes.
162///
163/// Public for out-of-tree consumers (e.g. a GUI holding its own `MapClient`) — the CLI's own
164/// `watch` command was removed in favor of `imsg daemon`, which drives the broker's actor loop
165/// directly rather than through this function.
166///
167/// # Errors
168///
169/// Returns an error if the initial backfill, outbox drain, or any store write fails.
170pub async fn run_watch<T: AsyncRead + AsyncWrite + Unpin>(
171    event_rx: &mut mpsc::Receiver<MnsEvent>,
172    client: &mut MapClient<T>,
173    store: &Store,
174    mut cancel_rx: watch::Receiver<bool>,
175) -> anyhow::Result<()> {
176    backfill_catch_up(client, store).await?;
177    drain_outbox(client, store, now_ms()).await?;
178    loop {
179        tokio::select! {
180            biased;
181            _ = cancel_rx.changed() => {
182                if *cancel_rx.borrow() { break; }
183            }
184            event = event_rx.recv() => {
185                let Some(ev) = event else { break; };
186                handle_mns_event(&ev, client, store, now_ms()).await?;
187            }
188        }
189    }
190    Ok(())
191}
192
193#[cfg(test)]
194mod tests;