1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone)]
10pub struct Update {
11 pub update_id: i64,
12 pub kind: UpdateKind,
13}
14
15#[derive(Debug, Clone)]
17#[allow(clippy::large_enum_variant)]
18pub enum UpdateKind {
19 Message(Message),
21 EditedMessage(Message),
23 CallbackQuery(CallbackQuery),
25 MessageReaction(MessageReactionUpdated),
28 Unknown,
30}
31
32impl<'de> Deserialize<'de> for Update {
33 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
34 #[derive(Deserialize)]
36 struct RawUpdate {
37 update_id: i64,
38 message: Option<Message>,
39 edited_message: Option<Message>,
40 callback_query: Option<CallbackQuery>,
41 message_reaction: Option<MessageReactionUpdated>,
42 }
43
44 let raw = RawUpdate::deserialize(deserializer)?;
45
46 let kind = if let Some(message) = raw.message {
47 UpdateKind::Message(message)
48 } else if let Some(message) = raw.edited_message {
49 UpdateKind::EditedMessage(message)
50 } else if let Some(callback_query) = raw.callback_query {
51 UpdateKind::CallbackQuery(callback_query)
52 } else if let Some(reaction) = raw.message_reaction {
53 UpdateKind::MessageReaction(reaction)
54 } else {
55 UpdateKind::Unknown
56 };
57
58 Ok(Self {
59 update_id: raw.update_id,
60 kind,
61 })
62 }
63}
64
65#[derive(Debug, Clone, Deserialize)]
67pub struct Message {
68 pub message_id: i64,
69 pub from: Option<User>,
70 pub chat: Chat,
71 pub date: i64,
72 pub edit_date: Option<i64>,
74 pub text: Option<String>,
75 pub caption: Option<String>,
78 pub entities: Option<Vec<MessageEntity>>,
79 pub reply_to_message: Option<Box<Message>>,
80 pub sticker: Option<Sticker>,
82 pub photo: Option<Vec<PhotoSize>>,
84 pub video: Option<MediaFile>,
86 pub audio: Option<MediaFile>,
88 pub voice: Option<MediaFile>,
90 pub document: Option<MediaFile>,
92 pub animation: Option<MediaFile>,
94 pub message_thread_id: Option<i64>,
96}
97
98#[derive(Debug, Clone, Deserialize)]
100pub struct Sticker {
101 pub file_id: String,
103 pub file_unique_id: String,
105 #[serde(rename = "type")]
107 pub sticker_type: String,
108 pub emoji: Option<String>,
110 pub set_name: Option<String>,
112 #[serde(default)]
114 pub is_animated: bool,
115 #[serde(default)]
117 pub is_video: bool,
118}
119
120#[derive(Debug, Clone, Deserialize)]
122pub struct PhotoSize {
123 pub file_id: String,
125 pub file_unique_id: String,
127 pub width: i64,
129 pub height: i64,
131}
132
133#[derive(Debug, Clone, Deserialize)]
135pub struct MediaFile {
136 pub file_id: String,
138 pub file_unique_id: String,
140 pub mime_type: Option<String>,
142 pub file_name: Option<String>,
144}
145
146#[derive(Debug, Clone, Deserialize)]
149pub struct File {
150 pub file_id: String,
152 pub file_unique_id: String,
154 pub file_size: Option<i64>,
156 pub file_path: Option<String>,
159}
160
161#[derive(Debug, Clone, Deserialize)]
163pub struct StickerSet {
164 pub name: String,
166 pub title: String,
168 pub sticker_type: String,
170 pub stickers: Vec<Sticker>,
172}
173
174#[derive(Debug, Clone, Deserialize)]
176pub struct User {
177 pub id: i64,
178 pub is_bot: bool,
179 pub first_name: String,
180 pub last_name: Option<String>,
181 pub username: Option<String>,
182 pub language_code: Option<String>,
183 #[serde(default)]
187 pub can_read_all_group_messages: Option<bool>,
188}
189
190#[derive(Debug, Clone, Deserialize)]
192pub struct Chat {
193 pub id: i64,
194 #[serde(rename = "type")]
195 pub chat_type: ChatType,
196 pub title: Option<String>,
197 pub username: Option<String>,
198 pub first_name: Option<String>,
199 pub last_name: Option<String>,
200}
201
202#[derive(Debug, Clone, Deserialize)]
204#[serde(rename_all = "snake_case")]
205pub enum ChatType {
206 Private,
207 Group,
208 Supergroup,
209 Channel,
210}
211
212#[derive(Debug, Clone, Deserialize)]
214pub struct MessageEntity {
215 #[serde(rename = "type")]
216 pub entity_type: EntityType,
217 pub offset: i64,
218 pub length: i64,
219 #[serde(default)]
221 pub user: Option<User>,
222}
223
224#[derive(Debug, Clone, Deserialize)]
226#[serde(rename_all = "snake_case")]
227pub enum EntityType {
228 Mention,
229 Hashtag,
230 Cashtag,
231 BotCommand,
232 Url,
233 Email,
234 PhoneNumber,
235 Bold,
236 Italic,
237 Underline,
238 Strikethrough,
239 Spoiler,
240 Code,
241 Pre,
242 TextLink,
243 TextMention,
244 CustomEmoji,
245 #[serde(other)]
246 Unknown,
247}
248
249#[derive(Debug, Clone, Deserialize)]
251pub struct CallbackQuery {
252 pub id: String,
253 pub from: User,
254 pub message: Option<Message>,
255 pub inline_message_id: Option<String>,
256 pub chat_instance: String,
257 pub data: Option<String>,
258}
259
260#[derive(Debug, Clone, Deserialize)]
263pub struct MessageReactionUpdated {
264 pub chat: Chat,
265 pub message_id: i64,
267 pub date: Option<i64>,
269 pub user: Option<User>,
271 pub actor_chat: Option<Chat>,
273 pub old_reaction: Vec<ReactionType>,
275 pub new_reaction: Vec<ReactionType>,
277}
278
279#[derive(Debug, Clone, Deserialize)]
282#[serde(tag = "type", rename_all = "snake_case")]
283pub enum ReactionType {
284 Emoji {
286 emoji: String,
288 },
289 CustomEmoji {
291 custom_emoji_id: String,
293 },
294 Paid,
296}
297
298#[derive(Debug, Clone, Serialize)]
300pub struct InlineKeyboardMarkup {
301 pub inline_keyboard: Vec<Vec<InlineKeyboardButton>>,
302}
303
304#[derive(Debug, Clone, Serialize)]
306pub struct InlineKeyboardButton {
307 pub text: String,
308 #[serde(skip_serializing_if = "Option::is_none")]
309 pub url: Option<String>,
310 #[serde(skip_serializing_if = "Option::is_none")]
311 pub callback_data: Option<String>,
312}
313
314impl InlineKeyboardButton {
315 pub fn callback(text: impl Into<String>, data: impl Into<String>) -> Self {
316 Self {
317 text: text.into(),
318 url: None,
319 callback_data: Some(data.into()),
320 }
321 }
322
323 pub fn url(text: impl Into<String>, url: impl Into<String>) -> Self {
324 Self {
325 text: text.into(),
326 url: Some(url.into()),
327 callback_data: None,
328 }
329 }
330}
331
332#[derive(Debug, Clone, Serialize)]
334#[serde(untagged)]
335pub enum ReplyMarkup {
336 InlineKeyboard(InlineKeyboardMarkup),
337}
338
339#[derive(Debug, Clone, Serialize)]
341pub struct BotCommand {
342 pub command: String,
344 pub description: String,
346}
347
348impl BotCommand {
349 pub fn new(command: impl Into<String>, description: impl Into<String>) -> Self {
351 Self {
352 command: command.into(),
353 description: description.into(),
354 }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 fn parse(json: serde_json::Value) -> Update {
363 serde_json::from_value(json).expect("update parses")
364 }
365
366 #[test]
367 fn parses_a_message_update() {
368 let update = parse(serde_json::json!({
369 "update_id": 1,
370 "message": {
371 "message_id": 5,
372 "date": 0,
373 "chat": { "id": 42, "type": "supergroup", "title": "Room" },
374 "text": "hi"
375 }
376 }));
377
378 assert_eq!(update.update_id, 1);
379 let UpdateKind::Message(message) = update.kind else {
380 panic!("expected a message");
381 };
382 assert_eq!(message.message_id, 5);
383 assert_eq!(message.text.as_deref(), Some("hi"));
384 }
385
386 #[test]
387 fn parses_an_edited_message_update() {
388 let update = parse(serde_json::json!({
389 "update_id": 2,
390 "edited_message": {
391 "message_id": 5,
392 "date": 0,
393 "chat": { "id": 42, "type": "private" },
394 "text": "fixed"
395 }
396 }));
397 assert!(matches!(update.kind, UpdateKind::EditedMessage(_)));
398 }
399
400 #[test]
401 fn unmodelled_update_kinds_still_parse() {
402 for kind in ["poll", "my_chat_member", "inline_query", "channel_post"] {
405 let update = parse(serde_json::json!({
406 "update_id": 3,
407 kind: { "id": "whatever", "unexpected": [1, 2, 3] }
408 }));
409 assert!(matches!(update.kind, UpdateKind::Unknown), "{kind}");
410 assert_eq!(update.update_id, 3);
411 }
412 }
413
414 #[test]
415 fn unknown_fields_on_known_kinds_are_ignored() {
416 let update = parse(serde_json::json!({
417 "update_id": 4,
418 "message": {
419 "message_id": 5,
420 "date": 0,
421 "chat": { "id": 42, "type": "private" },
422 "text": "hi",
423 "some_future_field": { "nested": true }
424 }
425 }));
426 assert!(matches!(update.kind, UpdateKind::Message(_)));
427 }
428
429 #[test]
430 fn inline_keyboards_serialize_the_way_telegram_expects() {
431 let markup = ReplyMarkup::InlineKeyboard(InlineKeyboardMarkup {
432 inline_keyboard: vec![vec![
433 InlineKeyboardButton::callback("Yes", "yes"),
434 InlineKeyboardButton::url("Docs", "https://example.com"),
435 ]],
436 });
437
438 let json = serde_json::to_value(&markup).unwrap();
439 assert_eq!(
440 json,
441 serde_json::json!({
442 "inline_keyboard": [[
443 { "text": "Yes", "callback_data": "yes" },
444 { "text": "Docs", "url": "https://example.com" }
445 ]]
446 })
447 );
448 }
449}