Skip to main content

maxoxide/
types.rs

1use std::{collections::BTreeMap, fmt};
2
3use serde::{
4    Deserialize, Deserializer, Serialize, Serializer,
5    de::{DeserializeOwned, Error as DeError},
6    ser::SerializeStruct,
7};
8
9// ────────────────────────────────────────────────
10// String enums with unknown-value preservation
11// ────────────────────────────────────────────────
12
13fn deserialize_string_enum<'de, D, T, F>(deserializer: D, from_str: F) -> Result<T, D::Error>
14where
15    D: Deserializer<'de>,
16    F: FnOnce(String) -> T,
17{
18    let value = String::deserialize(deserializer)?;
19    Ok(from_str(value))
20}
21
22fn serialize_string_enum<S>(serializer: S, value: &str) -> Result<S::Ok, S::Error>
23where
24    S: Serializer,
25{
26    serializer.serialize_str(value)
27}
28
29// ────────────────────────────────────────────────
30// User / Bot info
31// ────────────────────────────────────────────────
32
33/// Represents a Max user or bot.
34#[derive(Debug, Clone, Serialize)]
35pub struct User {
36    /// Global MAX user identifier.
37    ///
38    /// Do not confuse this with `chat_id`: one user can appear in different
39    /// private dialogs or group chats, each with its own `chat_id`.
40    pub user_id: i64,
41    pub first_name: String,
42    pub last_name: Option<String>,
43    pub username: Option<String>,
44    pub is_bot: Option<bool>,
45    pub last_activity_time: Option<i64>,
46    pub description: Option<String>,
47    pub avatar_url: Option<String>,
48    pub full_avatar_url: Option<String>,
49    pub commands: Option<Vec<BotCommand>>,
50}
51
52impl User {
53    /// Returns a user-facing display name from first and last name.
54    pub fn display_name(&self) -> String {
55        match self.last_name.as_deref() {
56            Some(last_name) if !last_name.is_empty() => {
57                format!("{} {}", self.first_name, last_name)
58            }
59            _ => self.first_name.clone(),
60        }
61    }
62}
63
64impl<'de> Deserialize<'de> for User {
65    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66    where
67        D: Deserializer<'de>,
68    {
69        #[derive(Deserialize)]
70        struct WireUser {
71            user_id: i64,
72            #[serde(default)]
73            first_name: Option<String>,
74            #[serde(default)]
75            last_name: Option<String>,
76            #[serde(default)]
77            name: Option<String>,
78            #[serde(default)]
79            username: Option<String>,
80            #[serde(default)]
81            is_bot: Option<bool>,
82            #[serde(default)]
83            last_activity_time: Option<i64>,
84            #[serde(default)]
85            description: Option<String>,
86            #[serde(default)]
87            avatar_url: Option<String>,
88            #[serde(default)]
89            full_avatar_url: Option<String>,
90            #[serde(default)]
91            commands: Option<Vec<BotCommand>>,
92        }
93
94        let wire = WireUser::deserialize(deserializer)?;
95        let first_name = wire
96            .first_name
97            .or(wire.name)
98            .ok_or_else(|| D::Error::missing_field("first_name"))?;
99
100        Ok(Self {
101            user_id: wire.user_id,
102            first_name,
103            last_name: wire.last_name,
104            username: wire.username,
105            is_bot: wire.is_bot,
106            last_activity_time: wire.last_activity_time,
107            description: wire.description,
108            avatar_url: wire.avatar_url,
109            full_avatar_url: wire.full_avatar_url,
110            commands: wire.commands,
111        })
112    }
113}
114
115// ────────────────────────────────────────────────
116// Chat
117// ────────────────────────────────────────────────
118
119#[non_exhaustive]
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub enum ChatType {
122    Dialog,
123    Chat,
124    Channel,
125    Unknown(String),
126}
127
128impl ChatType {
129    pub fn as_str(&self) -> &str {
130        match self {
131            Self::Dialog => "dialog",
132            Self::Chat => "chat",
133            Self::Channel => "channel",
134            Self::Unknown(value) => value.as_str(),
135        }
136    }
137}
138
139impl Serialize for ChatType {
140    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
141    where
142        S: Serializer,
143    {
144        serialize_string_enum(serializer, self.as_str())
145    }
146}
147
148impl<'de> Deserialize<'de> for ChatType {
149    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
150    where
151        D: Deserializer<'de>,
152    {
153        deserialize_string_enum(deserializer, |value| match value.as_str() {
154            "dialog" => Self::Dialog,
155            "chat" => Self::Chat,
156            "channel" => Self::Channel,
157            _ => Self::Unknown(value),
158        })
159    }
160}
161
162#[non_exhaustive]
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub enum ChatStatus {
165    Active,
166    Removed,
167    Left,
168    Closed,
169    Unknown(String),
170}
171
172impl ChatStatus {
173    pub fn as_str(&self) -> &str {
174        match self {
175            Self::Active => "active",
176            Self::Removed => "removed",
177            Self::Left => "left",
178            Self::Closed => "closed",
179            Self::Unknown(value) => value.as_str(),
180        }
181    }
182}
183
184impl Serialize for ChatStatus {
185    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
186    where
187        S: Serializer,
188    {
189        serialize_string_enum(serializer, self.as_str())
190    }
191}
192
193impl<'de> Deserialize<'de> for ChatStatus {
194    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
195    where
196        D: Deserializer<'de>,
197    {
198        deserialize_string_enum(deserializer, |value| match value.as_str() {
199            "active" => Self::Active,
200            "removed" => Self::Removed,
201            "left" => Self::Left,
202            "closed" => Self::Closed,
203            _ => Self::Unknown(value),
204        })
205    }
206}
207
208/// Represents a Max chat (dialog or group).
209#[derive(Debug, Clone, Deserialize, Serialize)]
210pub struct Chat {
211    /// Identifier of a concrete dialog, group, or channel.
212    ///
213    /// Do not confuse this with a user's global `user_id`.
214    pub chat_id: i64,
215    pub r#type: ChatType,
216    pub status: Option<ChatStatus>,
217    pub title: Option<String>,
218    pub icon: Option<Image>,
219    pub last_event_time: Option<i64>,
220    pub participants_count: Option<i32>,
221    pub owner_id: Option<i64>,
222    pub is_public: Option<bool>,
223    pub link: Option<String>,
224    pub description: Option<String>,
225    pub dialog_with_user: Option<User>,
226    pub chat_message_id: Option<String>,
227    pub pinned_message: Option<Box<Message>>,
228}
229
230#[derive(Debug, Clone, Deserialize, Serialize)]
231pub struct Image {
232    pub url: String,
233}
234
235/// Response from GET /chats.
236#[derive(Debug, Clone, Deserialize)]
237pub struct ChatList {
238    pub chats: Vec<Chat>,
239    pub marker: Option<i64>,
240}
241
242/// Body for PATCH /chats/{chatId}.
243#[derive(Debug, Clone, Serialize, Default)]
244pub struct EditChatBody {
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub icon: Option<PhotoAttachmentPayload>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub title: Option<String>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub description: Option<String>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub pin: Option<String>,
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub notify: Option<bool>,
255}
256
257// ────────────────────────────────────────────────
258// Message
259// ────────────────────────────────────────────────
260
261/// Text format for outgoing messages.
262///
263/// Omit `format` for plain text.
264#[non_exhaustive]
265#[derive(Debug, Clone, PartialEq, Eq, Default)]
266pub enum MessageFormat {
267    #[default]
268    Markdown,
269    Html,
270    Unknown(String),
271}
272
273impl MessageFormat {
274    pub fn as_str(&self) -> &str {
275        match self {
276            Self::Markdown => "markdown",
277            Self::Html => "html",
278            Self::Unknown(value) => value.as_str(),
279        }
280    }
281}
282
283impl Serialize for MessageFormat {
284    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
285    where
286        S: Serializer,
287    {
288        serialize_string_enum(serializer, self.as_str())
289    }
290}
291
292impl<'de> Deserialize<'de> for MessageFormat {
293    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
294    where
295        D: Deserializer<'de>,
296    {
297        deserialize_string_enum(deserializer, |value| match value.as_str() {
298            "markdown" => Self::Markdown,
299            "html" => Self::Html,
300            _ => Self::Unknown(value),
301        })
302    }
303}
304
305/// Represents a received message.
306#[derive(Debug, Clone, Deserialize, Serialize)]
307pub struct Message {
308    pub sender: Option<User>,
309    pub recipient: Recipient,
310    pub timestamp: i64,
311    pub link: Option<LinkedMessage>,
312    pub body: MessageBody,
313    pub stat: Option<MessageStat>,
314    pub url: Option<String>,
315    pub constructor: Option<serde_json::Value>,
316}
317
318impl Message {
319    /// Shortcut: get the `chat_id` this message was sent in.
320    ///
321    /// This is the dialog/group/channel identifier, not the sender's global
322    /// MAX `user_id`.
323    pub fn chat_id(&self) -> i64 {
324        self.recipient.chat_id
325    }
326
327    /// Shortcut: get the message_id.
328    pub fn message_id(&self) -> &str {
329        &self.body.mid
330    }
331
332    /// Shortcut: get text content of the message.
333    pub fn text(&self) -> Option<&str> {
334        self.body.text.as_deref()
335    }
336
337    /// Shortcut: get sender's global MAX user ID.
338    pub fn sender_user_id(&self) -> Option<i64> {
339        self.sender.as_ref().map(|sender| sender.user_id)
340    }
341
342    /// Returns true when this message contains at least one attachment.
343    pub fn has_attachments(&self) -> bool {
344        self.body
345            .attachments
346            .as_ref()
347            .map(|attachments| !attachments.is_empty())
348            .unwrap_or(false)
349    }
350}
351
352/// Target chat metadata attached to a received message.
353///
354/// In private dialogs, `chat_id` is the ID of the dialog itself, while
355/// `user_id` can carry the global MAX user ID of the peer.
356#[derive(Debug, Clone, Deserialize, Serialize)]
357pub struct Recipient {
358    /// ID of the concrete dialog/group/channel that received the message.
359    pub chat_id: i64,
360    pub chat_type: ChatType,
361    /// Optional global MAX user ID for dialog recipients.
362    pub user_id: Option<i64>,
363}
364
365#[derive(Debug, Clone, Deserialize, Serialize)]
366pub struct MessageBody {
367    pub mid: String,
368    pub seq: i64,
369    pub text: Option<String>,
370    #[serde(default, deserialize_with = "deserialize_attachments_lossy")]
371    pub attachments: Option<Vec<Attachment>>,
372}
373
374#[derive(Debug, Clone, Deserialize, Serialize)]
375pub struct MessageStat {
376    pub views: Option<i32>,
377}
378
379fn deserialize_attachments_lossy<'de, D>(
380    deserializer: D,
381) -> std::result::Result<Option<Vec<Attachment>>, D::Error>
382where
383    D: Deserializer<'de>,
384{
385    let raw = Option::<Vec<serde_json::Value>>::deserialize(deserializer)?;
386
387    Ok(raw.map(|items| {
388        items
389            .into_iter()
390            .map(|value| {
391                serde_json::from_value::<Attachment>(value.clone()).unwrap_or_else(|_| {
392                    Attachment::Unknown {
393                        r#type: value
394                            .get("type")
395                            .and_then(|value| value.as_str())
396                            .unwrap_or("unknown")
397                            .to_string(),
398                        payload: value.get("payload").cloned(),
399                        raw: value,
400                    }
401                })
402            })
403            .collect()
404    }))
405}
406
407#[derive(Debug, Clone, Deserialize, Serialize)]
408pub struct LinkedMessage {
409    pub r#type: String,
410    pub sender: Option<User>,
411    pub chat_id: Option<i64>,
412    pub message: Option<MessageBody>,
413}
414
415/// Response from GET /messages.
416#[derive(Debug, Clone, Deserialize)]
417pub struct MessageList {
418    pub messages: Vec<Message>,
419}
420
421// ────────────────────────────────────────────────
422// Attachments
423// ────────────────────────────────────────────────
424
425#[non_exhaustive]
426#[derive(Debug, Clone)]
427pub enum Attachment {
428    Image {
429        payload: MediaPayload,
430    },
431    Video {
432        payload: MediaPayload,
433    },
434    Audio {
435        payload: MediaPayload,
436    },
437    File {
438        payload: FilePayload,
439    },
440    Sticker {
441        payload: StickerPayload,
442    },
443    InlineKeyboard {
444        payload: KeyboardPayload,
445    },
446    Location {
447        payload: LocationPayload,
448    },
449    Contact {
450        payload: ContactPayload,
451    },
452    Unknown {
453        r#type: String,
454        payload: Option<serde_json::Value>,
455        raw: serde_json::Value,
456    },
457}
458
459impl Attachment {
460    pub fn kind(&self) -> AttachmentKind {
461        match self {
462            Self::Image { .. } => AttachmentKind::Image,
463            Self::Video { .. } => AttachmentKind::Video,
464            Self::Audio { .. } => AttachmentKind::Audio,
465            Self::File { .. } => AttachmentKind::File,
466            Self::Sticker { .. } => AttachmentKind::Sticker,
467            Self::InlineKeyboard { .. } => AttachmentKind::InlineKeyboard,
468            Self::Location { .. } => AttachmentKind::Location,
469            Self::Contact { .. } => AttachmentKind::Contact,
470            Self::Unknown { .. } => AttachmentKind::Unknown,
471        }
472    }
473}
474
475impl Serialize for Attachment {
476    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
477    where
478        S: Serializer,
479    {
480        match self {
481            Self::Image { payload } => serialize_attachment(serializer, "image", payload),
482            Self::Video { payload } => serialize_attachment(serializer, "video", payload),
483            Self::Audio { payload } => serialize_attachment(serializer, "audio", payload),
484            Self::File { payload } => serialize_attachment(serializer, "file", payload),
485            Self::Sticker { payload } => serialize_attachment(serializer, "sticker", payload),
486            Self::InlineKeyboard { payload } => {
487                serialize_attachment(serializer, "inline_keyboard", payload)
488            }
489            Self::Location { payload } => serialize_attachment(serializer, "location", payload),
490            Self::Contact { payload } => serialize_attachment(serializer, "contact", payload),
491            Self::Unknown { raw, .. } => raw.serialize(serializer),
492        }
493    }
494}
495
496fn serialize_attachment<S, T>(
497    serializer: S,
498    attachment_type: &str,
499    payload: &T,
500) -> Result<S::Ok, S::Error>
501where
502    S: Serializer,
503    T: Serialize,
504{
505    let mut state = serializer.serialize_struct("Attachment", 2)?;
506    state.serialize_field("type", attachment_type)?;
507    state.serialize_field("payload", payload)?;
508    state.end()
509}
510
511impl<'de> Deserialize<'de> for Attachment {
512    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
513    where
514        D: Deserializer<'de>,
515    {
516        let raw = serde_json::Value::deserialize(deserializer)?;
517        let attachment_type = raw
518            .get("type")
519            .and_then(|value| value.as_str())
520            .ok_or_else(|| D::Error::missing_field("type"))?;
521
522        match attachment_type {
523            "image" => Ok(Self::Image {
524                payload: deserialize_attachment_payload(&raw)?,
525            }),
526            "video" => Ok(Self::Video {
527                payload: deserialize_attachment_payload(&raw)?,
528            }),
529            "audio" => Ok(Self::Audio {
530                payload: deserialize_attachment_payload(&raw)?,
531            }),
532            "file" => Ok(Self::File {
533                payload: deserialize_attachment_payload(&raw)?,
534            }),
535            "sticker" => Ok(Self::Sticker {
536                payload: deserialize_attachment_payload(&raw)?,
537            }),
538            "inline_keyboard" => Ok(Self::InlineKeyboard {
539                payload: deserialize_attachment_payload(&raw)?,
540            }),
541            "location" => Ok(Self::Location {
542                payload: deserialize_attachment_payload(&raw)?,
543            }),
544            "contact" => Ok(Self::Contact {
545                payload: deserialize_attachment_payload(&raw)?,
546            }),
547            _ => Ok(Self::Unknown {
548                r#type: attachment_type.to_string(),
549                payload: raw.get("payload").cloned(),
550                raw,
551            }),
552        }
553    }
554}
555
556fn deserialize_attachment_payload<T, E>(raw: &serde_json::Value) -> Result<T, E>
557where
558    T: DeserializeOwned,
559    E: DeError,
560{
561    let value = raw.get("payload").cloned().unwrap_or_else(|| raw.clone());
562
563    serde_json::from_value(value).map_err(E::custom)
564}
565
566#[non_exhaustive]
567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
568pub enum AttachmentKind {
569    Image,
570    Video,
571    Audio,
572    File,
573    Sticker,
574    InlineKeyboard,
575    Location,
576    Contact,
577    Unknown,
578}
579
580#[derive(Debug, Clone, Deserialize, Serialize)]
581pub struct MediaPayload {
582    pub url: Option<String>,
583    pub token: Option<String>,
584    pub photo_id: Option<i64>,
585}
586
587#[derive(Debug, Clone, Deserialize, Serialize)]
588pub struct FilePayload {
589    pub url: Option<String>,
590    pub token: Option<String>,
591    pub filename: Option<String>,
592    pub size: Option<i64>,
593}
594
595#[derive(Debug, Clone, Deserialize, Serialize)]
596pub struct StickerPayload {
597    pub code: String,
598    pub url: Option<String>,
599    pub width: Option<i32>,
600    pub height: Option<i32>,
601}
602
603#[derive(Debug, Clone, Deserialize, Serialize)]
604pub struct LocationPayload {
605    pub latitude: f64,
606    pub longitude: f64,
607}
608
609#[derive(Debug, Clone, Deserialize, Serialize)]
610pub struct ContactPayload {
611    pub name: Option<String>,
612    pub contact_id: Option<i64>,
613    pub vcf_info: Option<String>,
614    pub vcf_phone: Option<String>,
615}
616
617// ────────────────────────────────────────────────
618// Keyboard
619// ────────────────────────────────────────────────
620
621#[derive(Debug, Clone, Deserialize, Serialize, Default)]
622pub struct KeyboardPayload {
623    /// Rows of buttons (max 30 rows, max 7 buttons per row).
624    pub buttons: Vec<Vec<Button>>,
625}
626
627#[non_exhaustive]
628#[derive(Debug, Clone, Deserialize, Serialize)]
629#[serde(tag = "type", rename_all = "snake_case")]
630pub enum Button {
631    /// Sends a callback event to the bot.
632    Callback {
633        text: String,
634        payload: String,
635        #[serde(skip_serializing_if = "Option::is_none")]
636        intent: Option<ButtonIntent>,
637    },
638    /// Opens a URL.
639    Link {
640        text: String,
641        url: String,
642        #[serde(skip_serializing_if = "Option::is_none")]
643        intent: Option<ButtonIntent>,
644    },
645    /// Sends a text message as the user.
646    Message {
647        text: String,
648        #[serde(skip_serializing_if = "Option::is_none")]
649        intent: Option<ButtonIntent>,
650    },
651    /// Opens a MAX mini app.
652    OpenApp {
653        text: String,
654        #[serde(default, skip_serializing_if = "String::is_empty")]
655        web_app: String,
656        #[serde(skip_serializing_if = "Option::is_none")]
657        payload: Option<String>,
658        #[serde(skip_serializing_if = "Option::is_none")]
659        contact_id: Option<i64>,
660    },
661    /// Copies the payload to the clipboard.
662    ///
663    /// This button exists in the official Go SDK, but is not listed in the
664    /// public REST documentation's button overview yet.
665    Clipboard { text: String, payload: String },
666    /// Requests the user's contact card.
667    ///
668    /// MAX documents this button, but live tests have observed contact updates
669    /// with empty `contact_id` and `vcf_phone`, so phone delivery is not
670    /// currently guaranteed on the MAX side.
671    RequestContact { text: String },
672    /// Requests the user's geo location.
673    ///
674    /// Live tests have observed MAX returning a `location` attachment with
675    /// `latitude` and `longitude` directly on the attachment object.
676    RequestGeoLocation {
677        text: String,
678        #[serde(skip_serializing_if = "Option::is_none")]
679        quick: Option<bool>,
680    },
681}
682
683/// Visual style of a button.
684#[non_exhaustive]
685#[derive(Debug, Clone, PartialEq, Eq, Default)]
686pub enum ButtonIntent {
687    #[default]
688    Default,
689    Positive,
690    Negative,
691    Unknown(String),
692}
693
694impl ButtonIntent {
695    pub fn as_str(&self) -> &str {
696        match self {
697            Self::Default => "default",
698            Self::Positive => "positive",
699            Self::Negative => "negative",
700            Self::Unknown(value) => value.as_str(),
701        }
702    }
703}
704
705impl Serialize for ButtonIntent {
706    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
707    where
708        S: Serializer,
709    {
710        serialize_string_enum(serializer, self.as_str())
711    }
712}
713
714impl<'de> Deserialize<'de> for ButtonIntent {
715    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
716    where
717        D: Deserializer<'de>,
718    {
719        deserialize_string_enum(deserializer, |value| match value.as_str() {
720            "default" => Self::Default,
721            "positive" => Self::Positive,
722            "negative" => Self::Negative,
723            _ => Self::Unknown(value),
724        })
725    }
726}
727
728impl Button {
729    pub fn callback(text: impl Into<String>, payload: impl Into<String>) -> Self {
730        Self::Callback {
731            text: text.into(),
732            payload: payload.into(),
733            intent: None,
734        }
735    }
736
737    pub fn link(text: impl Into<String>, url: impl Into<String>) -> Self {
738        Self::Link {
739            text: text.into(),
740            url: url.into(),
741            intent: None,
742        }
743    }
744
745    pub fn message(text: impl Into<String>) -> Self {
746        Self::Message {
747            text: text.into(),
748            intent: None,
749        }
750    }
751
752    pub fn open_app(text: impl Into<String>, web_app: impl Into<String>) -> Self {
753        Self::OpenApp {
754            text: text.into(),
755            web_app: web_app.into(),
756            payload: None,
757            contact_id: None,
758        }
759    }
760
761    pub fn open_app_with_payload(
762        text: impl Into<String>,
763        web_app: impl Into<String>,
764        payload: impl Into<String>,
765    ) -> Self {
766        Self::OpenApp {
767            text: text.into(),
768            web_app: web_app.into(),
769            payload: Some(payload.into()),
770            contact_id: None,
771        }
772    }
773
774    pub fn open_app_full(
775        text: impl Into<String>,
776        web_app: impl Into<String>,
777        payload: Option<String>,
778        contact_id: Option<i64>,
779    ) -> Self {
780        Self::OpenApp {
781            text: text.into(),
782            web_app: web_app.into(),
783            payload,
784            contact_id,
785        }
786    }
787
788    pub fn clipboard(text: impl Into<String>, payload: impl Into<String>) -> Self {
789        Self::Clipboard {
790            text: text.into(),
791            payload: payload.into(),
792        }
793    }
794
795    pub fn request_contact(text: impl Into<String>) -> Self {
796        Self::RequestContact { text: text.into() }
797    }
798
799    pub fn request_geo_location(text: impl Into<String>) -> Self {
800        Self::RequestGeoLocation {
801            text: text.into(),
802            quick: None,
803        }
804    }
805}
806
807// ────────────────────────────────────────────────
808// New message body (outgoing)
809// ────────────────────────────────────────────────
810
811/// Body for POST /messages.
812#[derive(Debug, Clone, Serialize, Default)]
813pub struct NewMessageBody {
814    #[serde(skip_serializing_if = "Option::is_none")]
815    pub text: Option<String>,
816    #[serde(skip_serializing_if = "Option::is_none")]
817    pub attachments: Option<Vec<NewAttachment>>,
818    #[serde(skip_serializing_if = "Option::is_none")]
819    pub link: Option<NewMessageLink>,
820    #[serde(skip_serializing_if = "Option::is_none")]
821    pub notify: Option<bool>,
822    #[serde(skip_serializing_if = "Option::is_none")]
823    pub format: Option<MessageFormat>,
824}
825
826impl NewMessageBody {
827    pub fn empty() -> Self {
828        Self::default()
829    }
830
831    pub fn text(text: impl Into<String>) -> Self {
832        Self {
833            text: Some(text.into()),
834            ..Default::default()
835        }
836    }
837
838    pub fn text_opt(text: Option<impl Into<String>>) -> Self {
839        match text {
840            Some(text) => Self::text(text),
841            None => Self::empty(),
842        }
843    }
844
845    pub fn with_attachment(mut self, attachment: NewAttachment) -> Self {
846        self.attachments
847            .get_or_insert_with(Vec::new)
848            .push(attachment);
849        self
850    }
851
852    pub fn with_attachments(
853        mut self,
854        attachments: impl IntoIterator<Item = NewAttachment>,
855    ) -> Self {
856        self.attachments
857            .get_or_insert_with(Vec::new)
858            .extend(attachments);
859        self
860    }
861
862    pub fn with_keyboard(self, keyboard: KeyboardPayload) -> Self {
863        self.with_attachment(NewAttachment::inline_keyboard(keyboard))
864    }
865
866    pub fn with_format(mut self, format: MessageFormat) -> Self {
867        self.format = Some(format);
868        self
869    }
870
871    pub fn with_notify(mut self, notify: bool) -> Self {
872        self.notify = Some(notify);
873        self
874    }
875
876    pub fn with_reply_to(mut self, message_id: impl Into<String>) -> Self {
877        self.link = Some(NewMessageLink {
878            r#type: LinkType::Reply,
879            mid: message_id.into(),
880        });
881        self
882    }
883
884    pub fn with_forward_from(mut self, message_id: impl Into<String>) -> Self {
885        self.link = Some(NewMessageLink {
886            r#type: LinkType::Forward,
887            mid: message_id.into(),
888        });
889        self
890    }
891}
892
893#[non_exhaustive]
894#[derive(Debug, Clone, Serialize)]
895#[serde(tag = "type", rename_all = "snake_case")]
896pub enum NewAttachment {
897    InlineKeyboard { payload: KeyboardPayload },
898    Image { payload: ImageAttachmentPayload },
899    Video { payload: UploadedToken },
900    Audio { payload: UploadedToken },
901    File { payload: UploadedToken },
902}
903
904impl NewAttachment {
905    pub fn inline_keyboard(keyboard: KeyboardPayload) -> Self {
906        Self::InlineKeyboard { payload: keyboard }
907    }
908
909    pub fn image(token: impl Into<String>) -> Self {
910        Self::Image {
911            payload: ImageAttachmentPayload::token(token),
912        }
913    }
914
915    pub fn image_url(url: impl Into<String>) -> Self {
916        Self::Image {
917            payload: ImageAttachmentPayload::url(url),
918        }
919    }
920
921    pub fn image_photos(photos: PhotoTokens) -> Self {
922        Self::Image {
923            payload: ImageAttachmentPayload::photos(photos),
924        }
925    }
926
927    pub fn video(token: impl Into<String>) -> Self {
928        Self::Video {
929            payload: UploadedToken::new(token),
930        }
931    }
932
933    pub fn audio(token: impl Into<String>) -> Self {
934        Self::Audio {
935            payload: UploadedToken::new(token),
936        }
937    }
938
939    pub fn file(token: impl Into<String>) -> Self {
940        Self::File {
941            payload: UploadedToken::new(token),
942        }
943    }
944}
945
946#[derive(Debug, Clone, Deserialize, Serialize)]
947pub struct PhotoToken {
948    pub token: String,
949    #[serde(flatten)]
950    pub extra: BTreeMap<String, serde_json::Value>,
951}
952
953impl PhotoToken {
954    pub fn new(token: impl Into<String>) -> Self {
955        Self {
956            token: token.into(),
957            extra: BTreeMap::new(),
958        }
959    }
960}
961
962pub type PhotoTokens = BTreeMap<String, PhotoToken>;
963
964#[derive(Debug, Clone, Default, Deserialize, Serialize)]
965pub struct ImageAttachmentPayload {
966    #[serde(skip_serializing_if = "Option::is_none")]
967    pub url: Option<String>,
968    #[serde(skip_serializing_if = "Option::is_none")]
969    pub token: Option<String>,
970    #[serde(skip_serializing_if = "Option::is_none")]
971    pub photos: Option<PhotoTokens>,
972}
973
974impl ImageAttachmentPayload {
975    pub fn token(token: impl Into<String>) -> Self {
976        Self {
977            token: Some(token.into()),
978            ..Default::default()
979        }
980    }
981
982    pub fn url(url: impl Into<String>) -> Self {
983        Self {
984            url: Some(url.into()),
985            ..Default::default()
986        }
987    }
988
989    pub fn photos(photos: PhotoTokens) -> Self {
990        Self {
991            photos: Some(photos),
992            ..Default::default()
993        }
994    }
995}
996
997#[derive(Debug, Clone, Serialize)]
998pub struct UploadedToken {
999    pub token: String,
1000}
1001
1002impl UploadedToken {
1003    pub fn new(token: impl Into<String>) -> Self {
1004        Self {
1005            token: token.into(),
1006        }
1007    }
1008}
1009
1010#[derive(Debug, Clone, Serialize)]
1011pub struct NewMessageLink {
1012    pub r#type: LinkType,
1013    pub mid: String,
1014}
1015
1016#[non_exhaustive]
1017#[derive(Debug, Clone, PartialEq, Eq)]
1018pub enum LinkType {
1019    Forward,
1020    Reply,
1021    Unknown(String),
1022}
1023
1024impl LinkType {
1025    pub fn as_str(&self) -> &str {
1026        match self {
1027            Self::Forward => "forward",
1028            Self::Reply => "reply",
1029            Self::Unknown(value) => value.as_str(),
1030        }
1031    }
1032}
1033
1034impl Serialize for LinkType {
1035    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1036    where
1037        S: Serializer,
1038    {
1039        serialize_string_enum(serializer, self.as_str())
1040    }
1041}
1042
1043impl<'de> Deserialize<'de> for LinkType {
1044    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1045    where
1046        D: Deserializer<'de>,
1047    {
1048        deserialize_string_enum(deserializer, |value| match value.as_str() {
1049            "forward" => Self::Forward,
1050            "reply" => Self::Reply,
1051            _ => Self::Unknown(value),
1052        })
1053    }
1054}
1055
1056/// Query options for POST /messages.
1057#[derive(Debug, Clone, Copy, Default, Serialize)]
1058pub struct SendMessageOptions {
1059    #[serde(skip_serializing_if = "Option::is_none")]
1060    pub disable_link_preview: Option<bool>,
1061}
1062
1063impl SendMessageOptions {
1064    pub fn disable_link_preview(disable: bool) -> Self {
1065        Self {
1066            disable_link_preview: Some(disable),
1067        }
1068    }
1069}
1070
1071// ────────────────────────────────────────────────
1072// Updates / Events (long polling & webhook)
1073// ────────────────────────────────────────────────
1074
1075/// Container returned by GET /updates.
1076#[derive(Debug, Clone, Deserialize)]
1077pub struct UpdatesResponse {
1078    pub updates: Vec<Update>,
1079    pub marker: Option<i64>,
1080}
1081
1082/// Raw container returned by GET /updates before typed update deserialization.
1083#[derive(Debug, Clone, Deserialize)]
1084pub struct RawUpdatesResponse {
1085    pub updates: Vec<serde_json::Value>,
1086    pub marker: Option<i64>,
1087}
1088
1089/// A single update event from the Max platform.
1090///
1091/// The large `Message` payloads intentionally stay inline to keep public match
1092/// ergonomics simple for bot handlers.
1093#[allow(clippy::large_enum_variant)]
1094#[non_exhaustive]
1095#[derive(Debug, Clone)]
1096pub enum Update {
1097    /// A new message was received.
1098    MessageCreated { timestamp: i64, message: Message },
1099    /// A message was edited.
1100    MessageEdited { timestamp: i64, message: Message },
1101    /// A message was deleted.
1102    MessageRemoved {
1103        timestamp: i64,
1104        message_id: String,
1105        chat_id: i64,
1106        user_id: i64,
1107    },
1108    /// A user pressed an inline button.
1109    MessageCallback {
1110        timestamp: i64,
1111        callback: Callback,
1112        message: Option<Message>,
1113        user_locale: Option<String>,
1114    },
1115    /// The bot was started in a private chat.
1116    BotStarted {
1117        timestamp: i64,
1118        chat_id: i64,
1119        user: User,
1120        payload: Option<String>,
1121        user_locale: Option<String>,
1122    },
1123    /// The bot was added to a chat.
1124    BotAdded {
1125        timestamp: i64,
1126        chat_id: i64,
1127        user: User,
1128        is_channel: Option<bool>,
1129    },
1130    /// The bot was removed from a chat.
1131    BotRemoved {
1132        timestamp: i64,
1133        chat_id: i64,
1134        user: User,
1135        is_channel: Option<bool>,
1136    },
1137    /// A user joined a chat where the bot is a member.
1138    UserAdded {
1139        timestamp: i64,
1140        chat_id: i64,
1141        user: User,
1142        inviter_id: Option<i64>,
1143        is_channel: Option<bool>,
1144    },
1145    /// A user left a chat where the bot is a member.
1146    UserRemoved {
1147        timestamp: i64,
1148        chat_id: i64,
1149        user: User,
1150        admin_id: Option<i64>,
1151        is_channel: Option<bool>,
1152    },
1153    /// The bot received a message with a chat title change.
1154    ChatTitleChanged {
1155        timestamp: i64,
1156        chat_id: i64,
1157        user: User,
1158        title: String,
1159    },
1160    /// A newer or currently unsupported update type.
1161    Unknown {
1162        update_type: Option<String>,
1163        timestamp: Option<i64>,
1164        raw: serde_json::Value,
1165    },
1166}
1167
1168impl Update {
1169    /// Returns the timestamp of this update when one was present.
1170    pub fn timestamp(&self) -> Option<i64> {
1171        match self {
1172            Self::MessageCreated { timestamp, .. }
1173            | Self::MessageEdited { timestamp, .. }
1174            | Self::MessageRemoved { timestamp, .. }
1175            | Self::MessageCallback { timestamp, .. }
1176            | Self::BotStarted { timestamp, .. }
1177            | Self::BotAdded { timestamp, .. }
1178            | Self::BotRemoved { timestamp, .. }
1179            | Self::UserAdded { timestamp, .. }
1180            | Self::UserRemoved { timestamp, .. }
1181            | Self::ChatTitleChanged { timestamp, .. } => Some(*timestamp),
1182            Self::Unknown { timestamp, .. } => *timestamp,
1183        }
1184    }
1185
1186    /// Returns the timestamp or `0` when an unknown update did not include one.
1187    pub fn timestamp_or_default(&self) -> i64 {
1188        self.timestamp().unwrap_or_default()
1189    }
1190
1191    pub fn update_type(&self) -> Option<&str> {
1192        match self {
1193            Self::MessageCreated { .. } => Some("message_created"),
1194            Self::MessageEdited { .. } => Some("message_edited"),
1195            Self::MessageRemoved { .. } => Some("message_removed"),
1196            Self::MessageCallback { .. } => Some("message_callback"),
1197            Self::BotStarted { .. } => Some("bot_started"),
1198            Self::BotAdded { .. } => Some("bot_added"),
1199            Self::BotRemoved { .. } => Some("bot_removed"),
1200            Self::UserAdded { .. } => Some("user_added"),
1201            Self::UserRemoved { .. } => Some("user_removed"),
1202            Self::ChatTitleChanged { .. } => Some("chat_title_changed"),
1203            Self::Unknown { update_type, .. } => update_type.as_deref(),
1204        }
1205    }
1206
1207    pub fn raw(&self) -> Option<&serde_json::Value> {
1208        match self {
1209            Self::Unknown { raw, .. } => Some(raw),
1210            _ => None,
1211        }
1212    }
1213}
1214
1215impl<'de> Deserialize<'de> for Update {
1216    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1217    where
1218        D: Deserializer<'de>,
1219    {
1220        let raw = serde_json::Value::deserialize(deserializer)?;
1221        let update_type = raw
1222            .get("update_type")
1223            .and_then(|value| value.as_str())
1224            .map(String::from);
1225        let timestamp = raw.get("timestamp").and_then(|value| value.as_i64());
1226
1227        let Some(kind) = update_type.as_deref() else {
1228            return Ok(Self::Unknown {
1229                update_type,
1230                timestamp,
1231                raw,
1232            });
1233        };
1234
1235        macro_rules! parse_update {
1236            ($wire:ty, $map:expr) => {
1237                match serde_json::from_value::<$wire>(raw.clone()) {
1238                    Ok(wire) => $map(wire),
1239                    Err(_) => Self::Unknown {
1240                        update_type,
1241                        timestamp,
1242                        raw,
1243                    },
1244                }
1245            };
1246        }
1247
1248        #[derive(Deserialize)]
1249        struct MessageUpdate {
1250            timestamp: i64,
1251            message: Message,
1252        }
1253
1254        #[derive(Deserialize)]
1255        struct MessageRemovedUpdate {
1256            timestamp: i64,
1257            message_id: String,
1258            chat_id: i64,
1259            user_id: i64,
1260        }
1261
1262        #[derive(Deserialize)]
1263        struct MessageCallbackUpdate {
1264            timestamp: i64,
1265            callback: Callback,
1266            #[serde(default)]
1267            message: Option<Message>,
1268            #[serde(default)]
1269            user_locale: Option<String>,
1270        }
1271
1272        #[derive(Deserialize)]
1273        struct BotStartedUpdate {
1274            timestamp: i64,
1275            chat_id: i64,
1276            user: User,
1277            #[serde(default)]
1278            payload: Option<String>,
1279            #[serde(default)]
1280            user_locale: Option<String>,
1281        }
1282
1283        #[derive(Deserialize)]
1284        struct BotChatUpdate {
1285            timestamp: i64,
1286            chat_id: i64,
1287            user: User,
1288            #[serde(default)]
1289            is_channel: Option<bool>,
1290        }
1291
1292        #[derive(Deserialize)]
1293        struct UserAddedUpdate {
1294            timestamp: i64,
1295            chat_id: i64,
1296            user: User,
1297            #[serde(default)]
1298            inviter_id: Option<i64>,
1299            #[serde(default)]
1300            is_channel: Option<bool>,
1301        }
1302
1303        #[derive(Deserialize)]
1304        struct UserRemovedUpdate {
1305            timestamp: i64,
1306            chat_id: i64,
1307            user: User,
1308            #[serde(default)]
1309            admin_id: Option<i64>,
1310            #[serde(default)]
1311            is_channel: Option<bool>,
1312        }
1313
1314        #[derive(Deserialize)]
1315        struct ChatTitleChangedUpdate {
1316            timestamp: i64,
1317            chat_id: i64,
1318            user: User,
1319            title: String,
1320        }
1321
1322        Ok(match kind {
1323            "message_created" => parse_update!(MessageUpdate, |wire: MessageUpdate| {
1324                Self::MessageCreated {
1325                    timestamp: wire.timestamp,
1326                    message: wire.message,
1327                }
1328            }),
1329            "message_edited" => parse_update!(MessageUpdate, |wire: MessageUpdate| {
1330                Self::MessageEdited {
1331                    timestamp: wire.timestamp,
1332                    message: wire.message,
1333                }
1334            }),
1335            "message_removed" => {
1336                parse_update!(MessageRemovedUpdate, |wire: MessageRemovedUpdate| {
1337                    Self::MessageRemoved {
1338                        timestamp: wire.timestamp,
1339                        message_id: wire.message_id,
1340                        chat_id: wire.chat_id,
1341                        user_id: wire.user_id,
1342                    }
1343                })
1344            }
1345            "message_callback" => {
1346                parse_update!(MessageCallbackUpdate, |wire: MessageCallbackUpdate| {
1347                    Self::MessageCallback {
1348                        timestamp: wire.timestamp,
1349                        callback: wire.callback,
1350                        message: wire.message,
1351                        user_locale: wire.user_locale,
1352                    }
1353                })
1354            }
1355            "bot_started" => parse_update!(BotStartedUpdate, |wire: BotStartedUpdate| {
1356                Self::BotStarted {
1357                    timestamp: wire.timestamp,
1358                    chat_id: wire.chat_id,
1359                    user: wire.user,
1360                    payload: wire.payload,
1361                    user_locale: wire.user_locale,
1362                }
1363            }),
1364            "bot_added" => parse_update!(BotChatUpdate, |wire: BotChatUpdate| {
1365                Self::BotAdded {
1366                    timestamp: wire.timestamp,
1367                    chat_id: wire.chat_id,
1368                    user: wire.user,
1369                    is_channel: wire.is_channel,
1370                }
1371            }),
1372            "bot_removed" => parse_update!(BotChatUpdate, |wire: BotChatUpdate| {
1373                Self::BotRemoved {
1374                    timestamp: wire.timestamp,
1375                    chat_id: wire.chat_id,
1376                    user: wire.user,
1377                    is_channel: wire.is_channel,
1378                }
1379            }),
1380            "user_added" => parse_update!(UserAddedUpdate, |wire: UserAddedUpdate| {
1381                Self::UserAdded {
1382                    timestamp: wire.timestamp,
1383                    chat_id: wire.chat_id,
1384                    user: wire.user,
1385                    inviter_id: wire.inviter_id,
1386                    is_channel: wire.is_channel,
1387                }
1388            }),
1389            "user_removed" => parse_update!(UserRemovedUpdate, |wire: UserRemovedUpdate| {
1390                Self::UserRemoved {
1391                    timestamp: wire.timestamp,
1392                    chat_id: wire.chat_id,
1393                    user: wire.user,
1394                    admin_id: wire.admin_id,
1395                    is_channel: wire.is_channel,
1396                }
1397            }),
1398            "chat_title_changed" => {
1399                parse_update!(ChatTitleChangedUpdate, |wire: ChatTitleChangedUpdate| {
1400                    Self::ChatTitleChanged {
1401                        timestamp: wire.timestamp,
1402                        chat_id: wire.chat_id,
1403                        user: wire.user,
1404                        title: wire.title,
1405                    }
1406                })
1407            }
1408            _ => Self::Unknown {
1409                update_type,
1410                timestamp,
1411                raw,
1412            },
1413        })
1414    }
1415}
1416
1417/// An inline button callback.
1418#[derive(Debug, Clone, Deserialize, Serialize)]
1419pub struct Callback {
1420    pub callback_id: String,
1421    pub user: User,
1422    pub payload: Option<String>,
1423    pub timestamp: i64,
1424}
1425
1426// ────────────────────────────────────────────────
1427// Subscriptions (webhook)
1428// ────────────────────────────────────────────────
1429
1430#[derive(Debug, Clone, Deserialize, Serialize)]
1431pub struct Subscription {
1432    pub url: String,
1433    pub time: i64,
1434    pub update_types: Option<Vec<String>>,
1435    pub version: Option<String>,
1436}
1437
1438#[derive(Debug, Clone, Deserialize)]
1439pub struct SubscriptionList {
1440    pub subscriptions: Vec<Subscription>,
1441}
1442
1443#[derive(Debug, Clone, Serialize)]
1444pub struct SubscribeBody {
1445    /// HTTPS URL of your bot endpoint (must be port 443, no self-signed certs).
1446    pub url: String,
1447    /// Optional list of update types to receive (e.g. `["message_created", "bot_started"]`).
1448    #[serde(skip_serializing_if = "Option::is_none")]
1449    pub update_types: Option<Vec<String>>,
1450    #[serde(skip_serializing_if = "Option::is_none")]
1451    pub version: Option<String>,
1452    /// Optional secret (5-256 chars, `[A-Za-z0-9_-]`).
1453    /// Sent in the `X-Max-Bot-Api-Secret` header on every webhook request.
1454    #[serde(skip_serializing_if = "Option::is_none")]
1455    pub secret: Option<String>,
1456}
1457
1458// ────────────────────────────────────────────────
1459// Upload
1460// ────────────────────────────────────────────────
1461
1462/// Response from a multipart file upload (image / file).
1463/// For video/audio the token is returned by `POST /uploads` before the upload.
1464#[derive(Debug, Clone, Deserialize)]
1465pub struct UploadResponse {
1466    /// Ready-to-use attachment token (for image and file types).
1467    pub token: Option<String>,
1468    /// Photo tokens returned by MAX image uploads.
1469    pub photos: Option<PhotoTokens>,
1470}
1471
1472#[non_exhaustive]
1473#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1474#[serde(rename_all = "lowercase")]
1475pub enum UploadType {
1476    /// Still images (JPG, JPEG, PNG, GIF, TIFF, BMP, HEIC).
1477    /// NOTE: `photo` was removed from the API — always use `image`.
1478    Image,
1479    /// Video files (MP4, MOV, MKV, WEBM, MATROSKA).
1480    Video,
1481    /// Audio files (MP3, WAV, M4A, ...).
1482    Audio,
1483    /// Any other file type (max 4 GB).
1484    File,
1485}
1486
1487impl UploadType {
1488    pub fn as_str(&self) -> &'static str {
1489        match self {
1490            Self::Image => "image",
1491            Self::Video => "video",
1492            Self::Audio => "audio",
1493            Self::File => "file",
1494        }
1495    }
1496}
1497
1498#[derive(Debug, Clone, Deserialize)]
1499pub struct UploadEndpoint {
1500    pub url: String,
1501    pub token: Option<String>,
1502}
1503
1504// ────────────────────────────────────────────────
1505// Answer on callback
1506// ────────────────────────────────────────────────
1507
1508/// Body for POST /answers.
1509#[derive(Debug, Clone, Serialize, Default)]
1510pub struct AnswerCallbackBody {
1511    pub callback_id: String,
1512    #[serde(skip_serializing_if = "Option::is_none")]
1513    pub message: Option<NewMessageBody>,
1514    #[serde(skip_serializing_if = "Option::is_none")]
1515    pub notification: Option<String>,
1516}
1517
1518// ────────────────────────────────────────────────
1519// Simple results and video metadata
1520// ────────────────────────────────────────────────
1521
1522/// Generic simple JSON result `{"success": true}`.
1523#[derive(Debug, Clone, Deserialize)]
1524pub struct SimpleResult {
1525    pub success: bool,
1526    pub message: Option<String>,
1527    pub failed_user_ids: Option<Vec<i64>>,
1528    pub failed_user_details: Option<Vec<serde_json::Value>>,
1529}
1530
1531#[derive(Debug, Clone, Deserialize, Serialize)]
1532pub struct VideoInfo {
1533    pub token: String,
1534    pub urls: Option<VideoUrls>,
1535    pub thumbnail: Option<PhotoAttachmentPayload>,
1536    pub width: Option<i32>,
1537    pub height: Option<i32>,
1538    pub duration: Option<i32>,
1539}
1540
1541#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1542pub struct VideoUrls {
1543    #[serde(flatten)]
1544    pub values: BTreeMap<String, serde_json::Value>,
1545}
1546
1547#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1548pub struct PhotoAttachmentPayload {
1549    pub url: Option<String>,
1550    pub token: Option<String>,
1551    pub photo_id: Option<i64>,
1552    pub width: Option<i32>,
1553    pub height: Option<i32>,
1554    #[serde(flatten)]
1555    pub extra: BTreeMap<String, serde_json::Value>,
1556}
1557
1558// ────────────────────────────────────────────────
1559// Chat members and admins
1560// ────────────────────────────────────────────────
1561
1562#[derive(Debug, Clone, Serialize)]
1563pub struct ChatMember {
1564    pub user_id: i64,
1565    pub first_name: String,
1566    pub last_name: Option<String>,
1567    pub username: Option<String>,
1568    pub avatar_url: Option<String>,
1569    pub full_avatar_url: Option<String>,
1570    pub description: Option<String>,
1571    pub is_owner: Option<bool>,
1572    pub is_admin: Option<bool>,
1573    pub join_time: Option<i64>,
1574    pub permissions: Option<Vec<ChatAdminPermission>>,
1575    pub last_activity_time: Option<i64>,
1576    pub last_access_time: Option<i64>,
1577    pub is_bot: Option<bool>,
1578    pub alias: Option<String>,
1579}
1580
1581impl<'de> Deserialize<'de> for ChatMember {
1582    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1583    where
1584        D: Deserializer<'de>,
1585    {
1586        #[derive(Deserialize)]
1587        struct WireChatMember {
1588            user_id: i64,
1589            #[serde(default)]
1590            first_name: Option<String>,
1591            #[serde(default)]
1592            name: Option<String>,
1593            #[serde(default)]
1594            last_name: Option<String>,
1595            #[serde(default)]
1596            username: Option<String>,
1597            #[serde(default)]
1598            avatar_url: Option<String>,
1599            #[serde(default)]
1600            full_avatar_url: Option<String>,
1601            #[serde(default)]
1602            description: Option<String>,
1603            #[serde(default)]
1604            is_owner: Option<bool>,
1605            #[serde(default)]
1606            is_admin: Option<bool>,
1607            #[serde(default)]
1608            join_time: Option<i64>,
1609            #[serde(default)]
1610            permissions: Option<Vec<ChatAdminPermission>>,
1611            #[serde(default)]
1612            last_activity_time: Option<i64>,
1613            #[serde(default)]
1614            last_access_time: Option<i64>,
1615            #[serde(default)]
1616            is_bot: Option<bool>,
1617            #[serde(default)]
1618            alias: Option<String>,
1619        }
1620
1621        let wire = WireChatMember::deserialize(deserializer)?;
1622        let first_name = wire
1623            .first_name
1624            .or(wire.name)
1625            .ok_or_else(|| D::Error::missing_field("first_name"))?;
1626
1627        Ok(Self {
1628            user_id: wire.user_id,
1629            first_name,
1630            last_name: wire.last_name,
1631            username: wire.username,
1632            avatar_url: wire.avatar_url,
1633            full_avatar_url: wire.full_avatar_url,
1634            description: wire.description,
1635            is_owner: wire.is_owner,
1636            is_admin: wire.is_admin,
1637            join_time: wire.join_time,
1638            permissions: wire.permissions,
1639            last_activity_time: wire.last_activity_time,
1640            last_access_time: wire.last_access_time,
1641            is_bot: wire.is_bot,
1642            alias: wire.alias,
1643        })
1644    }
1645}
1646
1647#[derive(Debug, Clone, Deserialize)]
1648pub struct ChatMembersList {
1649    pub members: Vec<ChatMember>,
1650    pub marker: Option<i64>,
1651}
1652
1653#[non_exhaustive]
1654#[derive(Debug, Clone, PartialEq, Eq)]
1655pub enum ChatAdminPermission {
1656    ReadAllMessages,
1657    AddRemoveMembers,
1658    AddAdmins,
1659    ChangeChatInfo,
1660    PinMessage,
1661    Write,
1662    CanCall,
1663    EditLink,
1664    PostEditDeleteMessage,
1665    EditMessage,
1666    DeleteMessage,
1667    Unknown(String),
1668}
1669
1670impl ChatAdminPermission {
1671    pub fn as_str(&self) -> &str {
1672        match self {
1673            Self::ReadAllMessages => "read_all_messages",
1674            Self::AddRemoveMembers => "add_remove_members",
1675            Self::AddAdmins => "add_admins",
1676            Self::ChangeChatInfo => "change_chat_info",
1677            Self::PinMessage => "pin_message",
1678            Self::Write => "write",
1679            Self::CanCall => "can_call",
1680            Self::EditLink => "edit_link",
1681            Self::PostEditDeleteMessage => "post_edit_delete_message",
1682            Self::EditMessage => "edit_message",
1683            Self::DeleteMessage => "delete_message",
1684            Self::Unknown(value) => value.as_str(),
1685        }
1686    }
1687}
1688
1689impl Serialize for ChatAdminPermission {
1690    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1691    where
1692        S: Serializer,
1693    {
1694        serialize_string_enum(serializer, self.as_str())
1695    }
1696}
1697
1698impl<'de> Deserialize<'de> for ChatAdminPermission {
1699    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1700    where
1701        D: Deserializer<'de>,
1702    {
1703        deserialize_string_enum(deserializer, |value| match value.as_str() {
1704            "read_all_messages" => Self::ReadAllMessages,
1705            "add_remove_members" => Self::AddRemoveMembers,
1706            "add_admins" => Self::AddAdmins,
1707            "change_chat_info" => Self::ChangeChatInfo,
1708            "pin_message" => Self::PinMessage,
1709            "write" => Self::Write,
1710            "can_call" => Self::CanCall,
1711            "edit_link" => Self::EditLink,
1712            "post_edit_delete_message" => Self::PostEditDeleteMessage,
1713            "edit_message" => Self::EditMessage,
1714            "delete_message" => Self::DeleteMessage,
1715            _ => Self::Unknown(value),
1716        })
1717    }
1718}
1719
1720#[derive(Debug, Clone, Serialize)]
1721pub struct ChatAdmin {
1722    pub user_id: i64,
1723    pub permissions: Vec<ChatAdminPermission>,
1724    #[serde(skip_serializing_if = "Option::is_none")]
1725    pub alias: Option<String>,
1726}
1727
1728#[derive(Debug, Clone, Serialize)]
1729pub struct SetChatAdminsBody {
1730    pub admins: Vec<ChatAdmin>,
1731    #[serde(skip_serializing_if = "Option::is_none")]
1732    pub marker: Option<i64>,
1733}
1734
1735/// Body for POST /chats/{chatId}/members.
1736#[derive(Debug, Clone, Serialize)]
1737pub struct AddMembersBody {
1738    pub user_ids: Vec<i64>,
1739}
1740
1741/// Body for DELETE /chats/{chatId}/members.
1742#[derive(Debug, Clone, Serialize)]
1743pub struct RemoveMemberQuery {
1744    pub user_id: i64,
1745}
1746
1747/// Pinned message info.
1748#[derive(Debug, Clone, Deserialize)]
1749pub struct PinnedMessage {
1750    pub message: Message,
1751}
1752
1753/// Body for PUT /chats/{chatId}/pin.
1754#[derive(Debug, Clone, Serialize)]
1755pub struct PinMessageBody {
1756    pub message_id: String,
1757    #[serde(skip_serializing_if = "Option::is_none")]
1758    pub notify: Option<bool>,
1759}
1760
1761/// Bot command for setMyCommands.
1762#[derive(Debug, Clone, Serialize, Deserialize)]
1763pub struct BotCommand {
1764    pub name: String,
1765    pub description: String,
1766}
1767
1768// ────────────────────────────────────────────────
1769// Sender actions
1770// ────────────────────────────────────────────────
1771
1772#[non_exhaustive]
1773#[derive(Debug, Clone, PartialEq, Eq)]
1774pub enum SenderAction {
1775    TypingOn,
1776    SendingImage,
1777    SendingVideo,
1778    SendingAudio,
1779    SendingFile,
1780    MarkSeen,
1781    Unknown(String),
1782}
1783
1784impl SenderAction {
1785    pub fn as_str(&self) -> &str {
1786        match self {
1787            Self::TypingOn => "typing_on",
1788            Self::SendingImage => "sending_photo",
1789            Self::SendingVideo => "sending_video",
1790            Self::SendingAudio => "sending_audio",
1791            Self::SendingFile => "sending_file",
1792            Self::MarkSeen => "mark_seen",
1793            Self::Unknown(value) => value.as_str(),
1794        }
1795    }
1796}
1797
1798impl Serialize for SenderAction {
1799    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1800    where
1801        S: Serializer,
1802    {
1803        serialize_string_enum(serializer, self.as_str())
1804    }
1805}
1806
1807impl<'de> Deserialize<'de> for SenderAction {
1808    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1809    where
1810        D: Deserializer<'de>,
1811    {
1812        deserialize_string_enum(deserializer, |value| match value.as_str() {
1813            "typing_on" => Self::TypingOn,
1814            "sending_photo" => Self::SendingImage,
1815            "sending_video" => Self::SendingVideo,
1816            "sending_audio" => Self::SendingAudio,
1817            "sending_file" => Self::SendingFile,
1818            "mark_seen" => Self::MarkSeen,
1819            _ => Self::Unknown(value),
1820        })
1821    }
1822}
1823
1824impl fmt::Display for SenderAction {
1825    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1826        f.write_str(self.as_str())
1827    }
1828}