umsh_text/node_adapter/
mod.rs

1//! Node-layer adapters for chat-style applications (feature `node`).
2//!
3//! - [`inbound_text`] classifies a received frame for a chat session, with
4//!   the display rules of the well-known channels, and copies a text frame
5//!   out of the receive callback for the [`ChatFacade`](crate::chat::ChatFacade).
6//! - [`TransmitQueue`] carries the facade's transmissions to `LocalNode`
7//!   sends, and maps acknowledgments and timeouts back onto the engine.
8//!   Channel sends need the `software-crypto` feature.
9//! - Thin wrappers for applications that send and receive text directly on
10//!   a peer connection or a bound channel.
11
12use alloc::vec::Vec;
13
14mod inbound;
15mod transmit;
16
17pub use inbound::{Inbound, InboundText, Refusal, RxSummary, WellKnownChannels, inbound_text};
18#[cfg(feature = "software-crypto")]
19pub use transmit::bound_channel;
20pub use transmit::{
21    Departed, Departure, Sent, Stall, TRANSMISSION_STALL_MS, TransmitPolicy, TransmitQueue,
22    Undeliverable, WELL_KNOWN_CHANNEL_MAX_FLOOD_HOPS, channel_reach, transmit, with_flood_reach,
23};
24
25use umsh_core::{PacketType, PayloadType};
26use umsh_mac::SendOptions;
27use umsh_node::{LocalNode, PeerConnection, SendProgressTicket, Subscription, Transport};
28
29#[cfg(feature = "software-crypto")]
30use umsh_node::{BoundChannel, MacBackend};
31
32use crate::{
33    EncodeError, OwnedTextMessage, ParseError, TextMessage, TextSendError, encode_text_message,
34    parse_text_message,
35};
36
37/// Reason a received packet did not become a text message callback.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum TextReceiveIssue {
40    WrongPayloadType(PayloadType),
41    Parse(ParseError),
42}
43
44/// Parse a typed text payload in the context of the enclosing packet type.
45pub fn parse_text_payload(
46    packet_type: PacketType,
47    payload: &[u8],
48) -> Result<TextMessage<'_>, ParseError> {
49    parse_text_message(expect_payload_type(
50        packet_type,
51        payload,
52        PayloadType::TextMessage,
53    )?)
54}
55
56/// Thin convenience wrapper for plain unicast text chat over a peer connection.
57#[derive(Clone)]
58pub struct UnicastTextChatWrapper<T: Transport + Clone> {
59    peer: PeerConnection<T>,
60}
61
62impl<T: Transport + Clone> UnicastTextChatWrapper<T> {
63    pub fn new(peer: PeerConnection<T>) -> Self {
64        Self { peer }
65    }
66
67    pub fn from_peer(peer: &PeerConnection<T>) -> Self {
68        Self { peer: peer.clone() }
69    }
70
71    pub fn peer_connection(&self) -> &PeerConnection<T> {
72        &self.peer
73    }
74
75    pub fn peer(&self) -> &umsh_core::PublicKey {
76        self.peer.peer()
77    }
78
79    pub async fn send_message(
80        &self,
81        message: &TextMessage<'_>,
82        options: &SendOptions,
83    ) -> Result<SendProgressTicket, TextSendError<T::Error>> {
84        let payload = encode_text_payload(message)?;
85        self.peer
86            .send(&payload, options)
87            .await
88            .map_err(TextSendError::Transport)
89    }
90
91    pub async fn send_owned_message(
92        &self,
93        message: &OwnedTextMessage,
94        options: &SendOptions,
95    ) -> Result<SendProgressTicket, TextSendError<T::Error>> {
96        self.send_message(&message.as_borrowed(), options).await
97    }
98
99    pub async fn send_text(
100        &self,
101        body: &str,
102        options: &SendOptions,
103    ) -> Result<SendProgressTicket, TextSendError<T::Error>> {
104        self.send_message(&TextMessage::basic(body), options).await
105    }
106}
107
108impl<M: umsh_node::MacBackend> UnicastTextChatWrapper<LocalNode<M>> {
109    pub fn on_text<F>(&self, handler: F) -> Subscription
110    where
111        F: FnMut(&umsh_node::ReceivedPacketRef<'_>, TextMessage<'_>) + 'static,
112    {
113        self.on_text_with_diagnostics(handler, |_, _| {})
114    }
115
116    pub fn on_text_with_diagnostics<F, D>(&self, mut handler: F, mut diagnostics: D) -> Subscription
117    where
118        F: FnMut(&umsh_node::ReceivedPacketRef<'_>, TextMessage<'_>) + 'static,
119        D: FnMut(&umsh_node::ReceivedPacketRef<'_>, TextReceiveIssue) + 'static,
120    {
121        self.peer.on_receive(move |packet| {
122            if packet.payload_type() != PayloadType::TextMessage {
123                diagnostics(
124                    packet,
125                    TextReceiveIssue::WrongPayloadType(packet.payload_type()),
126                );
127                return false;
128            }
129            let message = match parse_text_message(packet.payload()) {
130                Ok(message) => message,
131                Err(error) => {
132                    diagnostics(packet, TextReceiveIssue::Parse(error));
133                    return false;
134                }
135            };
136            handler(packet, message);
137            true
138        })
139    }
140}
141
142#[cfg(feature = "software-crypto")]
143#[derive(Clone)]
144pub struct MulticastTextChatWrapper<M: MacBackend> {
145    channel: BoundChannel<M>,
146}
147
148#[cfg(feature = "software-crypto")]
149impl<M: MacBackend> MulticastTextChatWrapper<M> {
150    pub fn new(channel: BoundChannel<M>) -> Self {
151        Self { channel }
152    }
153
154    pub fn from_channel(channel: &BoundChannel<M>) -> Self {
155        Self {
156            channel: channel.clone(),
157        }
158    }
159
160    pub fn bound_channel(&self) -> &BoundChannel<M> {
161        &self.channel
162    }
163
164    pub async fn send_message(
165        &self,
166        message: &TextMessage<'_>,
167        options: &SendOptions,
168    ) -> Result<SendProgressTicket, TextSendError<umsh_node::NodeError<M>>> {
169        let payload = encode_text_payload(message)?;
170        self.channel
171            .send_all(&payload, options)
172            .await
173            .map_err(TextSendError::Transport)
174    }
175
176    pub async fn send_owned_message(
177        &self,
178        message: &OwnedTextMessage,
179        options: &SendOptions,
180    ) -> Result<SendProgressTicket, TextSendError<umsh_node::NodeError<M>>> {
181        self.send_message(&message.as_borrowed(), options).await
182    }
183
184    pub async fn send_text(
185        &self,
186        body: &str,
187        options: &SendOptions,
188    ) -> Result<SendProgressTicket, TextSendError<umsh_node::NodeError<M>>> {
189        self.send_message(&TextMessage::basic(body), options).await
190    }
191
192    pub fn on_text<F>(&self, handler: F) -> Subscription
193    where
194        F: FnMut(&umsh_node::ReceivedPacketRef<'_>, TextMessage<'_>) + 'static,
195    {
196        self.on_text_with_diagnostics(handler, |_, _| {})
197    }
198
199    pub fn on_text_with_diagnostics<F, D>(&self, mut handler: F, mut diagnostics: D) -> Subscription
200    where
201        F: FnMut(&umsh_node::ReceivedPacketRef<'_>, TextMessage<'_>) + 'static,
202        D: FnMut(&umsh_node::ReceivedPacketRef<'_>, TextReceiveIssue) + 'static,
203    {
204        let channel_id = *self.channel.channel().channel_id();
205        self.channel
206            .node()
207            .on_receive(move |packet: &umsh_node::ReceivedPacketRef<'_>| {
208                let Some(channel) = packet.channel() else {
209                    return false;
210                };
211                if channel.id() != channel_id {
212                    return false;
213                }
214                if packet.payload_type() != PayloadType::TextMessage {
215                    diagnostics(
216                        packet,
217                        TextReceiveIssue::WrongPayloadType(packet.payload_type()),
218                    );
219                    return false;
220                }
221                let message = match parse_text_message(packet.payload()) {
222                    Ok(message) => message,
223                    Err(error) => {
224                        diagnostics(packet, TextReceiveIssue::Parse(error));
225                        return false;
226                    }
227                };
228                handler(packet, message);
229                true
230            })
231    }
232}
233
234fn encode_text_payload(message: &TextMessage<'_>) -> Result<Vec<u8>, EncodeError> {
235    let mut body = [0u8; 512];
236    let len = encode_text_message(message, &mut body)?;
237    let mut payload = Vec::with_capacity(len + 1);
238    payload.push(PayloadType::TextMessage as u8);
239    payload.extend_from_slice(&body[..len]);
240    Ok(payload)
241}
242
243/// Split a typed application payload into its type byte and body.
244fn split_payload_type(payload: &[u8]) -> Result<(PayloadType, &[u8]), ParseError> {
245    if payload.is_empty() {
246        return Ok((PayloadType::Empty, &[]));
247    }
248    if let Some(payload_type) = PayloadType::from_byte(payload[0]) {
249        Ok((payload_type, &payload[1..]))
250    } else {
251        Ok((PayloadType::Empty, payload))
252    }
253}
254
255/// Validate that a packet carries the expected typed text payload.
256fn expect_payload_type(
257    packet_type: PacketType,
258    payload: &[u8],
259    expected: PayloadType,
260) -> Result<&[u8], ParseError> {
261    let (payload_type, body) = split_payload_type(payload)?;
262    if !payload_type.allowed_for(packet_type) {
263        return Err(ParseError::PayloadTypeNotAllowed {
264            payload_type: payload_type as u8,
265            packet_type,
266        });
267    }
268    if payload_type != expected {
269        return Err(ParseError::InvalidPayloadType(payload_type as u8));
270    }
271    Ok(body)
272}