shrimple_telegram/
types.rs

1use {
2    crate::{Multipart, Result, PAYLOAD_LINK},
3    bytes::{Bytes, BytesMut},
4    reqwest::{multipart::Part, Body},
5    serde::{
6        de::{DeserializeOwned, Error, Unexpected},
7        Deserialize, Deserializer, Serialize, Serializer,
8    },
9    shrimple_telegram_proc_macro::telegram_type,
10    std::{
11        borrow::Cow,
12        fmt::{Debug, Formatter},
13        io,
14        mem::{replace, take},
15        num::NonZero,
16        sync::Mutex,
17    },
18    tokio::io::{AsyncRead, AsyncReadExt},
19    tokio_util::io::ReaderStream,
20};
21
22/// a boolean that is always `true`, useful for correct (de)serialization
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub struct True;
25
26impl<'de> Deserialize<'de> for True {
27    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
28        if !bool::deserialize(d)? {
29            return Err(D::Error::invalid_value(Unexpected::Bool(false), &"`true`"));
30        }
31        Ok(Self)
32    }
33}
34
35impl Serialize for True {
36    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
37        true.serialize(s)
38    }
39}
40
41#[telegram_type]
42pub struct BotCommand<'src> {
43    pub command: Cow<'src, str>,
44    pub description: Cow<'src, str>,
45}
46
47pub type UpdateId = u64;
48
49#[telegram_type(copy, no_doc)]
50pub enum AllowedUpdate {
51    CallbackQuery,
52    ChannelPost,
53    ChatBoost,
54    ChatJoinRequest,
55    ChatMember,
56    ChosenInlineResult,
57    EditedChannelPost,
58    EditedMessage,
59    InlineQuery,
60    Message,
61    MessageReaction,
62    MessageReactionCount,
63    MyChatMember,
64    Poll,
65    PollAnswer,
66    PreCheckoutQuery,
67    RemovedChatBoost,
68    ShippingQuery,
69}
70
71#[telegram_type(no_eq)]
72pub struct Update {
73    #[serde(rename = "update_id")]
74    pub id: u64,
75    #[serde(flatten)]
76    pub kind: UpdateKind,
77}
78
79impl Update {
80    pub fn from(&self) -> Option<&User> {
81        self.kind.from()
82    }
83
84    pub fn chat(&self) -> Option<&Chat> {
85        self.kind.chat()
86    }
87}
88
89#[telegram_type(no_doc, no_eq, common_fields {
90    #[optional] from: User,
91    #[optional] chat: Chat,
92})]
93pub enum UpdateKind {
94    #[telegram_type(nested_fields(from = &field0.from))]
95    CallbackQuery(CallbackQuery),
96    #[telegram_type(nested_fields(from = field0.from.as_ref()?, chat = &field0.chat))]
97    ChannelPost(Message),
98    #[telegram_type(nested_fields(chat = &field0.chat))]
99    ChatBoost(ChatBoostUpdated),
100    #[telegram_type(nested_fields(from = field0.from.as_ref()?, chat = &field0.chat))]
101    Message(Message),
102    #[serde(other)]
103    Other,
104}
105
106pub type MessageId = NonZero<i32>;
107
108#[telegram_type(no_eq)]
109pub struct Message {
110    #[serde(rename = "message_id")]
111    pub id: MessageId,
112    pub from: Option<User>,
113    pub chat: Chat,
114    pub date: Date,
115    #[serde(flatten)]
116    pub kind: MessageKind,
117}
118
119#[telegram_type(untagged, no_eq, no_doc)]
120pub enum MessageKind {
121    Common(MessageCommon),
122}
123
124#[telegram_type(no_eq, no_doc)]
125pub struct MessageCommon {
126    pub from: Option<User>,
127    pub sender_chat: Option<Chat>,
128    pub reply_to_message: Option<Box<Message>>,
129    #[serde(flatten)]
130    pub media: Media,
131}
132
133#[telegram_type(untagged, no_eq, no_doc)]
134pub enum Media {
135    Text {
136        text: Box<str>,
137        #[serde(default)]
138        entities: Box<[MessageEntity]>,
139    },
140    #[telegram_type(name_all(audio))]
141    Audio(File),
142    #[telegram_type(name_all(video))]
143    Video(File),
144    #[telegram_type(name_all(photo))]
145    Photo(Vec<File>),
146    #[telegram_type(name_all(document))]
147    Document(File),
148    #[telegram_type(name_all(location))]
149    Location(Location),
150}
151
152#[telegram_type]
153pub struct MessageEntity {
154    pub length: usize,
155    pub offset: usize,
156    #[serde(flatten)]
157    pub kind: MessageEntityKind,
158}
159
160#[telegram_type(no_doc)]
161#[serde(tag = "type")]
162pub enum MessageEntityKind {
163    BotCommand,
164    #[serde(other)]
165    Other,
166}
167
168#[telegram_type]
169pub struct File {
170    #[serde(rename = "file_id")]
171    pub id: Box<str>,
172}
173
174pub type UserId = i64;
175
176#[telegram_type]
177pub struct User {
178    pub id: UserId,
179    pub is_bot: bool,
180    pub first_name: Box<str>,
181    pub last_name: Option<Box<str>>,
182    pub username: Option<Box<str>>,
183    pub language_code: Option<Box<str>>,
184}
185
186impl User {
187    pub fn full_name(&self) -> String {
188        format!("{} {}", self.first_name, self.last_name.as_deref().unwrap_or_default())
189    }
190
191    pub fn username_or_full_name(&self) -> String {
192        self.username.as_deref().map_or_else(|| self.full_name(), |x| format!("@{x}"))
193    }
194}
195
196pub type ChatId = i64;
197
198#[telegram_type]
199pub struct Chat {
200    pub id: ChatId,
201    pub username: Option<Box<str>>,
202    #[serde(flatten)]
203    pub kind: ChatKind,
204}
205
206#[telegram_type(no_doc)]
207#[serde(tag = "type")]
208pub enum ChatKind {
209    Private {
210        first_name: Box<str>,
211        last_name: Option<Box<str>>,
212    },
213    Group {
214        title: Box<str>,
215    },
216    Supergroup {
217        title: Box<str>,
218        #[serde(default)]
219        is_forum: bool,
220    },
221    Channel {
222        title: Box<str>,
223    },
224}
225
226#[telegram_type(untagged, no_doc)]
227pub enum ReplyMarkup<'src> {
228    Keyboard(KeyboardMarkup<'src>),
229    InlineKeyboard(InlineKeyboardMarkup<'src>),
230    #[telegram_type(phantom_fields { remove_keyboard: True })]
231    Remove,
232    ForceReply(ForceReply<'src>),
233    #[serde(serialize_with = "Serializer::serialize_none")]
234    None,
235}
236
237#[telegram_type(name_all(keyboard))]
238pub struct KeyboardMarkup<'src>(pub Vec<Vec<KeyboardButton<'src>>>);
239
240impl<'src, A: IntoIterator<Item = KeyboardButton<'src>>> FromIterator<A> for KeyboardMarkup<'src> {
241    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
242        Self(Vec::from_iter(iter.into_iter().map(Vec::from_iter)))
243    }
244}
245
246impl<'src> KeyboardMarkup<'src> {
247    /// Collects buttons into a keyboard where every row has exactly 1 button 
248    pub fn from_rows(iter: impl IntoIterator<Item = KeyboardButton<'src>>) -> Self {
249        Self(iter.into_iter().map(|x| vec![x]).collect())
250    }
251}
252
253#[telegram_type(name_all(inline_keyboard))]
254#[derive(Default)]
255pub struct InlineKeyboardMarkup<'src>(pub Vec<Vec<InlineKeyboardButton<'src>>>);
256
257impl<'src, A: IntoIterator<Item = InlineKeyboardButton<'src>>> FromIterator<A> for InlineKeyboardMarkup<'src> {
258    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
259        Self(Vec::from_iter(iter.into_iter().map(Vec::from_iter)))
260    }
261}
262
263impl<'src> InlineKeyboardMarkup<'src> {
264    /// Collects buttons into a keyboard where every row has exactly 1 button 
265    pub fn from_rows(iter: impl IntoIterator<Item = InlineKeyboardButton<'src>>) -> Self {
266        Self(iter.into_iter().map(|x| vec![x]).collect())
267    }
268}
269
270#[telegram_type(phantom_fields { force_reply: True })]
271#[derive(Default)]
272pub struct ForceReply<'src> {
273    pub input_field_placeholder: Option<Cow<'src, str>>,
274}
275
276#[telegram_type]
277pub struct KeyboardButton<'src> {
278    pub text: Cow<'src, str>,
279}
280
281impl<'src, T: Into<Cow<'src, str>>> From<T> for KeyboardButton<'src> {
282    fn from(value: T) -> Self {
283        Self { text: value.into() }
284    }
285}
286
287#[telegram_type]
288pub struct InlineKeyboardButton<'src> {
289    pub text: Cow<'src, str>,
290    #[serde(skip_serializing_if = "str::is_empty")]
291    pub callback_data: Cow<'src, str>,
292    #[serde(skip_serializing_if = "str::is_empty")]
293    pub url: Cow<'src, str>,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
297pub enum ParseMode {
298    #[serde(rename = "MarkdownV2")]
299    Markdown,
300    #[serde(rename = "HTML")]
301    Html,
302}
303
304#[telegram_type(no_eq)]
305pub struct CallbackQuery {
306    pub id: Box<str>,
307    pub from: User,
308    pub data: Box<str>,
309    #[serde(flatten)]
310    pub message: Option<CallbackQueryMessage>,
311}
312
313// TODO: replace with MaybeInaccessibleMessage
314#[telegram_type(no_eq, untagged, no_doc)]
315pub enum CallbackQueryMessage {
316    Message {
317        message: Message,
318    },
319    InlineMessage {
320        #[serde(rename = "inline_message_id")]
321        id: Box<str>,
322    },
323}
324
325#[telegram_type]
326pub struct ChatBoostUpdated {
327    pub chat: Chat,
328    pub boost: ChatBoost,
329}
330
331#[telegram_type]
332pub struct ChatBoost {
333    #[serde(rename = "boost_id")]
334    pub id: Box<str>,
335    pub add_date: Date,
336    pub expiration_date: Date,
337    pub source: ChatBoostSource,
338}
339
340#[telegram_type]
341#[serde(tag = "source")]
342pub enum ChatBoostSource {
343    GiftCode {
344        user: User,
345    },
346    Giveaway {
347        user: User,
348        #[serde(deserialize_with = "deserialize_via_try_into::<i32, _, _>")]
349        giveaway_message_id: Option<MessageId>,
350        #[serde(default)]
351        prize_star_count: u32,
352        #[serde(default)]
353        is_unclaimed: bool,
354    },
355    Premium {
356        user: User,
357    },
358}
359
360#[telegram_type(copy, no_doc)]
361#[serde(transparent)]
362// TODO: feature-gated parsing as a date
363/// A date represented as a Unix timestamp.
364pub struct Date(pub i64);
365
366#[derive(Debug, Clone, PartialEq, Serialize)]
367#[serde(transparent)]
368pub struct InputFile<'src> {
369    kind: InputFileKind<'src>,
370    #[serde(skip_serializing)]
371    file_name: Option<Cow<'src, str>>,
372}
373
374impl Multipart for InputFile<'_> {
375    fn get_payload(&self) -> Option<Part> {
376        match &self.kind {
377            InputFileKind::UrlOrFileId(_) => None,
378            InputFileKind::Payload(payload) => {
379                let bytes = payload.get_shared();
380                let bytes_len = bytes.len();
381                let mut res = Part::stream_with_length(bytes, bytes_len as u64);
382                if let Some(filename) = self.file_name.as_deref() {
383                    res = res.file_name(filename.to_owned());
384                }
385                Some(res)
386            }
387        }
388    }
389
390    fn get_payload_mut(&mut self) -> Option<Part> {
391        match &mut self.kind {
392            InputFileKind::UrlOrFileId(_) => None,
393            InputFileKind::Payload(payload) => {
394                let mut res =
395                    match replace(payload, Payload(Mutex::new(PayloadRepr::Owned(vec![]))))
396                        .into_body()
397                    {
398                        (Some(len), body) => Part::stream_with_length(body, len as u64),
399                        (None, body) => Part::stream(body),
400                    };
401                if let Some(filename) = self.file_name.take() {
402                    res = res.file_name(filename.into_owned());
403                }
404                Some(res)
405            }
406        }
407    }
408}
409
410impl<'src> InputFile<'src> {
411    /// Creates an input file that points to a resource on the internet that Telegram should fetch.
412    pub fn url(url: impl Into<Cow<'src, str>>) -> Self {
413        Self { kind: InputFileKind::UrlOrFileId(url.into()), file_name: None }
414    }
415
416    /// Creates an input file that points to a file stored by Telegram by its ID
417    pub fn file_id(file_id: impl Into<Cow<'src, str>>) -> Self {
418        Self { kind: InputFileKind::UrlOrFileId(file_id.into()), file_name: None }
419    }
420
421    /// Creates an input file that's represented by bytes in memory. The ownership of the bytes is
422    /// transferred to the file.
423    pub fn memory(vec: Vec<u8>) -> Self {
424        Self { kind: InputFileKind::Payload(PayloadRepr::Owned(vec).into()), file_name: None }
425    }
426
427    /// Creates an input file that's represented by a shared span of bytes in memory.
428    pub fn shared_memory(bytes: Bytes) -> Self {
429        Self { kind: InputFileKind::Payload(PayloadRepr::Shared(bytes).into()), file_name: None }
430    }
431
432    /// Creates an input file that's represented by an async reader
433    pub fn reader(reader: impl AsyncRead + Send + Sync + Unpin + 'static) -> Self {
434        Self {
435            kind: InputFileKind::Payload(PayloadRepr::Reader(Box::new(reader)).into()),
436            file_name: None,
437        }
438    }
439
440    /// Creates an input file from a span of text, either owned or static
441    pub fn text(text: impl Into<Cow<'static, str>>) -> Self {
442        match text.into() {
443            Cow::Owned(text) => Self::memory(text.into_bytes()),
444            Cow::Borrowed(text) => Self::shared_memory(Bytes::from_static(text.as_bytes())),
445        }
446    }
447
448    /// Sets the name of the input file with which it should be labelled in Telegram
449    pub fn file_name(self, file_name: impl Into<Cow<'src, str>>) -> Self {
450        Self { file_name: Some(file_name.into()), ..self }
451    }
452
453    /// Makes the input file reusable by loading a reader into memory, if this input file is backed
454    /// by one. After calling this function, the input file can be cloned and
455    /// [`to_future`](crate::Request::to_future) can be called on the containing request.
456    ///
457    /// Should be called if the file could be backed by a reader and the containing request could be
458    /// sent more than once.
459    pub async fn reusable(self) -> Result<Self> {
460        Ok(match self.kind {
461            InputFileKind::UrlOrFileId(_) => self,
462            InputFileKind::Payload(p) => Self {
463                kind: InputFileKind::Payload(p.0.into_inner().unwrap().reusable().await?.into()),
464                ..self
465            },
466        })
467    }
468}
469
470#[derive(Debug, Clone, PartialEq, Serialize)]
471#[serde(untagged)]
472enum InputFileKind<'src> {
473    UrlOrFileId(Cow<'src, str>),
474    #[serde(serialize_with = "serialize_payload")]
475    Payload(Payload),
476}
477
478fn serialize_payload<S: Serializer>(_: &'_ Payload, s: S) -> Result<S::Ok, S::Error> {
479    s.serialize_str(PAYLOAD_LINK)
480}
481
482struct Payload(Mutex<PayloadRepr>);
483
484impl From<PayloadRepr> for Payload {
485    fn from(repr: PayloadRepr) -> Self {
486        Self(Mutex::new(repr))
487    }
488}
489
490impl PartialEq for Payload {
491    fn eq(&self, other: &Self) -> bool {
492        let (Ok(lhs), Ok(rhs)) = (self.0.try_lock(), other.0.try_lock()) else {
493            return false;
494        };
495        match (&*lhs, &*rhs) {
496            (PayloadRepr::Shared(b1), PayloadRepr::Shared(b2)) => b1 == b2,
497            (PayloadRepr::Owned(v1), PayloadRepr::Owned(v2)) => v1 == v2,
498            _ => false,
499        }
500    }
501}
502
503impl Debug for Payload {
504    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
505        f.debug_struct("Payload").finish_non_exhaustive()
506    }
507}
508
509impl Clone for Payload {
510    fn clone(&self) -> Self {
511        Self(Mutex::new(PayloadRepr::Shared(self.0.lock().unwrap().make_shared())))
512    }
513}
514
515impl From<Payload> for Body {
516    fn from(payload: Payload) -> Self {
517        match payload.0.into_inner().unwrap() {
518            PayloadRepr::Shared(bytes) => bytes.into(),
519            PayloadRepr::Owned(vec) => vec.into(),
520            PayloadRepr::Reader(reader) => Body::wrap_stream(ReaderStream::new(reader)),
521        }
522    }
523}
524
525impl Payload {
526    fn get_shared(&self) -> Bytes {
527        self.0.lock().unwrap().make_shared()
528    }
529
530    /// Returns the body and, optionally, its length
531    fn into_body(self) -> (Option<usize>, Body) {
532        match self.0.into_inner().unwrap() {
533            PayloadRepr::Shared(bytes) => (bytes.len().into(), bytes.into()),
534            PayloadRepr::Owned(vec) => (vec.len().into(), vec.into()),
535            PayloadRepr::Reader(reader) => (None, Body::wrap_stream(ReaderStream::new(reader))),
536        }
537    }
538}
539
540enum PayloadRepr {
541    Shared(Bytes),
542    Owned(Vec<u8>),
543    Reader(Box<dyn AsyncRead + Send + Sync + Unpin>),
544}
545
546impl PayloadRepr {
547    /// # Panics
548    /// Panics if `self` is `Reader`.
549    fn make_shared(&mut self) -> Bytes {
550        match self {
551            PayloadRepr::Shared(b) => b.clone(),
552            PayloadRepr::Owned(vec) => {
553                let bytes = Bytes::from(take(vec));
554                *self = PayloadRepr::Shared(bytes.clone());
555                bytes
556            }
557            PayloadRepr::Reader(_) => {
558                panic!("PayloadRepr::make_shared: can't extract bytes from an async reader")
559            }
560        }
561    }
562
563    async fn reusable(self) -> io::Result<Self> {
564        Ok(Self::Shared(match self {
565            PayloadRepr::Shared(bytes) => bytes,
566            PayloadRepr::Owned(vec) => vec.into(),
567            PayloadRepr::Reader(mut reader) => {
568                let mut buf = BytesMut::new();
569                reader.read_buf(&mut buf).await?;
570                buf.freeze()
571            }
572        }))
573    }
574}
575
576#[telegram_type(no_eq, copy)]
577pub struct Location {
578    pub latitude: f64,
579    pub longitude: f64,
580    #[serde(default, skip_serializing_if = "Option::is_none")]
581    pub horizontal_accuracy: Option<f64>,
582    #[serde(default, skip_serializing_if = "Option::is_none")]
583    pub live_period: Option<u32>,
584    #[serde(default, skip_serializing_if = "Option::is_none")]
585    pub heading: Option<u16>,
586    #[serde(default, skip_serializing_if = "Option::is_none")]
587    pub proximity_alert_radius: Option<u32>,
588}
589
590fn deserialize_via_try_into<'de, Medium, Out, D>(d: D) -> Result<Option<Out>, D::Error>
591where
592    D: Deserializer<'de>,
593    Medium: DeserializeOwned + TryInto<Out>,
594{
595    Medium::deserialize(d).map(|x| x.try_into().ok())
596}