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#[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 pub fn push(&mut self, value: TextEntity) {
27 self.items.push(value);
28 }
29
30 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#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
105#[serde(try_from = "RawTextEntity", into = "RawTextEntity")]
106pub enum TextEntity {
107 Blockquote(TextEntityPosition),
109 Bold(TextEntityPosition),
111 BotCommand(TextEntityPosition),
113 Cashtag(TextEntityPosition),
115 Code(TextEntityPosition),
117 CustomEmoji {
119 custom_emoji_id: String,
123 position: TextEntityPosition,
125 },
126 Email(TextEntityPosition),
128 ExpandableBlockquote(TextEntityPosition),
130 Hashtag(TextEntityPosition),
132 Italic(TextEntityPosition),
134 Mention(TextEntityPosition),
136 PhoneNumber(TextEntityPosition),
138 Pre {
140 position: TextEntityPosition,
142 language: Option<String>,
144 },
145 Spoiler(TextEntityPosition),
147 Strikethrough(TextEntityPosition),
149 TextLink {
151 position: TextEntityPosition,
153 url: String,
155 },
156 TextMention {
158 position: TextEntityPosition,
160 user: User,
162 },
163 Underline(TextEntityPosition),
165 Url(TextEntityPosition),
167}
168
169macro_rules! text_entity_factory {
170 ($($method_name:ident => $enum_variant: ident),*) => {
171 $(
172 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 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 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 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 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#[derive(Debug)]
291pub enum TextEntityError {
292 NoCustomEmoji,
294 NoUrl,
296 NoUser,
298 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#[derive(Clone, Debug, PartialEq, PartialOrd)]
411pub struct TextEntityBotCommand {
412 pub command: String,
414 pub bot_name: Option<String>,
416}
417
418#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
420pub struct TextEntityPosition {
421 pub offset: u32,
423 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}