Skip to main content

tgbot/types/definitions/text/
entities.rs

1use std::{
2    convert::TryFrom,
3    error::Error,
4    fmt,
5    ops::{Index, IndexMut, Range},
6};
7
8use serde::{Deserialize, Serialize};
9use serde_json::Error as JsonError;
10
11use crate::types::{Integer, User};
12
13/// Represents a collection of text entities.
14#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
15#[serde(into = "Vec<TextEntity>", try_from = "Vec<RawTextEntity>")]
16pub struct TextEntities {
17    items: Vec<TextEntity>,
18}
19
20impl TextEntities {
21    /// Pushes a new entity into the collection.
22    ///
23    /// # Arguments
24    ///
25    /// * `value` - The entity to push.
26    pub fn push(&mut self, value: TextEntity) {
27        self.items.push(value);
28    }
29
30    /// Serializes text entities into a JSON string.
31    pub fn serialize(&self) -> Result<String, TextEntityError> {
32        serde_json::to_string(self).map_err(TextEntityError::Serialize)
33    }
34}
35
36impl TryFrom<Vec<RawTextEntity>> for TextEntities {
37    type Error = TextEntityError;
38
39    fn try_from(entities: Vec<RawTextEntity>) -> Result<Self, Self::Error> {
40        entities
41            .into_iter()
42            .map(TryFrom::try_from)
43            .collect::<Result<Vec<TextEntity>, _>>()
44            .map(|items| Self { items })
45    }
46}
47
48impl FromIterator<TextEntity> for TextEntities {
49    fn from_iter<T: IntoIterator<Item = TextEntity>>(iter: T) -> Self {
50        Self {
51            items: iter.into_iter().collect(),
52        }
53    }
54}
55
56impl IntoIterator for TextEntities {
57    type Item = TextEntity;
58    type IntoIter = std::vec::IntoIter<Self::Item>;
59
60    fn into_iter(self) -> Self::IntoIter {
61        self.items.into_iter()
62    }
63}
64
65impl<'a> IntoIterator for &'a TextEntities {
66    type Item = &'a TextEntity;
67    type IntoIter = std::slice::Iter<'a, TextEntity>;
68
69    fn into_iter(self) -> Self::IntoIter {
70        self.items.as_slice().iter()
71    }
72}
73
74impl<'a> IntoIterator for &'a mut TextEntities {
75    type Item = &'a mut TextEntity;
76    type IntoIter = std::slice::IterMut<'a, TextEntity>;
77
78    fn into_iter(self) -> Self::IntoIter {
79        self.items.as_mut_slice().iter_mut()
80    }
81}
82
83impl Index<usize> for TextEntities {
84    type Output = TextEntity;
85
86    fn index(&self, index: usize) -> &Self::Output {
87        &self.items[index]
88    }
89}
90
91impl IndexMut<usize> for TextEntities {
92    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
93        &mut self.items[index]
94    }
95}
96
97impl From<TextEntities> for Vec<TextEntity> {
98    fn from(entities: TextEntities) -> Self {
99        entities.items
100    }
101}
102
103/// Represents an entity in a text.
104#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
105#[serde(try_from = "RawTextEntity", into = "RawTextEntity")]
106pub enum TextEntity {
107    /// A block quotation.
108    Blockquote(TextEntityPosition),
109    /// A bold text.
110    Bold(TextEntityPosition),
111    /// A bot command.
112    BotCommand(TextEntityPosition),
113    /// A cashtag.
114    Cashtag(TextEntityPosition),
115    /// A monospace string.
116    Code(TextEntityPosition),
117    /// An inline custom emoji sticker.
118    CustomEmoji {
119        /// Unique identifier of the custom emoji.
120        ///
121        /// Use [`crate::types::GetCustomEmojiStickers`] to get full information about the sticker.
122        custom_emoji_id: String,
123        /// Position of entity in text.
124        position: TextEntityPosition,
125    },
126    /// Formatted date and time.
127    DateTime {
128        /// Position of entity in text.
129        position: TextEntityPosition,
130        /// Unix time associated with the entity.
131        unix_time: Option<Integer>,
132        /// String that defines the formatting of the date and time.
133        ///
134        /// See (date-time entity formatting)[1] for more details.
135        ///
136        /// [1]: https://core.telegram.org/bots/api#date-time-entity-formatting
137        format: Option<String>,
138    },
139    /// An E-Mail.
140    Email(TextEntityPosition),
141    /// Collapsed-by-default block quotation.
142    ExpandableBlockquote(TextEntityPosition),
143    /// A hashtag.
144    Hashtag(TextEntityPosition),
145    /// An italic text.
146    Italic(TextEntityPosition),
147    /// A user mention (e.g. @username).
148    Mention(TextEntityPosition),
149    /// A phone number.
150    PhoneNumber(TextEntityPosition),
151    /// A monospace block.
152    Pre {
153        /// The position of the entity in the text.
154        position: TextEntityPosition,
155        /// The name of the programming language.
156        language: Option<String>,
157    },
158    /// A spoiler message.
159    Spoiler(TextEntityPosition),
160    /// A strikethrough text.
161    Strikethrough(TextEntityPosition),
162    /// A clickable text URLs.
163    TextLink {
164        /// The position of the entity in the text.
165        position: TextEntityPosition,
166        /// URL that will be opened after user taps on the text.
167        url: String,
168    },
169    /// A user mention without a username.
170    TextMention {
171        /// The position of the entity in the text.
172        position: TextEntityPosition,
173        /// The mentioned user.
174        user: User,
175    },
176    /// An underlined text.
177    Underline(TextEntityPosition),
178    /// An URL.
179    Url(TextEntityPosition),
180}
181
182macro_rules! text_entity_factory {
183    ($($method_name:ident => $enum_variant: ident),*) => {
184        $(
185            /// Creates a new `TextEntity`.
186            ///
187            /// # Arguments
188            ///
189            /// * `pos` - Position of TextEntity in UTF-16 code units.
190            pub fn $method_name<T: Into<TextEntityPosition>>(pos: T) -> Self {
191                Self::$enum_variant(pos.into())
192            }
193        )*
194    };
195}
196
197impl TextEntity {
198    text_entity_factory!(
199        blockquote => Blockquote,
200        bold => Bold,
201        bot_command => BotCommand,
202        cashtag => Cashtag,
203        code => Code,
204        email => Email,
205        expandable_blockquote => ExpandableBlockquote,
206        hashtag => Hashtag,
207        italic => Italic,
208        mention => Mention,
209        phone_number => PhoneNumber,
210        spoiler => Spoiler,
211        strikethrough => Strikethrough,
212        underline => Underline
213    );
214
215    /// Creates a new `TextEntity`.
216    ///
217    /// # Arguments
218    ///
219    /// * `pos` - Position of the entity in UTF-16 code units.
220    /// * `custom_emoji_id` - Unique identifier of the custom emoji.
221    pub fn custom_emoji<A, B>(pos: A, custom_emoji_id: B) -> Self
222    where
223        A: Into<TextEntityPosition>,
224        B: Into<String>,
225    {
226        Self::CustomEmoji {
227            position: pos.into(),
228            custom_emoji_id: custom_emoji_id.into(),
229        }
230    }
231
232    /// Creates a new `TextEntity`.
233    ///
234    /// # Arguments
235    ///
236    /// * `pos` - Position of the entity in UTF-16 code units.
237    /// * `unix_time` - Unix time associated with the entity.
238    /// * `format` - String that defines the formatting of the date and time.
239    pub fn date_time<A, B>(pos: A, unix_time: Option<Integer>, format: Option<B>) -> Self
240    where
241        A: Into<TextEntityPosition>,
242        B: Into<String>,
243    {
244        Self::DateTime {
245            position: pos.into(),
246            unix_time,
247            format: format.map(Into::into),
248        }
249    }
250
251    /// Creates a new `TextEntity`.
252    ///
253    /// # Arguments
254    ///
255    /// * `pos` - Position of the entity in UTF-16 code units.
256    /// * `language` - The programming language of the entity text.
257    pub fn pre<A, B>(pos: A, language: Option<B>) -> Self
258    where
259        A: Into<TextEntityPosition>,
260        B: Into<String>,
261    {
262        Self::Pre {
263            position: pos.into(),
264            language: language.map(|x| x.into()),
265        }
266    }
267
268    /// Creates a new `TextEntity`.
269    ///
270    /// # Arguments
271    ///
272    /// * `pos` - The position of the entity in UTF-16 code units.
273    /// * `url` - The URL that will be opened after user taps on the text.
274    pub fn text_link<A, B>(pos: A, url: B) -> Self
275    where
276        A: Into<TextEntityPosition>,
277        B: Into<String>,
278    {
279        Self::TextLink {
280            position: pos.into(),
281            url: url.into(),
282        }
283    }
284
285    /// Creates a new `TextEntity`.
286    ///
287    /// # Arguments
288    ///
289    /// * `pos` - The position of the entity in UTF-16 code units.
290    /// * `user` - The user to be mentioned.
291    pub fn text_mention<T>(pos: T, user: User) -> Self
292    where
293        T: Into<TextEntityPosition>,
294    {
295        Self::TextMention {
296            position: pos.into(),
297            user,
298        }
299    }
300}
301
302#[derive(Clone, Debug, Deserialize, Serialize)]
303struct RawTextEntity {
304    offset: u32,
305    length: u32,
306    #[serde(flatten)]
307    entity_type: RawTextEntityType,
308}
309
310#[serde_with::skip_serializing_none]
311#[derive(Clone, Debug, Deserialize, Serialize)]
312#[serde(rename_all = "snake_case")]
313#[serde(tag = "type")]
314enum RawTextEntityType {
315    Blockquote,
316    Bold,
317    BotCommand,
318    Cashtag,
319    Code,
320    CustomEmoji {
321        custom_emoji_id: Option<String>,
322    },
323    DateTime {
324        date_time_format: Option<String>,
325        unix_time: Option<Integer>,
326    },
327    Email,
328    ExpandableBlockquote,
329    Hashtag,
330    Italic,
331    Mention,
332    PhoneNumber,
333    Pre {
334        language: Option<String>,
335    },
336    Spoiler,
337    Strikethrough,
338    TextLink {
339        url: Option<String>,
340    },
341    TextMention {
342        user: Option<User>,
343    },
344    Underline,
345    Url,
346}
347
348/// Represents an error when parsing/serializing entities.
349#[derive(Debug)]
350pub enum TextEntityError {
351    /// Custom emoji is required for custom_emoji entity.
352    NoCustomEmoji,
353    /// URL is required for `text_link` entity.
354    NoUrl,
355    /// User is required for `text_mention` entity.
356    NoUser,
357    /// Failed to serialize entities.
358    Serialize(JsonError),
359}
360
361impl Error for TextEntityError {
362    fn source(&self) -> Option<&(dyn Error + 'static)> {
363        match self {
364            Self::Serialize(err) => Some(err),
365            _ => None,
366        }
367    }
368}
369
370impl fmt::Display for TextEntityError {
371    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
372        use self::TextEntityError::*;
373        write!(
374            out,
375            "{}",
376            match self {
377                NoCustomEmoji => String::from("Custom emoji is required for custom_emoji entity"),
378                NoUrl => String::from("URL is required for text_link entity"),
379                NoUser => String::from("user is required for text_mention entity"),
380                Serialize(err) => format!("failed to serialize text entities: {err}"),
381            }
382        )
383    }
384}
385
386impl TryFrom<RawTextEntity> for TextEntity {
387    type Error = TextEntityError;
388
389    fn try_from(raw: RawTextEntity) -> Result<Self, Self::Error> {
390        let position = TextEntityPosition {
391            offset: raw.offset,
392            length: raw.length,
393        };
394
395        Ok(match raw.entity_type {
396            RawTextEntityType::Blockquote => Self::Blockquote(position),
397            RawTextEntityType::Bold => Self::Bold(position),
398            RawTextEntityType::BotCommand => Self::BotCommand(position),
399            RawTextEntityType::Cashtag => Self::Cashtag(position),
400            RawTextEntityType::Code => Self::Code(position),
401            RawTextEntityType::CustomEmoji { custom_emoji_id } => Self::CustomEmoji {
402                position,
403                custom_emoji_id: custom_emoji_id.ok_or(TextEntityError::NoCustomEmoji)?,
404            },
405            RawTextEntityType::DateTime {
406                unix_time,
407                date_time_format,
408            } => Self::DateTime {
409                position,
410                unix_time,
411                format: date_time_format,
412            },
413            RawTextEntityType::Email => Self::Email(position),
414            RawTextEntityType::ExpandableBlockquote => Self::ExpandableBlockquote(position),
415            RawTextEntityType::Hashtag => Self::Hashtag(position),
416            RawTextEntityType::Italic => Self::Italic(position),
417            RawTextEntityType::Mention => Self::Mention(position),
418            RawTextEntityType::PhoneNumber => Self::PhoneNumber(position),
419            RawTextEntityType::Pre { language } => Self::Pre { position, language },
420            RawTextEntityType::Spoiler => Self::Spoiler(position),
421            RawTextEntityType::Strikethrough => Self::Strikethrough(position),
422            RawTextEntityType::TextLink { url } => Self::TextLink {
423                position,
424                url: url.ok_or(TextEntityError::NoUrl)?,
425            },
426            RawTextEntityType::TextMention { user } => Self::TextMention {
427                position,
428                user: user.ok_or(TextEntityError::NoUser)?,
429            },
430            RawTextEntityType::Underline => Self::Underline(position),
431            RawTextEntityType::Url => Self::Url(position),
432        })
433    }
434}
435
436impl From<TextEntity> for RawTextEntity {
437    fn from(entity: TextEntity) -> Self {
438        macro_rules! raw {
439            ($entity_type:ident($position:ident $( $($item:ident)+ )?)) => {
440                Self {
441                    entity_type: RawTextEntityType::$entity_type $( { $($item: $item.into(),)+ } )?,
442                    offset: $position.offset as _,
443                    length: $position.length as _,
444                }
445            };
446        }
447        match entity {
448            TextEntity::Blockquote(p) => raw!(Blockquote(p)),
449            TextEntity::Bold(p) => raw!(Bold(p)),
450            TextEntity::BotCommand(p) => raw!(BotCommand(p)),
451            TextEntity::Cashtag(p) => raw!(Cashtag(p)),
452            TextEntity::Code(p) => raw!(Code(p)),
453            TextEntity::CustomEmoji {
454                position: p,
455                custom_emoji_id,
456            } => raw!(CustomEmoji(p custom_emoji_id)),
457            TextEntity::DateTime {
458                position: p,
459                unix_time,
460                format: date_time_format,
461            } => raw!(DateTime(p unix_time date_time_format)),
462            TextEntity::Email(p) => raw!(Email(p)),
463            TextEntity::ExpandableBlockquote(p) => raw!(ExpandableBlockquote(p)),
464            TextEntity::Hashtag(p) => raw!(Hashtag(p)),
465            TextEntity::Italic(p) => raw!(Italic(p)),
466            TextEntity::Mention(p) => raw!(Mention(p)),
467            TextEntity::PhoneNumber(p) => raw!(PhoneNumber(p)),
468            TextEntity::Pre { position: p, language } => raw!(Pre(p language)),
469            TextEntity::Spoiler(p) => raw!(Spoiler(p)),
470            TextEntity::Strikethrough(p) => raw!(Strikethrough(p)),
471            TextEntity::TextLink { position: p, url } => raw!(TextLink(p url)),
472            TextEntity::TextMention { position: p, user } => raw!(TextMention(p user)),
473            TextEntity::Underline(p) => raw!(Underline(p)),
474            TextEntity::Url(p) => raw!(Url(p)),
475        }
476    }
477}
478
479/// Represents a bot command found in text.
480///
481/// Use [`TextEntity::BotCommand`] to get a position of the command.
482#[derive(Clone, Debug, PartialEq, PartialOrd)]
483pub struct TextEntityBotCommand {
484    /// Actual command.
485    pub command: String,
486    /// Username of a bot.
487    pub bot_name: Option<String>,
488}
489
490/// Represents a position of an entity in a text.
491#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
492pub struct TextEntityPosition {
493    /// Offset in UTF-16 code units to the start of the entity.
494    pub offset: u32,
495    /// Length of the entity in UTF-16 code units.
496    pub length: u32,
497}
498
499impl From<Range<u32>> for TextEntityPosition {
500    fn from(range: Range<u32>) -> Self {
501        Self {
502            offset: range.start,
503            length: range.end - range.start,
504        }
505    }
506}