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