tele 0.1.23

Ergonomic Telegram Bot API SDK for Rust, built on reqx
Documentation
use std::collections::BTreeMap;

use serde::Deserialize;
use serde_json::Value;

use crate::types::common::MessageId;
use crate::types::sticker::Sticker;
use crate::types::telegram::LinkPreviewOptions;

use super::common::{Chat, MessageEntity, MessageOrigin, PhotoSize};
use super::content::{Checklist, Contact, Dice, Game, Location, Poll, Venue};
use super::media::{
    Animation, Audio, Document, LivePhoto, PaidMediaInfo, Story, Video, VideoNote, Voice,
};
use super::model::Message;
use super::payments::Invoice;
use super::service::{Giveaway, GiveawayWinners};

#[derive(Clone, Debug, Deserialize)]
#[non_exhaustive]
pub struct TextQuote {
    pub text: String,
    #[serde(default)]
    pub entities: Option<Vec<MessageEntity>>,
    pub position: u32,
    #[serde(default)]
    pub is_manual: bool,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Clone, Debug, Deserialize)]
#[non_exhaustive]
pub struct ExternalReplyInfo {
    pub origin: MessageOrigin,
    #[serde(default)]
    pub chat: Option<Chat>,
    #[serde(default)]
    pub message_id: Option<MessageId>,
    #[serde(default)]
    pub link_preview_options: Option<LinkPreviewOptions>,
    #[serde(default)]
    pub animation: Option<Animation>,
    #[serde(default)]
    pub audio: Option<Audio>,
    #[serde(default)]
    pub document: Option<Document>,
    #[serde(default)]
    pub live_photo: Option<LivePhoto>,
    #[serde(default)]
    pub paid_media: Option<PaidMediaInfo>,
    #[serde(default)]
    pub photo: Option<Vec<PhotoSize>>,
    #[serde(default)]
    pub sticker: Option<Sticker>,
    #[serde(default)]
    pub story: Option<Story>,
    #[serde(default)]
    pub video: Option<Video>,
    #[serde(default)]
    pub video_note: Option<VideoNote>,
    #[serde(default)]
    pub voice: Option<Voice>,
    #[serde(default)]
    pub has_media_spoiler: bool,
    #[serde(default)]
    pub checklist: Option<Checklist>,
    #[serde(default)]
    pub contact: Option<Contact>,
    #[serde(default)]
    pub dice: Option<Dice>,
    #[serde(default)]
    pub game: Option<Game>,
    #[serde(default)]
    pub giveaway: Option<Giveaway>,
    #[serde(default)]
    pub giveaway_winners: Option<GiveawayWinners>,
    #[serde(default)]
    pub invoice: Option<Invoice>,
    #[serde(default)]
    pub location: Option<Location>,
    #[serde(default)]
    pub poll: Option<Poll>,
    #[serde(default)]
    pub venue: Option<Venue>,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Clone, Debug, Deserialize)]
#[non_exhaustive]
pub struct InaccessibleMessage {
    pub chat: Chat,
    pub message_id: MessageId,
    pub date: i64,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum MaybeInaccessibleMessage {
    Accessible(Box<Message>),
    Inaccessible(InaccessibleMessage),
}

impl<'de> Deserialize<'de> for MaybeInaccessibleMessage {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = Value::deserialize(deserializer)?;
        let date = value
            .get("date")
            .and_then(Value::as_i64)
            .unwrap_or_default();
        if date == 0 {
            InaccessibleMessage::deserialize(value)
                .map(Self::Inaccessible)
                .map_err(serde::de::Error::custom)
        } else {
            Message::deserialize(value)
                .map(|message| Self::Accessible(Box::new(message)))
                .map_err(serde::de::Error::custom)
        }
    }
}

impl MaybeInaccessibleMessage {
    pub fn is_accessible(&self) -> bool {
        matches!(self, Self::Accessible(_))
    }

    pub fn is_inaccessible(&self) -> bool {
        matches!(self, Self::Inaccessible(_))
    }

    pub fn as_accessible(&self) -> Option<&Message> {
        match self {
            Self::Accessible(message) => Some(message.as_ref()),
            Self::Inaccessible(_) => None,
        }
    }

    pub fn into_accessible(self) -> Option<Message> {
        match self {
            Self::Accessible(message) => Some(*message),
            Self::Inaccessible(_) => None,
        }
    }

    pub fn as_inaccessible(&self) -> Option<&InaccessibleMessage> {
        match self {
            Self::Accessible(_) => None,
            Self::Inaccessible(message) => Some(message),
        }
    }

    pub fn into_inaccessible(self) -> Option<InaccessibleMessage> {
        match self {
            Self::Accessible(_) => None,
            Self::Inaccessible(message) => Some(message),
        }
    }

    pub fn accessible(&self) -> Option<&Message> {
        self.as_accessible()
    }

    pub fn inaccessible(&self) -> Option<&InaccessibleMessage> {
        self.as_inaccessible()
    }

    pub fn chat(&self) -> &Chat {
        match self {
            Self::Accessible(message) => &message.chat,
            Self::Inaccessible(message) => &message.chat,
        }
    }

    pub fn message_id(&self) -> MessageId {
        match self {
            Self::Accessible(message) => message.message_id,
            Self::Inaccessible(message) => message.message_id,
        }
    }

    pub fn date(&self) -> i64 {
        match self {
            Self::Accessible(message) => message.date,
            Self::Inaccessible(message) => message.date,
        }
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::MaybeInaccessibleMessage;

    #[test]
    fn inaccessible_message_preserves_future_fields() -> Result<(), Box<dyn std::error::Error>> {
        let input = json!({
            "message_id": 55,
            "date": 0,
            "chat": {"id": -10010, "type": "supergroup", "title": "mods"},
            "future": {"kept": true}
        });

        let message: MaybeInaccessibleMessage = serde_json::from_value(input.clone())?;

        assert!(!message.is_accessible());
        assert!(message.is_inaccessible());
        assert!(message.as_accessible().is_none());
        assert_eq!(
            message
                .as_inaccessible()
                .and_then(|message| message.extra.get("future")),
            Some(&json!({"kept": true}))
        );
        assert_eq!(
            message
                .into_inaccessible()
                .and_then(|message| message.extra.get("future").cloned()),
            Some(json!({"kept": true}))
        );
        Ok(())
    }

    #[test]
    fn accessible_message_can_be_consumed() -> Result<(), Box<dyn std::error::Error>> {
        let message: MaybeInaccessibleMessage = serde_json::from_value(json!({
            "message_id": 56,
            "date": 1700000000,
            "chat": {"id": 1, "type": "private"},
            "text": "hello"
        }))?;

        assert!(message.is_accessible());
        assert!(!message.is_inaccessible());
        assert!(message.as_inaccessible().is_none());
        assert_eq!(
            message
                .into_accessible()
                .and_then(|message| message.text)
                .as_deref(),
            Some("hello")
        );
        Ok(())
    }
}