1use serde::Deserialize;
14
15pub fn parse_update(body: &[u8]) -> Result<Update, UpdateError> {
24 let raw: RawUpdate =
25 serde_json::from_slice(body).map_err(|error| UpdateError::Malformed(error.to_string()))?;
26 let kind = if let Some(callback) = raw.callback_query {
27 UpdateKind::CallbackQuery(build_callback(callback))
28 } else if let Some(post) = raw.channel_post {
29 UpdateKind::ChannelPost(ChannelPost {
30 message: build_message(post),
31 edited: false,
32 })
33 } else if let Some(post) = raw.edited_channel_post {
34 UpdateKind::ChannelPost(ChannelPost {
35 message: build_message(post),
36 edited: true,
37 })
38 } else if let Some(message) = raw.message {
39 as_command_or_message(message)
40 } else {
41 UpdateKind::Other
43 };
44 Ok(Update {
45 update_id: raw.update_id,
46 kind,
47 })
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52#[non_exhaustive]
53pub enum UpdateError {
54 Malformed(String),
57}
58
59impl std::fmt::Display for UpdateError {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Self::Malformed(detail) => write!(f, "malformed telegram update body: {detail}"),
63 }
64 }
65}
66
67impl std::error::Error for UpdateError {}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct Update {
73 pub update_id: i64,
77 pub kind: UpdateKind,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum UpdateKind {
84 Message(Message),
86 Command(Command),
88 CallbackQuery(CallbackQuery),
90 ChannelPost(ChannelPost),
92 Other,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct User {
100 pub id: i64,
102 pub is_bot: bool,
104 pub username: Option<String>,
106 pub first_name: String,
108 pub language_code: Option<String>,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum ChatKind {
115 Private,
117 Group,
119 Supergroup,
121 Channel,
123 Other,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct Chat {
130 pub id: i64,
132 pub kind: ChatKind,
134 pub title: Option<String>,
136 pub username: Option<String>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct Message {
145 pub message_id: i64,
148 pub chat: Chat,
150 pub from: Option<User>,
153 pub date: i64,
155 pub text: Option<String>,
157 pub links: Vec<String>,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct Command {
166 pub message: Message,
168 pub name: String,
171 pub bot: Option<String>,
174 pub args: String,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct CallbackQuery {
183 pub id: String,
185 pub from: User,
187 pub chat_id: Option<i64>,
190 pub message_id: Option<i64>,
192 pub data: Option<String>,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct ChannelPost {
199 pub message: Message,
201 pub edited: bool,
204}
205
206fn as_command_or_message(raw: RawMessage) -> UpdateKind {
209 match command_parts(&raw) {
210 Some((name, bot, args)) => UpdateKind::Command(Command {
211 message: build_message(raw),
212 name,
213 bot,
214 args,
215 }),
216 None => UpdateKind::Message(build_message(raw)),
217 }
218}
219
220fn command_parts(raw: &RawMessage) -> Option<(String, Option<String>, String)> {
223 let text = raw.text.as_deref()?;
225 let entity = raw
226 .entities
227 .iter()
228 .find(|entity| entity.entity_type == "bot_command" && entity.offset == 0)?;
229 let length = usize::try_from(entity.length).ok()?;
230 let units: Vec<u16> = text.encode_utf16().collect();
231 if length == 0 || length > units.len() || units[0] != u16::from(b'/') {
232 return None;
233 }
234 let word = String::from_utf16_lossy(&units[1..length]);
235 let (name, bot) = match word.split_once('@') {
236 Some((name, bot)) if !name.is_empty() => (name, Some(bot.to_owned())),
237 Some(_) => return None,
238 None => (word.as_str(), None),
239 };
240 let args = String::from_utf16_lossy(&units[length..]).trim().to_owned();
241 Some((name.to_ascii_lowercase(), bot, args))
242}
243
244fn build_message(raw: RawMessage) -> Message {
245 let mut links = Vec::new();
247 collect_links(raw.text.as_deref(), &raw.entities, &mut links);
248 collect_links(raw.caption.as_deref(), &raw.caption_entities, &mut links);
249 Message {
250 message_id: raw.message_id,
251 chat: build_chat(raw.chat),
252 from: raw.from.map(build_user),
253 date: raw.date,
254 text: raw.text.or(raw.caption),
257 links,
258 }
259}
260
261fn build_chat(raw: RawChat) -> Chat {
262 Chat {
263 id: raw.id,
264 kind: match raw.chat_type.as_deref() {
265 Some("private") => ChatKind::Private,
266 Some("group") => ChatKind::Group,
267 Some("supergroup") => ChatKind::Supergroup,
268 Some("channel") => ChatKind::Channel,
269 _ => ChatKind::Other,
270 },
271 title: raw.title,
272 username: raw.username,
273 }
274}
275
276fn build_user(raw: RawUser) -> User {
277 User {
278 id: raw.id,
279 is_bot: raw.is_bot,
280 username: raw.username,
281 first_name: raw.first_name,
282 language_code: raw.language_code,
283 }
284}
285
286fn build_callback(raw: RawCallbackQuery) -> CallbackQuery {
287 let (chat_id, message_id) = raw.message.map_or((None, None), |message| {
288 (Some(message.chat.id), Some(message.message_id))
289 });
290 CallbackQuery {
291 id: raw.id,
292 from: build_user(raw.from),
293 chat_id,
294 message_id,
295 data: raw.data,
296 }
297}
298
299fn collect_links(source: Option<&str>, entities: &[RawEntity], links: &mut Vec<String>) {
305 let Some(text) = source else {
306 return;
307 };
308 let units: Vec<u16> = text.encode_utf16().collect();
309 for entity in entities {
310 match entity.entity_type.as_str() {
311 "url" => {
312 let (Ok(start), Ok(length)) = (
313 usize::try_from(entity.offset),
314 usize::try_from(entity.length),
315 ) else {
316 continue;
317 };
318 let Some(end) = start.checked_add(length).filter(|&end| end <= units.len()) else {
319 continue;
320 };
321 links.push(String::from_utf16_lossy(&units[start..end]));
322 }
323 "text_link" => {
324 if let Some(url) = &entity.url {
325 links.push(url.clone());
326 }
327 }
328 _ => {}
329 }
330 }
331}
332
333#[derive(Deserialize)]
337struct RawUpdate {
338 update_id: i64,
339 #[serde(default)]
340 message: Option<RawMessage>,
341 #[serde(default)]
345 channel_post: Option<RawMessage>,
346 #[serde(default)]
347 edited_channel_post: Option<RawMessage>,
348 #[serde(default)]
349 callback_query: Option<RawCallbackQuery>,
350}
351
352#[derive(Deserialize)]
353struct RawMessage {
354 message_id: i64,
355 chat: RawChat,
356 #[serde(default)]
357 from: Option<RawUser>,
358 #[serde(default)]
359 date: i64,
360 #[serde(default)]
361 text: Option<String>,
362 #[serde(default)]
363 caption: Option<String>,
364 #[serde(default)]
365 entities: Vec<RawEntity>,
366 #[serde(default)]
367 caption_entities: Vec<RawEntity>,
368}
369
370#[derive(Deserialize)]
371struct RawChat {
372 id: i64,
373 #[serde(rename = "type")]
374 chat_type: Option<String>,
375 #[serde(default)]
376 title: Option<String>,
377 #[serde(default)]
378 username: Option<String>,
379}
380
381#[derive(Deserialize)]
382struct RawUser {
383 id: i64,
384 #[serde(default)]
385 is_bot: bool,
386 #[serde(default)]
387 username: Option<String>,
388 #[serde(default)]
389 first_name: String,
390 #[serde(default)]
391 language_code: Option<String>,
392}
393
394#[derive(Deserialize)]
395struct RawEntity {
396 #[serde(rename = "type")]
397 entity_type: String,
398 offset: i64,
399 length: i64,
400 #[serde(default)]
401 url: Option<String>,
402}
403
404#[derive(Deserialize)]
405struct RawCallbackQuery {
406 id: String,
407 from: RawUser,
408 #[serde(default)]
409 message: Option<RawMessage>,
410 #[serde(default)]
411 data: Option<String>,
412}