mzrs-sdk 0.1.21

High-level Rust SDK for Mezon platform
Documentation
//! Channel-scoped handle.

use serde_json::{json, Value};

use mzrs_proto::{api, realtime as rt};

use mzrs_proto::api::MessageAttachment;

use crate::builders::message::MessageBuilder;
use crate::client::{
    DeleteMessageRequest, JoinChatRequest, LeaveChatRequest, MzrsClient, ReactMessageRequest,
    SendMessageRequest, UpdateMessageRequest,
};
use crate::error::SdkError;
use crate::upload::AttachmentSource;

/// Default channel type (text).
const DEFAULT_CHANNEL_TYPE: i32 = 1;
/// Default stream mode (channel).
const DEFAULT_MODE: i32 = 2;
/// Default visibility.
const DEFAULT_IS_PUBLIC: bool = true;

/// A handle scoped to a single channel within a clan.
///
/// Created via [`MzrsClient::channel`] or [`ClanHandle::channel`].
/// All operations automatically supply the clan ID, channel ID, mode,
/// and other defaults.
///
/// # Example
///
/// ```rust,ignore
/// let ch = client.channel("clan_id", "channel_id");
/// ch.join().await?;
/// ch.send_text("Hello from Rust!").await?;
/// ```
#[derive(Clone)]
pub struct ChannelHandle {
    client: MzrsClient,
    clan_id: String,
    channel_id: String,
    channel_type: i32,
    mode: i32,
    is_public: bool,
}

impl ChannelHandle {
    /// Create a new channel handle (called internally).
    pub(crate) fn new(client: MzrsClient, clan_id: String, channel_id: String) -> Self {
        Self {
            client,
            clan_id,
            channel_id,
            channel_type: DEFAULT_CHANNEL_TYPE,
            mode: DEFAULT_MODE,
            is_public: DEFAULT_IS_PUBLIC,
        }
    }

    /// Return the clan ID this channel belongs to.
    pub fn clan_id(&self) -> &str {
        &self.clan_id
    }

    /// Return the channel ID.
    pub fn id(&self) -> &str {
        &self.channel_id
    }

    /// Return a reference to the underlying client.
    pub fn client(&self) -> &MzrsClient {
        &self.client
    }

    /// Override the channel type (default: text = 1).
    pub fn with_channel_type(mut self, channel_type: i32) -> Self {
        self.channel_type = channel_type;
        self
    }

    /// Override the stream mode (default: channel = 2).
    pub fn with_mode(mut self, mode: i32) -> Self {
        self.mode = mode;
        self
    }

    /// Override the public flag (default: true).
    pub fn with_public(mut self, is_public: bool) -> Self {
        self.is_public = is_public;
        self
    }

    /// Return the stream mode used for requests from this handle.
    pub fn mode(&self) -> i32 {
        self.mode
    }

    /// Return whether requests from this handle are public.
    pub fn is_public(&self) -> bool {
        self.is_public
    }

    /// Join the channel.
    #[tracing::instrument(skip(self), fields(clan_id = %self.clan_id, channel_id = %self.channel_id))]
    pub async fn join(&self) -> Result<rt::Channel, SdkError> {
        self.client
            .join_chat(JoinChatRequest {
                clan_id: self.clan_id.clone(),
                channel_id: self.channel_id.clone(),
                channel_type: self.channel_type,
                is_public: self.is_public,
            })
            .await
    }

    /// Leave the channel.
    #[tracing::instrument(skip(self), fields(clan_id = %self.clan_id, channel_id = %self.channel_id))]
    pub async fn leave(&self) -> Result<(), SdkError> {
        self.client
            .leave_chat(LeaveChatRequest {
                clan_id: self.clan_id.clone(),
                channel_id: self.channel_id.clone(),
                channel_type: self.channel_type,
                is_public: self.is_public,
            })
            .await
    }

    /// Send a plain text message.
    #[tracing::instrument(skip(self, content))]
    pub async fn send_text(
        &self,
        content: impl Into<String>,
    ) -> Result<rt::ChannelMessageAck, SdkError> {
        self.send_json(json!({ "t": content.into() })).await
    }

    /// Send a message with a raw JSON content value.
    #[tracing::instrument(skip(self, content))]
    pub async fn send_json(&self, content: Value) -> Result<rt::ChannelMessageAck, SdkError> {
        self.client
            .send_message(SendMessageRequest {
                clan_id: self.clan_id.clone(),
                channel_id: self.channel_id.clone(),
                mode: self.mode,
                is_public: self.is_public,
                content,
                mentions: vec![],
                attachments: vec![],
                references: vec![],
                anonymous_message: false,
                mention_everyone: false,
                avatar: None,
                code: 0,
                topic_id: None,
                id: None,
            })
            .await
    }

    /// Send a message built from a [`MessageContent`](crate::builders::message::MessageContent).
    ///
    /// This is the primary way to send rich messages with embeds, buttons,
    /// and formatting.
    #[tracing::instrument(skip(self, content))]
    pub async fn send_content(
        &self,
        content: crate::builders::message::MessageContent,
    ) -> Result<rt::ChannelMessageAck, SdkError> {
        let prepared = content.build_prepared();
        let json_value: Value = serde_json::from_str(&prepared.json).unwrap_or(Value::Null);

        self.client
            .send_message(SendMessageRequest {
                clan_id: self.clan_id.clone(),
                channel_id: self.channel_id.clone(),
                mode: self.mode,
                is_public: self.is_public,
                content: json_value,
                mentions: prepared.mentions,
                attachments: vec![],
                references: vec![],
                anonymous_message: false,
                mention_everyone: false,
                avatar: None,
                code: 0,
                topic_id: None,
                id: None,
            })
            .await
    }

    /// Update a message with new JSON content.
    #[tracing::instrument(skip(self, content))]
    pub async fn update_json(
        &self,
        message_id: &str,
        content: Value,
    ) -> Result<rt::ChannelMessageAck, SdkError> {
        self.client
            .update_message(UpdateMessageRequest {
                clan_id: self.clan_id.clone(),
                channel_id: self.channel_id.clone(),
                message_id: message_id.to_string(),
                mode: self.mode,
                is_public: self.is_public,
                content,
                mentions: vec![],
                attachments: vec![],
                hide_editted: true,
                topic_id: None,
                is_update_msg_topic: false,
            })
            .await
    }

    /// Delete a message.
    #[tracing::instrument(skip(self))]
    pub async fn delete(&self, message_id: &str) -> Result<rt::ChannelMessageAck, SdkError> {
        self.client
            .delete_message(DeleteMessageRequest {
                clan_id: self.clan_id.clone(),
                channel_id: self.channel_id.clone(),
                message_id: message_id.to_string(),
                mode: self.mode,
                is_public: self.is_public,
                has_attachment: false,
                topic_id: None,
                mentions: vec![],
                references: vec![],
            })
            .await
    }

    /// React to a message with an emoji.
    #[tracing::instrument(skip(self, emoji))]
    pub async fn react(
        &self,
        message_id: &str,
        emoji_id: &str,
        emoji: impl Into<String>,
        message_sender_id: &str,
        action_delete: bool,
    ) -> Result<api::MessageReaction, SdkError> {
        self.client
            .react_message(ReactMessageRequest {
                id: None,
                clan_id: self.clan_id.clone(),
                channel_id: self.channel_id.clone(),
                mode: self.mode,
                is_public: self.is_public,
                message_id: message_id.to_string(),
                emoji_id: emoji_id.to_string(),
                emoji: emoji.into(),
                count: 1,
                message_sender_id: message_sender_id.to_string(),
                sender_id: None,
                sender_name: None,
                sender_avatar: None,
                action_delete,
                topic_id: None,
                emoji_recent_id: None,
            })
            .await
    }

    /// Update this channel's avatar image URL.
    ///
    /// Pass `Some(url)` to set an avatar, or `None` to clear it.
    #[tracing::instrument(skip(self, avatar))]
    pub async fn update_avatar(&self, avatar: Option<String>) -> Result<(), SdkError> {
        self.client
            .update_channel_avatar(&self.clan_id, &self.channel_id, avatar)
            .await
    }

    /// Direct attachment upload is not supported by the current Mezon bot API.
    ///
    /// This wrapper is kept only for API compatibility and forwards the same
    /// unsupported error as [`MzrsClient::upload_attachment`].
    #[deprecated(
        note = "Mezon bot attachment upload is not supported; attach externally hosted URLs instead"
    )]
    #[tracing::instrument(skip(self, _source, _filename, _filetype))]
    pub async fn upload_attachment(
        &self,
        _source: impl Into<AttachmentSource>,
        _filename: impl Into<String>,
        _filetype: impl Into<String>,
    ) -> Result<MessageAttachment, SdkError> {
        Err(SdkError::Core(mzrs_core::CoreError::Unsupported(
            "Mezon does not support attachment upload for bots".to_string(),
        )))
    }

    /// Start building a message for this channel.
    ///
    /// Returns a [`MessageBuilder`] that can be finalized with `.send().await`.
    pub fn message(&self) -> MessageBuilder {
        MessageBuilder::new(self.clone())
    }
}