tgbot/types/definitions/
story.rs

1use std::{error::Error, fmt};
2
3use serde::{Deserialize, Serialize};
4use serde_json::Error as JsonError;
5
6use crate::{
7    api::{Method, Payload},
8    types::{Chat, Float, Integer, LocationAddress, ReactionType},
9};
10
11/// Represents a story.
12#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
13pub struct Story {
14    /// Chat that posted the story.
15    pub chat: Chat,
16    /// Unique identifier of the story in the chat.
17    pub id: Integer,
18}
19
20impl Story {
21    /// Creates a new `Story`.
22    ///
23    /// # Arguments
24    ///
25    /// * `chat` - Chat that posted the story.
26    /// * `id` - Unique identifier of the story in the chat.
27    pub fn new<T>(chat: T, id: Integer) -> Self
28    where
29        T: Into<Chat>,
30    {
31        Self { chat: chat.into(), id }
32    }
33
34    /// Sets a new chat.
35    ///
36    /// # Arguments
37    ///
38    /// * `value` - Chat that posted the story.
39    pub fn with_chat<T>(mut self, value: T) -> Self
40    where
41        T: Into<Chat>,
42    {
43        self.chat = value.into();
44        self
45    }
46
47    /// Sets a new ID.
48    ///
49    /// # Arguments
50    ///
51    /// `value` - Unique identifier of the story in the chat.
52    pub fn with_id(mut self, value: Integer) -> Self {
53        self.id = value;
54        self
55    }
56}
57
58/// Describes a list of clickable areas on a story media.
59pub struct StoryAreas {
60    items: Vec<StoryArea>,
61}
62
63impl StoryAreas {
64    pub(crate) fn serialize(&self) -> Result<String, StoryAreasError> {
65        serde_json::to_string(&self.items).map_err(StoryAreasError::Serialize)
66    }
67}
68
69impl<T> From<T> for StoryAreas
70where
71    T: IntoIterator<Item = StoryArea>,
72{
73    fn from(value: T) -> Self {
74        Self {
75            items: value.into_iter().collect(),
76        }
77    }
78}
79
80/// Represents a story areas error
81#[derive(Debug)]
82pub enum StoryAreasError {
83    /// Can not serialize to JSON
84    Serialize(JsonError),
85}
86
87impl fmt::Display for StoryAreasError {
88    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
89        match self {
90            Self::Serialize(err) => write!(out, "can not serialize: {err}"),
91        }
92    }
93}
94
95impl Error for StoryAreasError {
96    fn source(&self) -> Option<&(dyn Error + 'static)> {
97        Some(match self {
98            Self::Serialize(err) => err,
99        })
100    }
101}
102
103/// Describes a clickable area on a story media.
104#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
105pub struct StoryArea {
106    /// Type of the area.
107    #[serde(rename = "type")]
108    pub area_type: StoryAreaType,
109    /// Position of the area
110    pub position: StoryAreaPosition,
111}
112
113impl StoryArea {
114    /// Creates a new `StoryArea`.
115    ///
116    /// # Arguments
117    ///
118    /// * `area_type` - Type of the area.
119    /// * `position` - Position of the area.
120    pub fn new<T>(area_type: T, position: StoryAreaPosition) -> Self
121    where
122        T: Into<StoryAreaType>,
123    {
124        Self {
125            area_type: area_type.into(),
126            position,
127        }
128    }
129}
130
131/// Describes the position of a clickable area within a story.
132#[derive(Clone, Debug, Deserialize, derive_more::From, PartialEq, PartialOrd, Serialize)]
133pub struct StoryAreaPosition {
134    /// The radius of the rectangle corner rounding, as a percentage of the media width.
135    pub corner_radius_percentage: Float,
136    /// The height of the area's rectangle, as a percentage of the media height.
137    pub height_percentage: Float,
138    /// The clockwise rotation angle of the rectangle, in degrees; 0-360.
139    pub rotation_angle: Float,
140    /// The width of the area's rectangle, as a percentage of the media width.
141    pub width_percentage: Float,
142    /// The abscissa of the area's center, as a percentage of the media width.
143    pub x_percentage: Float,
144    /// The ordinate of the area's center, as a percentage of the media height.
145    pub y_percentage: Float,
146}
147
148/// Describes the type of a clickable area on a story.
149#[derive(Clone, Debug, Deserialize, derive_more::From, PartialEq, PartialOrd, Serialize)]
150#[serde(tag = "type", rename_all = "snake_case")]
151pub enum StoryAreaType {
152    /// An area pointing to an HTTP or tg:// link.
153    Link(StoryAreaTypeLink),
154    /// An area pointing to a location.
155    Location(StoryAreaTypeLocation),
156    /// An area pointing to a suggested reaction.
157    SuggestedReaction(StoryAreaTypeSuggestedReaction),
158    /// An area pointing to a unique gift.
159    UniqueGift(StoryAreaTypeUniqueGift),
160    /// An area containing weather information.
161    Weather(StoryAreaTypeWeather),
162}
163
164/// Describes a story area pointing to an HTTP or tg:// link.
165///
166/// Currently, a story can have up to 3 link areas.
167#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
168pub struct StoryAreaTypeLink {
169    /// HTTP or tg:// URL to be opened when the area is clicked.
170    pub url: String,
171}
172
173impl StoryAreaTypeLink {
174    /// Creates a new `StoryAreaTypeLink`.
175    ///
176    /// # Arguments
177    ///
178    /// * `url` - HTTP or tg:// URL to be opened when the area is clicked.
179    pub fn new<T>(url: T) -> Self
180    where
181        T: Into<String>,
182    {
183        Self { url: url.into() }
184    }
185}
186
187/// Describes a story area pointing to a location.
188///
189/// Currently, a story can have up to 10 location areas.
190#[serde_with::skip_serializing_none]
191#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
192pub struct StoryAreaTypeLocation {
193    /// Location latitude in degrees.
194    pub latitude: Float,
195    /// Location longitude in degrees.
196    pub longitude: Float,
197    /// Address of the location.
198    pub address: Option<LocationAddress>,
199}
200
201impl StoryAreaTypeLocation {
202    /// Creates a new `StoryAreaTypeLocation`.
203    ///
204    /// # Arguments
205    ///
206    /// * `latitude` - Location latitude in degrees.
207    /// * `longitude` - Location longitude in degrees.
208    pub fn new(latitude: Float, longitude: Float) -> Self {
209        Self {
210            latitude,
211            longitude,
212            address: None,
213        }
214    }
215
216    /// Sets a new address
217    ///
218    /// # Arguments
219    ///
220    /// * `value` - Address of the location.
221    pub fn with_address(mut self, value: LocationAddress) -> Self {
222        self.address = Some(value);
223        self
224    }
225}
226
227/// Describes a story area pointing to a suggested reaction.
228///
229/// Currently, a story can have up to 5 suggested reaction areas.
230#[serde_with::skip_serializing_none]
231#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
232pub struct StoryAreaTypeSuggestedReaction {
233    /// Type of the reaction.
234    pub reaction_type: ReactionType,
235    /// Whether the reaction area has a dark background.
236    pub is_dark: Option<bool>,
237    /// Whether reaction area corner is flipped.
238    pub is_flipped: Option<bool>,
239}
240
241impl StoryAreaTypeSuggestedReaction {
242    /// Creates a new `StoryAreaTypeSuggestedReaction`.
243    ///
244    /// # Arguments
245    ///
246    /// * `reaction_type` - Type of the reaction.
247    pub fn new(reaction_type: ReactionType) -> Self {
248        Self {
249            reaction_type,
250            is_dark: None,
251            is_flipped: None,
252        }
253    }
254
255    /// Sets a new value for the `is_dark` flag.
256    ///
257    /// # Arguments
258    ///
259    /// * `value` - Whether the reaction area has a dark background.
260    pub fn with_is_dark(mut self, value: bool) -> Self {
261        self.is_dark = Some(value);
262        self
263    }
264
265    /// Sets a new value for the `is_flipped` flag.
266    ///
267    /// # Arguments
268    ///
269    /// * `value` - Whether reaction area corner is flipped.
270    pub fn with_is_flipped(mut self, value: bool) -> Self {
271        self.is_flipped = Some(value);
272        self
273    }
274}
275
276/// Describes a story area pointing to a unique gift.
277///
278/// Currently, a story can have at most 1 unique gift area.
279#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
280pub struct StoryAreaTypeUniqueGift {
281    /// Unique name of the gift.
282    pub name: String,
283}
284
285impl StoryAreaTypeUniqueGift {
286    /// Creates a new `StoryAreaTypeUniqueGift`.
287    ///
288    /// # Arguments
289    ///
290    /// * `name` - Unique name of the gift.
291    pub fn new<T>(name: T) -> Self
292    where
293        T: Into<String>,
294    {
295        Self { name: name.into() }
296    }
297}
298
299/// Describes a story area containing weather information.
300///
301/// Currently, a story can have up to 3 weather areas.
302#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
303pub struct StoryAreaTypeWeather {
304    /// A color of the area background in the ARGB format.
305    pub background_color: Integer,
306    /// Emoji representing the weather.
307    pub emoji: String,
308    /// Temperature, in degree Celsius
309    pub temperature: Float,
310}
311
312impl StoryAreaTypeWeather {
313    /// Creates a new `StoryAreaTypeWeather`.
314    ///
315    /// # Arguments
316    ///
317    /// * `background_color` - A color of the area background in the ARGB format.
318    /// * `emoji` - Emoji representing the weather.
319    /// * `temperature` - Temperature, in degree Celsius.
320    pub fn new<T>(background_color: Integer, emoji: T, temperature: Float) -> Self
321    where
322        T: Into<String>,
323    {
324        Self {
325            background_color,
326            emoji: emoji.into(),
327            temperature,
328        }
329    }
330}
331
332/// Reposts a story on behalf of a business account from another business account.
333///
334/// Both business accounts must be managed by the same bot,
335/// and the story on the source account must have been posted (or reposted) by the bot.
336///
337/// Requires the `can_manage_stories` business bot right for both business accounts.
338#[derive(Clone, Debug, Serialize)]
339pub struct RepostStory {
340    active_period: Integer,
341    business_connection_id: String,
342    from_chat_id: Integer,
343    from_story_id: Integer,
344    post_to_chat_page: Option<bool>,
345    protect_content: Option<bool>,
346}
347
348impl RepostStory {
349    /// Creates a new `RepostStory`.
350    ///
351    /// # Arguments
352    ///
353    /// * `active_period` - Period after which the story is moved to the archive, in seconds; must be one of 6 * 3600, 12 * 3600, 86400, or 2 * 86400.
354    /// * `business_connection_id` - Unique identifier of the business connection.
355    /// * `from_chat_id` - Unique identifier of the chat which posted the story that should be reposted.
356    /// * `from_story_id` - Unique identifier of the story that should be reposted.
357    pub fn new<T>(
358        active_period: Integer,
359        business_connection_id: T,
360        from_chat_id: Integer,
361        from_story_id: Integer,
362    ) -> Self
363    where
364        T: Into<String>,
365    {
366        Self {
367            active_period,
368            business_connection_id: business_connection_id.into(),
369            from_chat_id,
370            from_story_id,
371            post_to_chat_page: None,
372            protect_content: None,
373        }
374    }
375    /// Sets a new value for the `post_to_chat_page` flag.
376    ///
377    /// # Arguments
378    ///
379    /// * `value` - Whether to keep the story accessible after it expires.
380    pub fn with_post_to_chat_page(mut self, value: bool) -> Self {
381        self.post_to_chat_page = Some(value);
382        self
383    }
384    /// Sets a new value for the `protect_content` flag.
385    ///
386    /// # Arguments
387    ///
388    /// * `value` - Whether the content of the story must be protected from forwarding and screenshotting.
389    pub fn with_protect_content(mut self, value: bool) -> Self {
390        self.protect_content = Some(value);
391        self
392    }
393}
394
395impl Method for RepostStory {
396    type Response = Story;
397
398    fn into_payload(self) -> Payload {
399        Payload::json("repostStory", self)
400    }
401}