mzrs-sdk 0.1.21

High-level Rust SDK for Mezon platform
Documentation
//! Message content parsing functions.
//!
//! These functions decode the various binary and JSON formats used by Mezon
//! for message metadata fields (`mentions`, `attachments`, `references`, `content`).

use mzrs_proto::prost::Message;
use serde::Deserialize;

// ── Parsed types ────────────────────────────────────────────────────

/// A mention extracted from an incoming message.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ParsedMention {
    /// User ID (if this is a user mention).
    pub user_id: Option<String>,
    /// Username (if this is a user mention).
    pub username: Option<String>,
    /// Role ID (if this is a role mention).
    pub role_id: Option<String>,
    /// Role name (if this is a role mention).
    pub rolename: Option<String>,
    /// Start UTF-16 offset in the message text.
    pub s: Option<i32>,
    /// End UTF-16 offset in the message text.
    pub e: Option<i32>,
}

/// An attachment extracted from an incoming message.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ParsedAttachment {
    /// File name.
    pub filename: Option<String>,
    /// MIME type.
    pub filetype: Option<String>,
    /// Download URL.
    pub url: Option<String>,
    /// File size in bytes.
    pub size: Option<i64>,
    /// Image/video width in pixels.
    pub width: Option<i32>,
    /// Image/video height in pixels.
    pub height: Option<i32>,
    /// Media duration in seconds.
    pub duration: Option<i32>,
}

/// A reference (reply) extracted from an incoming message.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ParsedReference {
    /// The message being referenced.
    pub message_id: Option<String>,
    /// The reference message ID.
    pub message_ref_id: Option<String>,
    /// Content preview of the referenced message.
    pub content: Option<String>,
    /// Whether the referenced message has an attachment.
    pub has_attachment: Option<bool>,
    /// Reference type (0 = reply).
    pub ref_type: Option<i32>,
    /// The original message sender's ID.
    pub message_sender_id: Option<String>,
    /// The original message sender's username.
    pub message_sender_username: Option<String>,
    /// The original message sender's avatar URL.
    pub message_sender_avatar: Option<String>,
    /// The original message sender's display name.
    pub message_sender_display_name: Option<String>,
}

// ── Helpers ─────────────────────────────────────────────────────────

fn opt_str(s: String) -> Option<String> {
    if s.is_empty() {
        None
    } else {
        Some(s)
    }
}

fn opt_i64(v: i64) -> Option<String> {
    if v == 0 {
        None
    } else {
        Some(v.to_string())
    }
}

fn opt_i32(v: i32) -> Option<i32> {
    if v == 0 {
        None
    } else {
        Some(v)
    }
}

// ── parse_text ──────────────────────────────────────────────────────

/// Extract the plain-text portion from a Mezon message content JSON string.
///
/// Mezon encodes plain text as `{"t": "..."}`.
///
/// # Example
///
/// ```rust
/// use mzrs_sdk::content::parse_text;
///
/// let text = parse_text(r#"{"t":"Hello world"}"#);
/// assert_eq!(text, "Hello world");
/// ```
pub fn parse_text(content: &str) -> String {
    #[derive(Deserialize, Default)]
    struct C {
        #[serde(default)]
        t: String,
    }
    serde_json::from_str::<C>(content)
        .map(|c| c.t)
        .unwrap_or_default()
}

// ── parse_mentions ──────────────────────────────────────────────────

/// Decode `ChannelMessage.mentions` bytes into a list of [`ParsedMention`].
///
/// Supports both protobuf binary encoding and legacy JSON array format.
///
/// # Example
///
/// ```rust
/// use mzrs_sdk::content::parse_mentions;
///
/// let mentions = parse_mentions(&[]);
/// assert!(mentions.is_empty());
/// ```
pub fn parse_mentions(bytes: &[u8]) -> Vec<ParsedMention> {
    if bytes.is_empty() {
        return vec![];
    }

    // Fallback: JSON array (starts with '[')
    if bytes.first() == Some(&b'[') {
        if let Ok(v) = serde_json::from_slice::<Vec<ParsedMention>>(bytes) {
            return v;
        }
    }

    mzrs_proto::api::MessageMentionList::decode(bytes)
        .map(|list| {
            list.mentions
                .into_iter()
                .map(|m| ParsedMention {
                    user_id: opt_i64(m.user_id),
                    username: opt_str(m.username),
                    role_id: opt_i64(m.role_id),
                    rolename: opt_str(m.rolename),
                    s: opt_i32(m.s),
                    e: opt_i32(m.e),
                })
                .collect()
        })
        .unwrap_or_default()
}

// ── parse_attachments ───────────────────────────────────────────────

/// Decode `ChannelMessage.attachments` bytes into a list of [`ParsedAttachment`].
///
/// Supports both protobuf binary encoding and legacy JSON array format.
///
/// # Example
///
/// ```rust
/// use mzrs_sdk::content::parse_attachments;
///
/// let attachments = parse_attachments(&[]);
/// assert!(attachments.is_empty());
/// ```
pub fn parse_attachments(bytes: &[u8]) -> Vec<ParsedAttachment> {
    if bytes.is_empty() {
        return vec![];
    }

    if bytes.first() == Some(&b'[') {
        if let Ok(v) = serde_json::from_slice::<Vec<ParsedAttachment>>(bytes) {
            return v;
        }
    }

    mzrs_proto::api::MessageAttachmentList::decode(bytes)
        .map(|list| {
            list.attachments
                .into_iter()
                .map(|a| ParsedAttachment {
                    filename: opt_str(a.filename),
                    filetype: opt_str(a.filetype),
                    url: opt_str(a.url),
                    size: if a.size != 0 {
                        Some(a.size as i64)
                    } else {
                        None
                    },
                    width: if a.width != 0 { Some(a.width) } else { None },
                    height: if a.height != 0 { Some(a.height) } else { None },
                    duration: if a.duration != 0 {
                        Some(a.duration)
                    } else {
                        None
                    },
                })
                .collect()
        })
        .unwrap_or_default()
}

// ── parse_references ────────────────────────────────────────────────

/// Decode `ChannelMessage.references` bytes into a list of [`ParsedReference`].
///
/// Supports both protobuf binary encoding and legacy JSON array format.
///
/// # Example
///
/// ```rust
/// use mzrs_sdk::content::parse_references;
///
/// let refs = parse_references(&[]);
/// assert!(refs.is_empty());
/// ```
pub fn parse_references(bytes: &[u8]) -> Vec<ParsedReference> {
    if bytes.is_empty() {
        return vec![];
    }

    if bytes.first() == Some(&b'[') {
        if let Ok(v) = serde_json::from_slice::<Vec<ParsedReference>>(bytes) {
            return v;
        }
    }

    mzrs_proto::api::MessageRefList::decode(bytes)
        .map(|list| {
            list.refs
                .into_iter()
                .map(|r| ParsedReference {
                    message_id: opt_i64(r.message_id),
                    message_ref_id: opt_i64(r.message_ref_id),
                    content: opt_str(r.content),
                    has_attachment: if r.has_attachment { Some(true) } else { None },
                    ref_type: opt_i32(r.ref_type),
                    message_sender_id: opt_i64(r.message_sender_id),
                    message_sender_username: opt_str(r.message_sender_username),
                    message_sender_avatar: opt_str(r.mesages_sender_avatar),
                    message_sender_display_name: opt_str(r.message_sender_display_name),
                })
                .collect()
        })
        .unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_text_extracts_t_field() {
        assert_eq!(parse_text(r#"{"t":"hello"}"#), "hello");
    }

    #[test]
    fn parse_text_empty_on_invalid() {
        assert_eq!(parse_text("not json"), "");
    }

    #[test]
    fn parse_text_empty_on_missing_t() {
        assert_eq!(parse_text(r#"{"mk":[]}"#), "");
    }

    #[test]
    fn parse_mentions_empty_bytes() {
        assert!(parse_mentions(&[]).is_empty());
    }

    #[test]
    fn parse_mentions_json_fallback() {
        let json = r#"[{"user_id":"42","username":"alice","s":0,"e":6}]"#;
        let m = parse_mentions(json.as_bytes());
        assert_eq!(m.len(), 1);
        assert_eq!(m[0].user_id.as_deref(), Some("42"));
    }

    #[test]
    fn parse_attachments_empty_bytes() {
        assert!(parse_attachments(&[]).is_empty());
    }

    #[test]
    fn parse_references_empty_bytes() {
        assert!(parse_references(&[]).is_empty());
    }
}