use serde::Deserialize;
pub fn parse_update(body: &[u8]) -> Result<Update, UpdateError> {
let raw: RawUpdate =
serde_json::from_slice(body).map_err(|error| UpdateError::Malformed(error.to_string()))?;
let kind = if let Some(callback) = raw.callback_query {
UpdateKind::CallbackQuery(build_callback(callback))
} else if let Some(post) = raw.channel_post {
UpdateKind::ChannelPost(ChannelPost {
message: build_message(post),
edited: false,
})
} else if let Some(post) = raw.edited_channel_post {
UpdateKind::ChannelPost(ChannelPost {
message: build_message(post),
edited: true,
})
} else if let Some(message) = raw.message {
as_command_or_message(message)
} else {
UpdateKind::Other
};
Ok(Update {
update_id: raw.update_id,
kind,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum UpdateError {
Malformed(String),
}
impl std::fmt::Display for UpdateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Malformed(detail) => write!(f, "malformed telegram update body: {detail}"),
}
}
}
impl std::error::Error for UpdateError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Update {
pub update_id: i64,
pub kind: UpdateKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpdateKind {
Message(Message),
Command(Command),
CallbackQuery(CallbackQuery),
ChannelPost(ChannelPost),
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct User {
pub id: i64,
pub is_bot: bool,
pub username: Option<String>,
pub first_name: String,
pub language_code: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChatKind {
Private,
Group,
Supergroup,
Channel,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Chat {
pub id: i64,
pub kind: ChatKind,
pub title: Option<String>,
pub username: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
pub message_id: i64,
pub chat: Chat,
pub from: Option<User>,
pub date: i64,
pub text: Option<String>,
pub links: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Command {
pub message: Message,
pub name: String,
pub bot: Option<String>,
pub args: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CallbackQuery {
pub id: String,
pub from: User,
pub chat_id: Option<i64>,
pub message_id: Option<i64>,
pub data: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChannelPost {
pub message: Message,
pub edited: bool,
}
fn as_command_or_message(raw: RawMessage) -> UpdateKind {
match command_parts(&raw) {
Some((name, bot, args)) => UpdateKind::Command(Command {
message: build_message(raw),
name,
bot,
args,
}),
None => UpdateKind::Message(build_message(raw)),
}
}
fn command_parts(raw: &RawMessage) -> Option<(String, Option<String>, String)> {
let text = raw.text.as_deref()?;
let entity = raw
.entities
.iter()
.find(|entity| entity.entity_type == "bot_command" && entity.offset == 0)?;
let length = usize::try_from(entity.length).ok()?;
let units: Vec<u16> = text.encode_utf16().collect();
if length == 0 || length > units.len() || units[0] != u16::from(b'/') {
return None;
}
let word = String::from_utf16_lossy(&units[1..length]);
let (name, bot) = match word.split_once('@') {
Some((name, bot)) if !name.is_empty() => (name, Some(bot.to_owned())),
Some(_) => return None,
None => (word.as_str(), None),
};
let args = String::from_utf16_lossy(&units[length..]).trim().to_owned();
Some((name.to_ascii_lowercase(), bot, args))
}
fn build_message(raw: RawMessage) -> Message {
let mut links = Vec::new();
collect_links(raw.text.as_deref(), &raw.entities, &mut links);
collect_links(raw.caption.as_deref(), &raw.caption_entities, &mut links);
Message {
message_id: raw.message_id,
chat: build_chat(raw.chat),
from: raw.from.map(build_user),
date: raw.date,
text: raw.text.or(raw.caption),
links,
}
}
fn build_chat(raw: RawChat) -> Chat {
Chat {
id: raw.id,
kind: match raw.chat_type.as_deref() {
Some("private") => ChatKind::Private,
Some("group") => ChatKind::Group,
Some("supergroup") => ChatKind::Supergroup,
Some("channel") => ChatKind::Channel,
_ => ChatKind::Other,
},
title: raw.title,
username: raw.username,
}
}
fn build_user(raw: RawUser) -> User {
User {
id: raw.id,
is_bot: raw.is_bot,
username: raw.username,
first_name: raw.first_name,
language_code: raw.language_code,
}
}
fn build_callback(raw: RawCallbackQuery) -> CallbackQuery {
let (chat_id, message_id) = raw.message.map_or((None, None), |message| {
(Some(message.chat.id), Some(message.message_id))
});
CallbackQuery {
id: raw.id,
from: build_user(raw.from),
chat_id,
message_id,
data: raw.data,
}
}
fn collect_links(source: Option<&str>, entities: &[RawEntity], links: &mut Vec<String>) {
let Some(text) = source else {
return;
};
let units: Vec<u16> = text.encode_utf16().collect();
for entity in entities {
match entity.entity_type.as_str() {
"url" => {
let (Ok(start), Ok(length)) = (
usize::try_from(entity.offset),
usize::try_from(entity.length),
) else {
continue;
};
let Some(end) = start.checked_add(length).filter(|&end| end <= units.len()) else {
continue;
};
links.push(String::from_utf16_lossy(&units[start..end]));
}
"text_link" => {
if let Some(url) = &entity.url {
links.push(url.clone());
}
}
_ => {}
}
}
}
#[derive(Deserialize)]
struct RawUpdate {
update_id: i64,
#[serde(default)]
message: Option<RawMessage>,
#[serde(default)]
channel_post: Option<RawMessage>,
#[serde(default)]
edited_channel_post: Option<RawMessage>,
#[serde(default)]
callback_query: Option<RawCallbackQuery>,
}
#[derive(Deserialize)]
struct RawMessage {
message_id: i64,
chat: RawChat,
#[serde(default)]
from: Option<RawUser>,
#[serde(default)]
date: i64,
#[serde(default)]
text: Option<String>,
#[serde(default)]
caption: Option<String>,
#[serde(default)]
entities: Vec<RawEntity>,
#[serde(default)]
caption_entities: Vec<RawEntity>,
}
#[derive(Deserialize)]
struct RawChat {
id: i64,
#[serde(rename = "type")]
chat_type: Option<String>,
#[serde(default)]
title: Option<String>,
#[serde(default)]
username: Option<String>,
}
#[derive(Deserialize)]
struct RawUser {
id: i64,
#[serde(default)]
is_bot: bool,
#[serde(default)]
username: Option<String>,
#[serde(default)]
first_name: String,
#[serde(default)]
language_code: Option<String>,
}
#[derive(Deserialize)]
struct RawEntity {
#[serde(rename = "type")]
entity_type: String,
offset: i64,
length: i64,
#[serde(default)]
url: Option<String>,
}
#[derive(Deserialize)]
struct RawCallbackQuery {
id: String,
from: RawUser,
#[serde(default)]
message: Option<RawMessage>,
#[serde(default)]
data: Option<String>,
}