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}