Skip to main content

gtp/
message.rs

1//! GTP message codec.
2
3use gbp::CodecError;
4use serde::{Deserialize, Serialize};
5use serde_bytes::ByteBuf;
6
7/// Body content type.
8#[repr(u8)]
9#[derive(Copy, Clone, Debug, PartialEq, Eq)]
10pub enum GtpContentType {
11    /// UTF-8 plaintext.
12    Plain = 0,
13    /// CommonMark.
14    Markdown = 1,
15    /// Opaque binary blob.
16    Binary = 2,
17    /// Reference to an out-of-band attachment.
18    AttachmentRef = 3,
19}
20
21/// GTP message envelope. Eight CBOR keys.
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct GtpMessage {
24    /// Message identifier (used for idempotency).
25    #[serde(rename = "mid")]
26    pub message_id: u64,
27    /// Sender member identifier.
28    #[serde(rename = "sid")]
29    pub sender_id: u32,
30    /// Send timestamp in milliseconds since the Unix epoch.
31    #[serde(rename = "ts")]
32    pub timestamp_ms: u64,
33    /// Request identifier (echoed in ACK / NACK).
34    #[serde(rename = "rid")]
35    pub request_id: u32,
36    /// Message flag bits (`urgent` / `ephemeral` / `persistent`).
37    #[serde(rename = "fl")]
38    pub flags: u8,
39    /// content_type (see [`GtpContentType`]).
40    #[serde(rename = "ct")]
41    pub content_type: u8,
42    /// Declared length of [`content`](Self::content).
43    #[serde(rename = "len")]
44    pub content_length: u32,
45    /// Body bytes.
46    #[serde(rename = "body")]
47    pub content: ByteBuf,
48}
49
50impl GtpMessage {
51    /// Builds a plaintext (UTF-8) message.
52    pub fn plain(sender_id: u32, message_id: u64, text: &str) -> Self {
53        let body = text.as_bytes().to_vec();
54        Self {
55            message_id,
56            sender_id,
57            timestamp_ms: 0,
58            request_id: 0,
59            flags: 0x01,
60            content_type: GtpContentType::Plain as u8,
61            content_length: body.len() as u32,
62            content: ByteBuf::from(body),
63        }
64    }
65
66    /// CBOR-encodes the message.
67    pub fn to_cbor(&self) -> Vec<u8> {
68        let mut buf = Vec::new();
69        ciborium::into_writer(self, &mut buf).expect("cbor encode");
70        buf
71    }
72
73    /// Decodes a CBOR-encoded message and validates `content_length`.
74    pub fn from_cbor(data: &[u8]) -> Result<Self, CodecError> {
75        let m: Self = ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))?;
76        if m.content_length as usize != m.content.len() {
77            return Err(CodecError::PayloadSizeMismatch);
78        }
79        Ok(m)
80    }
81
82    /// Returns the body as a `&str` when it is valid UTF-8.
83    pub fn text(&self) -> Option<&str> {
84        std::str::from_utf8(&self.content).ok()
85    }
86}