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::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    /// An E-Mail.
127    Email(TextEntityPosition),
128    /// Collapsed-by-default block quotation.
129    ExpandableBlockquote(TextEntityPosition),
130    /// A hashtag.
131    Hashtag(TextEntityPosition),
132    /// An italic text.
133    Italic(TextEntityPosition),
134    /// A user mention (e.g. @username).
135    Mention(TextEntityPosition),
136    /// A phone number.
137    PhoneNumber(TextEntityPosition),
138    /// A monospace block.
139    Pre {
140        /// The position of the entity in the text.
141        position: TextEntityPosition,
142        /// The name of the programming language.
143        language: Option<String>,
144    },
145    /// A spoiler message.
146    Spoiler(TextEntityPosition),
147    /// A strikethrough text.
148    Strikethrough(TextEntityPosition),
149    /// A clickable text URLs.
150    TextLink {
151        /// The position of the entity in the text.
152        position: TextEntityPosition,
153        /// URL that will be opened after user taps on the text.
154        url: String,
155    },
156    /// A user mention without a username.
157    TextMention {
158        /// The position of the entity in the text.
159        position: TextEntityPosition,
160        /// The mentioned user.
161        user: User,
162    },
163    /// An underlined text.
164    Underline(TextEntityPosition),
165    /// An URL.
166    Url(TextEntityPosition),
167}
168
169macro_rules! text_entity_factory {
170    ($($method_name:ident => $enum_variant: ident),*) => {
171        $(
172            /// Creates a new `TextEntity`.
173            ///
174            /// # Arguments
175            ///
176            /// * `pos` - Position of TextEntity in UTF-16 code units.
177            pub fn $method_name<T: Into<TextEntityPosition>>(pos: T) -> TextEntity {
178                TextEntity::$enum_variant(pos.into())
179            }
180        )*
181    };
182}
183
184impl TextEntity {
185    text_entity_factory!(
186        blockquote => Blockquote,
187        bold => Bold,
188        bot_command => BotCommand,
189        cashtag => Cashtag,
190        code => Code,
191        email => Email,
192        expandable_blockquote => ExpandableBlockquote,
193        hashtag => Hashtag,
194        italic => Italic,
195        mention => Mention,
196        phone_number => PhoneNumber,
197        spoiler => Spoiler,
198        strikethrough => Strikethrough,
199        underline => Underline
200    );
201
202    /// Creates a new `TextEntity`.
203    ///
204    /// # Arguments
205    ///
206    /// * `pos` - Position of the entity in UTF-16 code units.
207    /// * `custom_emoji_id` - Unique identifier of the custom emoji.
208    pub fn custom_emoji<P: Into<TextEntityPosition>, I: Into<String>>(pos: P, custom_emoji_id: I) -> TextEntity {
209        TextEntity::CustomEmoji {
210            position: pos.into(),
211            custom_emoji_id: custom_emoji_id.into(),
212        }
213    }
214
215    /// Creates a new `TextEntity`.
216    ///
217    /// # Arguments
218    ///
219    /// * `pos` - Position of the entity in UTF-16 code units.
220    /// * `language` - The programming language of the entity text.
221    pub fn pre<P: Into<TextEntityPosition>, L: Into<String>>(pos: P, language: Option<L>) -> TextEntity {
222        TextEntity::Pre {
223            position: pos.into(),
224            language: language.map(|x| x.into()),
225        }
226    }
227
228    /// Creates a new `TextEntity`.
229    ///
230    /// # Arguments
231    ///
232    /// * `pos` - The position of the entity in UTF-16 code units.
233    /// * `url` - The URL that will be opened after user taps on the text.
234    pub fn text_link<P: Into<TextEntityPosition>, U: Into<String>>(pos: P, url: U) -> TextEntity {
235        TextEntity::TextLink {
236            position: pos.into(),
237            url: url.into(),
238        }
239    }
240
241    /// Creates a new `TextEntity`.
242    ///
243    /// # Arguments
244    ///
245    /// * `pos` - The position of the entity in UTF-16 code units.
246    /// * `user` - The user to be mentioned.
247    pub fn text_mention<P: Into<TextEntityPosition>>(pos: P, user: User) -> TextEntity {
248        TextEntity::TextMention {
249            position: pos.into(),
250            user,
251        }
252    }
253}
254
255#[derive(Clone, Debug, Deserialize, Serialize)]
256struct RawTextEntity {
257    offset: u32,
258    length: u32,
259    #[serde(flatten)]
260    entity_type: RawTextEntityType,
261}
262
263#[serde_with::skip_serializing_none]
264#[derive(Clone, Debug, Deserialize, Serialize)]
265#[serde(rename_all = "snake_case")]
266#[serde(tag = "type")]
267enum RawTextEntityType {
268    Blockquote,
269    Bold,
270    BotCommand,
271    Cashtag,
272    Code,
273    CustomEmoji { custom_emoji_id: Option<String> },
274    Email,
275    ExpandableBlockquote,
276    Hashtag,
277    Italic,
278    Mention,
279    PhoneNumber,
280    Pre { language: Option<String> },
281    Spoiler,
282    Strikethrough,
283    TextLink { url: Option<String> },
284    TextMention { user: Option<User> },
285    Underline,
286    Url,
287}
288
289/// Represents an error when parsing/serializing entities.
290#[derive(Debug)]
291pub enum TextEntityError {
292    /// Custom emoji is required for custom_emoji entity.
293    NoCustomEmoji,
294    /// URL is required for `text_link` entity.
295    NoUrl,
296    /// User is required for `text_mention` entity.
297    NoUser,
298    /// Failed to serialize entities.
299    Serialize(JsonError),
300}
301
302impl Error for TextEntityError {
303    fn source(&self) -> Option<&(dyn Error + 'static)> {
304        match self {
305            Self::Serialize(err) => Some(err),
306            _ => None,
307        }
308    }
309}
310
311impl fmt::Display for TextEntityError {
312    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
313        use self::TextEntityError::*;
314        write!(
315            out,
316            "{}",
317            match self {
318                NoCustomEmoji => String::from("Custom emoji is required for custom_emoji entity"),
319                NoUrl => String::from("URL is required for text_link entity"),
320                NoUser => String::from("user is required for text_mention entity"),
321                Serialize(err) => format!("failed to serialize text entities: {err}"),
322            }
323        )
324    }
325}
326
327impl TryFrom<RawTextEntity> for TextEntity {
328    type Error = TextEntityError;
329
330    fn try_from(raw: RawTextEntity) -> Result<Self, Self::Error> {
331        let position = TextEntityPosition {
332            offset: raw.offset,
333            length: raw.length,
334        };
335
336        Ok(match raw.entity_type {
337            RawTextEntityType::Blockquote => TextEntity::Blockquote(position),
338            RawTextEntityType::Bold => TextEntity::Bold(position),
339            RawTextEntityType::BotCommand => TextEntity::BotCommand(position),
340            RawTextEntityType::Cashtag => TextEntity::Cashtag(position),
341            RawTextEntityType::Code => TextEntity::Code(position),
342            RawTextEntityType::CustomEmoji { custom_emoji_id } => TextEntity::CustomEmoji {
343                position,
344                custom_emoji_id: custom_emoji_id.ok_or(TextEntityError::NoCustomEmoji)?,
345            },
346            RawTextEntityType::Email => TextEntity::Email(position),
347            RawTextEntityType::ExpandableBlockquote => TextEntity::ExpandableBlockquote(position),
348            RawTextEntityType::Hashtag => TextEntity::Hashtag(position),
349            RawTextEntityType::Italic => TextEntity::Italic(position),
350            RawTextEntityType::Mention => TextEntity::Mention(position),
351            RawTextEntityType::PhoneNumber => TextEntity::PhoneNumber(position),
352            RawTextEntityType::Pre { language } => TextEntity::Pre { position, language },
353            RawTextEntityType::Spoiler => TextEntity::Spoiler(position),
354            RawTextEntityType::Strikethrough => TextEntity::Strikethrough(position),
355            RawTextEntityType::TextLink { url } => TextEntity::TextLink {
356                position,
357                url: url.ok_or(TextEntityError::NoUrl)?,
358            },
359            RawTextEntityType::TextMention { user } => TextEntity::TextMention {
360                position,
361                user: user.ok_or(TextEntityError::NoUser)?,
362            },
363            RawTextEntityType::Underline => TextEntity::Underline(position),
364            RawTextEntityType::Url => TextEntity::Url(position),
365        })
366    }
367}
368
369impl From<TextEntity> for RawTextEntity {
370    fn from(entity: TextEntity) -> Self {
371        macro_rules! raw {
372            ($entity_type:ident($position:ident $( , $item:ident )?)) => {
373                RawTextEntity {
374                    entity_type: RawTextEntityType::$entity_type $( { $item: $item.into() } )?,
375                    offset: $position.offset as _,
376                    length: $position.length as _,
377                }
378            };
379        }
380        match entity {
381            TextEntity::Blockquote(p) => raw!(Blockquote(p)),
382            TextEntity::Bold(p) => raw!(Bold(p)),
383            TextEntity::BotCommand(p) => raw!(BotCommand(p)),
384            TextEntity::Cashtag(p) => raw!(Cashtag(p)),
385            TextEntity::Code(p) => raw!(Code(p)),
386            TextEntity::CustomEmoji {
387                position: p,
388                custom_emoji_id,
389            } => raw!(CustomEmoji(p, custom_emoji_id)),
390            TextEntity::Email(p) => raw!(Email(p)),
391            TextEntity::ExpandableBlockquote(p) => raw!(ExpandableBlockquote(p)),
392            TextEntity::Hashtag(p) => raw!(Hashtag(p)),
393            TextEntity::Italic(p) => raw!(Italic(p)),
394            TextEntity::Mention(p) => raw!(Mention(p)),
395            TextEntity::PhoneNumber(p) => raw!(PhoneNumber(p)),
396            TextEntity::Pre { position: p, language } => raw!(Pre(p, language)),
397            TextEntity::Spoiler(p) => raw!(Spoiler(p)),
398            TextEntity::Strikethrough(p) => raw!(Strikethrough(p)),
399            TextEntity::TextLink { position: p, url } => raw!(TextLink(p, url)),
400            TextEntity::TextMention { position: p, user } => raw!(TextMention(p, user)),
401            TextEntity::Underline(p) => raw!(Underline(p)),
402            TextEntity::Url(p) => raw!(Url(p)),
403        }
404    }
405}
406
407/// Represents a bot command found in text.
408///
409/// Use [`TextEntity::BotCommand`] to get a position of the command.
410#[derive(Clone, Debug, PartialEq, PartialOrd)]
411pub struct TextEntityBotCommand {
412    /// Actual command.
413    pub command: String,
414    /// Username of a bot.
415    pub bot_name: Option<String>,
416}
417
418/// Represents a position of an entity in a text.
419#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
420pub struct TextEntityPosition {
421    /// Offset in UTF-16 code units to the start of the entity.
422    pub offset: u32,
423    /// Length of the entity in UTF-16 code units.
424    pub length: u32,
425}
426
427impl From<Range<u32>> for TextEntityPosition {
428    fn from(range: Range<u32>) -> Self {
429        Self {
430            offset: range.start,
431            length: range.end - range.start,
432        }
433    }
434}