umsh_text/
node_adapter.rs

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