zello_client/
message.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// SPDX-FileCopyrightText: 2024 John C. Murray
3
4//! Message types for Zello protocol
5
6use anyhow::{Result, anyhow};
7use base64::{Engine as _, engine::general_purpose::STANDARD};
8use bytes::{Buf, BufMut, Bytes, BytesMut};
9use serde::{Deserialize, Serialize};
10
11/// Messages that can be sent to Zello
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(tag = "command")]
14pub enum Message {
15    /// Logon request
16    #[serde(rename = "logon")]
17    Logon {
18        seq: u32,
19        #[serde(skip_serializing_if = "Option::is_none")]
20        username: Option<String>,
21        #[serde(skip_serializing_if = "Option::is_none")]
22        password: Option<String>,
23        #[serde(skip_serializing_if = "Option::is_none")]
24        auth_token: Option<String>,
25        #[serde(skip_serializing_if = "Option::is_none")]
26        channels: Option<Vec<String>>,
27    },
28
29    /// Send text message
30    #[serde(rename = "send_text_message")]
31    SendTextMessage {
32        seq: u32,
33        channel: String,
34        #[serde(rename = "for")]
35        for_user: Option<String>,
36        text: String,
37    },
38
39    /// Start outgoing audio stream
40    #[serde(rename = "start_stream")]
41    StartStream {
42        seq: u32,
43        channel: String,
44        #[serde(rename = "for")]
45        for_user: Option<String>,
46        codec: String,
47        codec_header: Option<String>,
48        packet_duration: u32,
49    },
50
51    /// Stop outgoing audio stream
52    #[serde(rename = "stop_stream")]
53    StopStream { seq: u32, stream_id: u32 },
54}
55
56impl Message {
57    /// Create a logon message with username/password/token
58    #[must_use]
59    pub fn logon_password(
60        seq: u32,
61        username: String,
62        password: String,
63        auth_token: String,
64        channel: String,
65    ) -> Self {
66        Self::Logon {
67            seq,
68            username: Some(username),
69            password: Some(password),
70            auth_token: Some(auth_token),
71            channels: Some(vec![channel]),
72        }
73    }
74
75    /// Create a logon message with token only
76    #[must_use]
77    pub fn logon_token(seq: u32, auth_token: String, channel: String) -> Self {
78        Self::Logon {
79            seq,
80            username: None,
81            password: None,
82            auth_token: Some(auth_token),
83            channels: Some(vec![channel]),
84        }
85    }
86
87    /// Create a text message
88    #[must_use]
89    pub fn send_text(seq: u32, channel: String, text: String) -> Self {
90        Self::SendTextMessage {
91            seq,
92            channel,
93            for_user: None,
94            text,
95        }
96    }
97
98    /// Create a text message for a specific callsign
99    #[must_use]
100    pub fn send_text_for_callsign(
101        seq: u32,
102        channel: String,
103        text: String,
104        for_user: String,
105    ) -> Self {
106        Self::SendTextMessage {
107            seq,
108            channel,
109            for_user: Some(for_user),
110            text,
111        }
112    }
113
114    /// Create a start stream message
115    #[must_use]
116    pub fn start_stream(seq: u32, channel: String, codec: String, packet_duration: u32) -> Self {
117        Self::StartStream {
118            seq,
119            channel,
120            for_user: None,
121            codec,
122            codec_header: None,
123            packet_duration,
124        }
125    }
126
127    /// Create a stop stream message
128    #[must_use]
129    pub fn stop_stream(seq: u32, stream_id: u32) -> Self {
130        Self::StopStream { seq, stream_id }
131    }
132
133    /// Get the sequence number if present
134    #[must_use]
135    pub fn seq(&self) -> Option<u32> {
136        match self {
137            Self::Logon { seq, .. }
138            | Self::SendTextMessage { seq, .. }
139            | Self::StartStream { seq, .. }
140            | Self::StopStream { seq, .. } => Some(*seq),
141        }
142    }
143}
144
145/// Response messages from Zello
146#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(untagged)]
148pub enum Response {
149    /// Logon response
150    Logon {
151        seq: u32,
152        success: bool,
153        refresh_token: String,
154        #[serde(skip_serializing_if = "Option::is_none")]
155        error: Option<String>,
156    },
157
158    /// Generic response
159    Generic {
160        seq: u32,
161        success: bool,
162        #[serde(skip_serializing_if = "Option::is_none")]
163        error: Option<String>,
164    },
165}
166
167impl Response {
168    /// Get the sequence number if present
169    #[must_use]
170    pub fn seq(&self) -> Option<u32> {
171        match self {
172            Self::Logon { seq, .. } | Self::Generic { seq, .. } => Some(*seq),
173        }
174    }
175
176    /// Check if this is a success response
177    #[must_use]
178    pub fn is_success(&self) -> bool {
179        match self {
180            Self::Logon { success, .. } | Self::Generic { success, .. } => *success,
181        }
182    }
183
184    /// Get error message if present
185    #[must_use]
186    pub fn error(&self) -> Option<&str> {
187        match self {
188            Self::Logon { error, .. } | Self::Generic { error, .. } => error.as_deref(),
189        }
190    }
191}
192
193/// Error messages from Zello
194#[derive(Debug, Clone, Serialize, Deserialize)]
195#[serde(tag = "command")]
196pub enum Error {
197    /// Error message
198    #[serde(rename = "on_error")]
199    Error { error: String },
200}
201
202impl Error {
203    /// Get the error message
204    #[must_use]
205    pub fn error(&self) -> &str {
206        match self {
207            Self::Error { error } => error,
208        }
209    }
210}
211
212/// Events that can be received from Zello
213#[derive(Debug, Clone, Serialize, Deserialize)]
214#[serde(tag = "command")]
215pub enum Event {
216    /// Text message received
217    #[serde(rename = "on_text_message")]
218    TextMessage {
219        message_id: u64,
220        channel: String,
221        from: String,
222        #[serde(rename = "for")]
223        for_user: Option<String>,
224        text: String,
225        #[serde(skip_serializing_if = "Option::is_none")]
226        author: Option<String>,
227    },
228
229    /// Audio stream start
230    #[serde(rename = "on_stream_start")]
231    AudioStart {
232        stream_id: u32,
233        channel: String,
234        from: String,
235        #[serde(rename = "for")]
236        for_user: Option<String>,
237        codec: String,
238        codec_header: Option<String>,
239        packet_duration: u32,
240    },
241
242    /// Audio data packet
243    #[serde(rename = "on_stream_data")]
244    AudioData {
245        stream_id: u32,
246        packet_id: u32,
247        data: Vec<u8>,
248    },
249
250    /// Audio stream stop
251    #[serde(rename = "on_stream_stop")]
252    AudioStop { stream_id: u32 },
253
254    /// Channel status update
255    #[serde(rename = "on_channel_status")]
256    ChannelStatus {
257        channel: String,
258        status: String,
259        users_online: u32,
260        #[serde(skip_serializing_if = "Option::is_none")]
261        images: Option<Vec<ChannelImage>>,
262    },
263
264    /// User online/offline status
265    #[serde(rename = "on_online_status")]
266    OnlineStatus {
267        channel: String,
268        from: String,
269        online: bool,
270    },
271}
272
273/// Top-level enum for all incoming messages
274#[derive(Debug, Clone, Serialize, Deserialize)]
275#[serde(untagged)]
276pub enum IncomingMessage {
277    Response(Response),
278    Error(Error),
279    Event(Event),
280}
281
282/// Channel image information
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct ChannelImage {
285    pub url: String,
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub thumbnail_url: Option<String>,
288}
289
290/// Opus codec header with audio parameters
291#[derive(Debug, Clone)]
292pub struct CodecHeader {
293    pub sample_rate_hz: u16,
294    pub frames_per_packet: u8,
295    pub frame_size_ms: u8,
296}
297
298impl CodecHeader {
299    /// Decode a base64-encoded codec header
300    ///
301    /// # Errors
302    ///
303    /// Returns an error if the base64 decoding fails or the header length is invalid
304    pub fn from_base64(encoded: &str) -> Result<Self> {
305        let bytes = STANDARD.decode(encoded)?;
306        Self::from_bytes(Bytes::from(bytes))
307    }
308
309    /// Parse codec header from bytes
310    ///
311    /// # Errors
312    ///
313    /// Returns an error if the bytes length is invalid
314    pub fn from_bytes(mut bytes: Bytes) -> Result<Self> {
315        if bytes.len() != 4 {
316            return Err(anyhow!(
317                "Invalid codec header length: expected 4 bytes, got {}",
318                bytes.len()
319            ));
320        }
321
322        let sample_rate_hz = bytes.get_u16_le();
323        let frames_per_packet = bytes.get_u8();
324        let frame_size_ms = bytes.get_u8();
325
326        Ok(Self {
327            sample_rate_hz,
328            frames_per_packet,
329            frame_size_ms,
330        })
331    }
332
333    /// Encode to base64 string
334    #[must_use]
335    pub fn to_base64(&self) -> String {
336        STANDARD.encode(self.to_bytes())
337    }
338
339    /// Convert to bytes
340    #[must_use]
341    pub fn to_bytes(&self) -> Bytes {
342        let mut buf = BytesMut::with_capacity(4);
343        buf.put_u16_le(self.sample_rate_hz);
344        buf.put_u8(self.frames_per_packet);
345        buf.put_u8(self.frame_size_ms);
346        buf.freeze()
347    }
348}
349
350impl Default for CodecHeader {
351    fn default() -> Self {
352        Self {
353            sample_rate_hz: 16000,
354            frames_per_packet: 1,
355            frame_size_ms: 60,
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_message_serialization() {
366        let msg = Message::send_text(1, "test_channel".to_string(), "Hello".to_string());
367        let json = serde_json::to_string(&msg).expect("Failed to serialize");
368        assert!(json.contains("send_text_message"));
369        assert!(json.contains("Hello"));
370    }
371
372    #[test]
373    fn test_message_seq() {
374        let msg = Message::send_text(42, "channel".to_string(), "test".to_string());
375        assert_eq!(msg.seq(), Some(42));
376    }
377}