maxbot 0.1.3

Автоматизация работы с чат-ботами MAX
Documentation
use std::path::PathBuf;
use serde::Serialize;

/// Источник файлового вложения: локальный файл или уже полученный токен.
#[derive(Debug, Clone)]
pub enum AttachmentSource {
    LocalFile(PathBuf),
    Token(String),
}

/// Данные контакта для вложения `contact`.
#[derive(Debug, Clone, Serialize)]
pub struct ContactData {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub contact_id: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vcf_info: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vcf_phone: Option<String>,
}

/// Кнопка inline-клавиатуры.
#[derive(Debug, Clone, Serialize)]
pub struct InlineKeyboardButton {
    pub r#type: String,          // "callback", "link", "request_geo_location", "open_app", "message"
    pub text: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub payload: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub quick: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub web_app: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub contact_id: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub app_payload: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message_text: Option<String>,
}

/// Inline-клавиатура (двумерный массив кнопок).
#[derive(Debug, Clone, Serialize)]
pub struct InlineKeyboard {
    pub buttons: Vec<Vec<InlineKeyboardButton>>,
}

/// Данные для вложения `share` (отсылка/предпросмотр ссылки).
#[derive(Debug, Clone, Serialize)]
pub struct ShareData {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token: Option<String>,
}

/// Тип вложения.
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", content = "payload")]
pub enum Attachment {
    #[serde(rename = "image")]
    Image {
        #[serde(skip)]
        source: AttachmentSource,
        token: Option<String>,
    },
    #[serde(rename = "video")]
    Video {
        #[serde(skip)]
        source: AttachmentSource,
        token: Option<String>,
    },
    #[serde(rename = "audio")]
    Audio {
        #[serde(skip)]
        source: AttachmentSource,
        token: Option<String>,
    },
    #[serde(rename = "file")]
    File {
        #[serde(skip)]
        source: AttachmentSource,
        token: Option<String>,
    },
    #[serde(rename = "sticker")]
    Sticker { code: String },
    #[serde(rename = "contact")]
    Contact(ContactData),
    #[serde(rename = "inline_keyboard")]
    InlineKeyboard(InlineKeyboard),
    #[serde(rename = "location")]
    Location { latitude: f64, longitude: f64 },
    #[serde(rename = "share")]
    Share(ShareData),
}

impl Attachment {
    /// Возвращает строковый тип вложения (как требуется API MAX).
    pub fn get_type(&self) -> &'static str {
        match self {
            Attachment::Image { .. } => "image",
            Attachment::Video { .. } => "video",
            Attachment::Audio { .. } => "audio",
            Attachment::File { .. } => "file",
            Attachment::Sticker { .. } => "sticker",
            Attachment::Contact(_) => "contact",
            Attachment::InlineKeyboard(_) => "inline_keyboard",
            Attachment::Location { .. } => "location",
            Attachment::Share(_) => "share",
        }
    }

    // Конструкторы для файловых вложений

    pub fn image_local(path: impl Into<PathBuf>) -> Self {
        Attachment::Image {
            source: AttachmentSource::LocalFile(path.into()),
            token: None,
        }
    }

    pub fn image_token(token: impl Into<String>) -> Self {
        Attachment::Image {
            source: AttachmentSource::Token(token.into()),
            token: None,
        }
    }

    pub fn video_local(path: impl Into<PathBuf>) -> Self {
        Attachment::Video {
            source: AttachmentSource::LocalFile(path.into()),
            token: None,
        }
    }

    pub fn video_token(token: impl Into<String>) -> Self {
        Attachment::Video {
            source: AttachmentSource::Token(token.into()),
            token: None,
        }
    }

    pub fn audio_local(path: impl Into<PathBuf>) -> Self {
        Attachment::Audio {
            source: AttachmentSource::LocalFile(path.into()),
            token: None,
        }
    }

    pub fn audio_token(token: impl Into<String>) -> Self {
        Attachment::Audio {
            source: AttachmentSource::Token(token.into()),
            token: None,
        }
    }

    pub fn file_local(path: impl Into<PathBuf>) -> Self {
        Attachment::File {
            source: AttachmentSource::LocalFile(path.into()),
            token: None,
        }
    }

    pub fn file_token(token: impl Into<String>) -> Self {
        Attachment::File {
            source: AttachmentSource::Token(token.into()),
            token: None,
        }
    }

    // Конструкторы для других типов вложений

    pub fn sticker(code: impl Into<String>) -> Self {
        Attachment::Sticker { code: code.into() }
    }

    pub fn contact(data: ContactData) -> Self {
        Attachment::Contact(data)
    }

    pub fn inline_keyboard(keyboard: InlineKeyboard) -> Self {
        Attachment::InlineKeyboard(keyboard)
    }

    pub fn location(latitude: f64, longitude: f64) -> Self {
        Attachment::Location { latitude, longitude }
    }

    pub fn share(data: ShareData) -> Self {
        Attachment::Share(data)
    }
}

// Построитель для клавиатуры
pub struct InlineKeyboardBuilder {
    rows: Vec<Vec<InlineKeyboardButton>>,
}

impl InlineKeyboardBuilder {
    pub fn new() -> Self {
        Self { rows: vec![] }
    }

    /// Добавляет строку кнопок.
    pub fn row(mut self, buttons: Vec<InlineKeyboardButton>) -> Self {
        self.rows.push(buttons);
        self
    }

    /// Добавляет кнопку в последнюю строку; если строк нет, создаёт новую.
    pub fn button(mut self, btn: InlineKeyboardButton) -> Self {
        if self.rows.is_empty() {
            self.rows.push(vec![]);
        }
        self.rows.last_mut().unwrap().push(btn);
        self
    }

    /// Добавляет несколько кнопок в последнюю строку.
    pub fn buttons(mut self, btns: Vec<InlineKeyboardButton>) -> Self {
        if self.rows.is_empty() {
            self.rows.push(vec![]);
        }
        self.rows.last_mut().unwrap().extend(btns);
        self
    }

    pub fn build(self) -> InlineKeyboard {
        InlineKeyboard { buttons: self.rows }
    }
}

impl Default for InlineKeyboardBuilder {
    fn default() -> Self {
        Self::new()
    }
}

// Конструкторы кнопок
impl InlineKeyboardButton {
    pub fn callback(text: impl Into<String>, payload: impl Into<String>) -> Self {
        Self {
            r#type: "callback".to_string(),
            text: text.into(),
            payload: Some(payload.into()),
            url: None,
            quick: None,
            web_app: None,
            contact_id: None,
            app_payload: None,
            message_text: None,
        }
    }

    pub fn link(text: impl Into<String>, url: impl Into<String>) -> Self {
        Self {
            r#type: "link".to_string(),
            text: text.into(),
            payload: None,
            url: Some(url.into()),
            quick: None,
            web_app: None,
            contact_id: None,
            app_payload: None,
            message_text: None,
        }
    }

    pub fn request_contact(text: impl Into<String>) -> Self {
        Self {
            r#type: "request_contact".to_string(),
            text: text.into(),
            payload: None,
            url: None,
            quick: Some(true),
            web_app: None,
            contact_id: None,
            app_payload: None,
            message_text: None,
        }
    }

    pub fn request_location(text: impl Into<String>) -> Self {
        Self {
            r#type: "request_geo_location".to_string(),
            text: text.into(),
            payload: None,
            url: None,
            quick: Some(true),
            web_app: None,
            contact_id: None,
            app_payload: None,
            message_text: None,
        }
    }

    pub fn open_app(
        text: impl Into<String>,
        web_app_url: impl Into<String>,
        contact_id: i64,
        payload: Option<String>,
    ) -> Self {
        Self {
            r#type: "open_app".to_string(),
            text: text.into(),
            payload,
            url: None,
            quick: None,
            web_app: Some(web_app_url.into()),
            contact_id: Some(contact_id),
            app_payload: None,
            message_text: None,
        }
    }

    pub fn message(text: impl Into<String>, message_text: impl Into<String>) -> Self {
        Self {
            r#type: "message".to_string(),
            text: text.into(),
            payload: None,
            url: None,
            quick: None,
            web_app: None,
            contact_id: None,
            app_payload: None,
            message_text: Some(message_text.into()),
        }
    }
}