use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use crate::types::bot::User;
use crate::types::chat::{ChatInviteLink, ChatMember};
use crate::types::common::MessageId;
use crate::types::message::{
Chat, Location, MaybeInaccessibleMessage, Message, OrderInfo, Poll, ShippingAddress,
};
use crate::types::telegram::{
InlineQueryResult, InlineQueryResultsButton, ReactionType, WebAppData,
};
use crate::{Error, Result};
const MAX_GET_UPDATES_LIMIT: u8 = 100;
const MAX_CALLBACK_ANSWER_TEXT_CHARS: usize = 200;
const MAX_INLINE_QUERY_RESULTS: usize = 50;
const MAX_INLINE_NEXT_OFFSET_BYTES: usize = 64;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[non_exhaustive]
pub enum UpdateKind {
Message,
EditedMessage,
ChannelPost,
EditedChannelPost,
BusinessConnection,
BusinessMessage,
EditedBusinessMessage,
DeletedBusinessMessages,
GuestMessage,
MessageReaction,
MessageReactionCount,
CallbackQuery,
InlineQuery,
ChosenInlineResult,
ShippingQuery,
PreCheckoutQuery,
PurchasedPaidMedia,
Poll,
PollAnswer,
MyChatMember,
ChatMember,
ChatJoinRequest,
ChatBoost,
RemovedChatBoost,
ManagedBot,
Unknown,
}
const KNOWN_UPDATE_KINDS: [UpdateKind; 25] = [
UpdateKind::Message,
UpdateKind::EditedMessage,
UpdateKind::ChannelPost,
UpdateKind::EditedChannelPost,
UpdateKind::BusinessConnection,
UpdateKind::BusinessMessage,
UpdateKind::EditedBusinessMessage,
UpdateKind::DeletedBusinessMessages,
UpdateKind::GuestMessage,
UpdateKind::MessageReaction,
UpdateKind::MessageReactionCount,
UpdateKind::CallbackQuery,
UpdateKind::InlineQuery,
UpdateKind::ChosenInlineResult,
UpdateKind::ShippingQuery,
UpdateKind::PreCheckoutQuery,
UpdateKind::PurchasedPaidMedia,
UpdateKind::Poll,
UpdateKind::PollAnswer,
UpdateKind::MyChatMember,
UpdateKind::ChatMember,
UpdateKind::ChatJoinRequest,
UpdateKind::ChatBoost,
UpdateKind::RemovedChatBoost,
UpdateKind::ManagedBot,
];
impl UpdateKind {
pub const fn all_known() -> &'static [Self] {
&KNOWN_UPDATE_KINDS
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Message => "message",
Self::EditedMessage => "edited_message",
Self::ChannelPost => "channel_post",
Self::EditedChannelPost => "edited_channel_post",
Self::BusinessConnection => "business_connection",
Self::BusinessMessage => "business_message",
Self::EditedBusinessMessage => "edited_business_message",
Self::DeletedBusinessMessages => "deleted_business_messages",
Self::GuestMessage => "guest_message",
Self::MessageReaction => "message_reaction",
Self::MessageReactionCount => "message_reaction_count",
Self::CallbackQuery => "callback_query",
Self::InlineQuery => "inline_query",
Self::ChosenInlineResult => "chosen_inline_result",
Self::ShippingQuery => "shipping_query",
Self::PreCheckoutQuery => "pre_checkout_query",
Self::PurchasedPaidMedia => "purchased_paid_media",
Self::Poll => "poll",
Self::PollAnswer => "poll_answer",
Self::MyChatMember => "my_chat_member",
Self::ChatMember => "chat_member",
Self::ChatJoinRequest => "chat_join_request",
Self::ChatBoost => "chat_boost",
Self::RemovedChatBoost => "removed_chat_boost",
Self::ManagedBot => "managed_bot",
Self::Unknown => "unknown",
}
}
pub const fn allowed_update_name(self) -> Option<&'static str> {
match self {
Self::Unknown => None,
known => Some(known.as_str()),
}
}
pub fn from_name(value: &str) -> Option<Self> {
match value {
"message" => Some(Self::Message),
"edited_message" => Some(Self::EditedMessage),
"channel_post" => Some(Self::ChannelPost),
"edited_channel_post" => Some(Self::EditedChannelPost),
"business_connection" => Some(Self::BusinessConnection),
"business_message" => Some(Self::BusinessMessage),
"edited_business_message" => Some(Self::EditedBusinessMessage),
"deleted_business_messages" => Some(Self::DeletedBusinessMessages),
"guest_message" => Some(Self::GuestMessage),
"message_reaction" => Some(Self::MessageReaction),
"message_reaction_count" => Some(Self::MessageReactionCount),
"callback_query" => Some(Self::CallbackQuery),
"inline_query" => Some(Self::InlineQuery),
"chosen_inline_result" => Some(Self::ChosenInlineResult),
"shipping_query" => Some(Self::ShippingQuery),
"pre_checkout_query" => Some(Self::PreCheckoutQuery),
"purchased_paid_media" => Some(Self::PurchasedPaidMedia),
"poll" => Some(Self::Poll),
"poll_answer" => Some(Self::PollAnswer),
"my_chat_member" => Some(Self::MyChatMember),
"chat_member" => Some(Self::ChatMember),
"chat_join_request" => Some(Self::ChatJoinRequest),
"chat_boost" => Some(Self::ChatBoost),
"removed_chat_boost" => Some(Self::RemovedChatBoost),
"managed_bot" => Some(Self::ManagedBot),
"unknown" => Some(Self::Unknown),
_ => None,
}
}
}
impl fmt::Display for UpdateKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl std::str::FromStr for UpdateKind {
type Err = Error;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
Self::from_name(value)
.ok_or_else(|| invalid_request(format!("unknown update kind `{value}`")))
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
#[serde(transparent)]
pub struct AllowedUpdate(String);
impl AllowedUpdate {
pub fn new(value: impl Into<String>) -> Result<Self> {
let value = value.into();
validate_allowed_update_name(&value)?;
Ok(Self(value))
}
pub fn from_kind(kind: UpdateKind) -> Result<Self> {
let name = kind.allowed_update_name().ok_or_else(|| {
invalid_request("UpdateKind::Unknown cannot be used as allowed_updates")
})?;
Ok(Self(name.to_owned()))
}
pub fn from_kinds(kinds: impl IntoIterator<Item = UpdateKind>) -> Result<Vec<Self>> {
kinds.into_iter().map(Self::from_kind).collect()
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl AsRef<str> for AllowedUpdate {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for AllowedUpdate {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl std::str::FromStr for AllowedUpdate {
type Err = Error;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
Self::new(value)
}
}
impl<'de> Deserialize<'de> for AllowedUpdate {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(serde::de::Error::custom)
}
}
impl TryFrom<String> for AllowedUpdate {
type Error = Error;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<&str> for AllowedUpdate {
type Error = Error;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<UpdateKind> for AllowedUpdate {
type Error = Error;
fn try_from(kind: UpdateKind) -> std::result::Result<Self, Self::Error> {
Self::from_kind(kind)
}
}
impl From<AllowedUpdate> for String {
fn from(value: AllowedUpdate) -> Self {
value.into_inner()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct CallbackQuery {
pub id: String,
pub from: User,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<Box<MaybeInaccessibleMessage>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inline_message_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chat_instance: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct InlineQuery {
pub id: String,
pub from: User,
pub query: String,
pub offset: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chat_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location: Option<Location>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ChosenInlineResult {
pub result_id: String,
pub from: User,
pub query: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inline_message_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location: Option<Location>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct PollAnswer {
pub poll_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub voter_chat: Option<Chat>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<User>,
pub option_ids: Vec<u8>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ShippingQuery {
pub id: String,
pub from: User,
pub invoice_payload: String,
pub shipping_address: ShippingAddress,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct PreCheckoutQuery {
pub id: String,
pub from: User,
pub currency: String,
pub total_amount: i64,
pub invoice_payload: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shipping_option_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub order_info: Option<OrderInfo>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct PaidMediaPurchased {
pub from: User,
pub paid_media_payload: String,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[non_exhaustive]
pub struct BusinessBotRights {
#[serde(default, skip_serializing_if = "is_false")]
pub can_reply: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_read_messages: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_delete_sent_messages: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_delete_all_messages: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_edit_name: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_edit_bio: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_edit_profile_photo: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_edit_username: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_change_gift_settings: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_view_gifts_and_stars: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_convert_gifts_to_stars: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_transfer_and_upgrade_gifts: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_transfer_stars: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub can_manage_stories: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct BusinessConnection {
pub id: String,
pub user: User,
pub user_chat_id: i64,
pub date: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rights: Option<BusinessBotRights>,
pub is_enabled: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl BusinessConnection {
pub fn user_id(&self) -> i64 {
self.user.id.0
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ReactionCount {
#[serde(rename = "type")]
pub kind: ReactionType,
pub total_count: i64,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct MessageReactionUpdated {
pub chat: Chat,
pub message_id: MessageId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<User>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actor_chat: Option<Chat>,
pub date: i64,
pub old_reaction: Vec<ReactionType>,
pub new_reaction: Vec<ReactionType>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct MessageReactionCountUpdated {
pub chat: Chat,
pub message_id: MessageId,
pub date: i64,
pub reactions: Vec<ReactionCount>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ChatBoostSource {
pub source: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<User>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub giveaway_message_id: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prize_star_count: Option<i64>,
#[serde(default, skip_serializing_if = "is_false")]
pub is_unclaimed: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ChatBoost {
pub boost_id: String,
pub add_date: i64,
pub expiration_date: i64,
pub source: ChatBoostSource,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ChatBoostUpdated {
pub chat: Chat,
pub boost: ChatBoost,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ChatBoostRemoved {
pub chat: Chat,
pub boost_id: String,
pub remove_date: i64,
pub source: ChatBoostSource,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ManagedBotUpdated {
pub user: User,
pub bot: User,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ChatJoinRequest {
pub chat: Chat,
pub from: User,
pub user_chat_id: i64,
pub date: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bio: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invite_link: Option<ChatInviteLink>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl ChatJoinRequest {
pub fn chat_id(&self) -> i64 {
self.chat.id
}
pub fn user_id(&self) -> i64 {
self.from.id.0
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct BusinessMessagesDeleted {
pub business_connection_id: String,
pub chat: Chat,
pub message_ids: Vec<MessageId>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl BusinessMessagesDeleted {
pub fn chat_id(&self) -> i64 {
self.chat.id
}
pub fn message_ids(&self) -> &[MessageId] {
self.message_ids.as_slice()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ChatMemberUpdated {
pub chat: Chat,
pub from: User,
pub date: i64,
pub old_chat_member: ChatMember,
pub new_chat_member: ChatMember,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invite_link: Option<ChatInviteLink>,
#[serde(default, skip_serializing_if = "is_false")]
pub via_join_request: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub via_chat_folder_invite_link: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl ChatMemberUpdated {
pub fn chat_id(&self) -> i64 {
self.chat.id
}
pub fn actor(&self) -> &User {
&self.from
}
pub fn actor_id(&self) -> i64 {
self.from.id.0
}
pub fn member(&self) -> &ChatMember {
&self.new_chat_member
}
pub fn subject(&self) -> &User {
self.new_chat_member.user()
}
pub fn subject_id(&self) -> i64 {
self.subject().id.0
}
pub fn member_user(&self) -> &User {
self.subject()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct Update {
pub update_id: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<Box<Message>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edited_message: Option<Box<Message>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel_post: Option<Box<Message>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edited_channel_post: Option<Box<Message>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub business_connection: Option<BusinessConnection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub business_message: Option<Box<Message>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edited_business_message: Option<Box<Message>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deleted_business_messages: Option<BusinessMessagesDeleted>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guest_message: Option<Box<Message>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message_reaction: Option<MessageReactionUpdated>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message_reaction_count: Option<MessageReactionCountUpdated>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub callback_query: Option<CallbackQuery>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inline_query: Option<InlineQuery>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chosen_inline_result: Option<ChosenInlineResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shipping_query: Option<ShippingQuery>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pre_checkout_query: Option<PreCheckoutQuery>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub purchased_paid_media: Option<PaidMediaPurchased>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub poll: Option<Box<Poll>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub poll_answer: Option<PollAnswer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub my_chat_member: Option<ChatMemberUpdated>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chat_member: Option<ChatMemberUpdated>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chat_join_request: Option<ChatJoinRequest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chat_boost: Option<ChatBoostUpdated>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub removed_chat_boost: Option<ChatBoostRemoved>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub managed_bot: Option<ManagedBotUpdated>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl Update {
fn has_modeled_kind(&self) -> bool {
self.message.is_some()
|| self.edited_message.is_some()
|| self.channel_post.is_some()
|| self.edited_channel_post.is_some()
|| self.business_connection.is_some()
|| self.business_message.is_some()
|| self.edited_business_message.is_some()
|| self.deleted_business_messages.is_some()
|| self.guest_message.is_some()
|| self.message_reaction.is_some()
|| self.message_reaction_count.is_some()
|| self.callback_query.is_some()
|| self.inline_query.is_some()
|| self.chosen_inline_result.is_some()
|| self.shipping_query.is_some()
|| self.pre_checkout_query.is_some()
|| self.purchased_paid_media.is_some()
|| self.poll.is_some()
|| self.poll_answer.is_some()
|| self.my_chat_member.is_some()
|| self.chat_member.is_some()
|| self.chat_join_request.is_some()
|| self.chat_boost.is_some()
|| self.removed_chat_boost.is_some()
|| self.managed_bot.is_some()
}
fn has_unmodeled_kind(&self) -> bool {
!self.extra.is_empty()
}
pub fn kind(&self) -> UpdateKind {
for kind in KNOWN_UPDATE_KINDS {
if self.has_kind(kind) {
return kind;
}
}
UpdateKind::Unknown
}
pub fn kinds(&self) -> Vec<UpdateKind> {
let mut kinds = Vec::with_capacity(KNOWN_UPDATE_KINDS.len() + 1);
for kind in KNOWN_UPDATE_KINDS {
if self.has_kind(kind) {
kinds.push(kind);
}
}
if self.has_kind(UpdateKind::Unknown) {
kinds.push(UpdateKind::Unknown);
}
kinds
}
pub fn has_kind(&self, kind: UpdateKind) -> bool {
match kind {
UpdateKind::Message => self.message.is_some(),
UpdateKind::EditedMessage => self.edited_message.is_some(),
UpdateKind::ChannelPost => self.channel_post.is_some(),
UpdateKind::EditedChannelPost => self.edited_channel_post.is_some(),
UpdateKind::BusinessConnection => self.business_connection.is_some(),
UpdateKind::BusinessMessage => self.business_message.is_some(),
UpdateKind::EditedBusinessMessage => self.edited_business_message.is_some(),
UpdateKind::DeletedBusinessMessages => self.deleted_business_messages.is_some(),
UpdateKind::GuestMessage => self.guest_message.is_some(),
UpdateKind::MessageReaction => self.message_reaction.is_some(),
UpdateKind::MessageReactionCount => self.message_reaction_count.is_some(),
UpdateKind::CallbackQuery => self.callback_query.is_some(),
UpdateKind::InlineQuery => self.inline_query.is_some(),
UpdateKind::ChosenInlineResult => self.chosen_inline_result.is_some(),
UpdateKind::ShippingQuery => self.shipping_query.is_some(),
UpdateKind::PreCheckoutQuery => self.pre_checkout_query.is_some(),
UpdateKind::PurchasedPaidMedia => self.purchased_paid_media.is_some(),
UpdateKind::Poll => self.poll.is_some(),
UpdateKind::PollAnswer => self.poll_answer.is_some(),
UpdateKind::MyChatMember => self.my_chat_member.is_some(),
UpdateKind::ChatMember => self.chat_member.is_some(),
UpdateKind::ChatJoinRequest => self.chat_join_request.is_some(),
UpdateKind::ChatBoost => self.chat_boost.is_some(),
UpdateKind::RemovedChatBoost => self.removed_chat_boost.is_some(),
UpdateKind::ManagedBot => self.managed_bot.is_some(),
UpdateKind::Unknown => self.has_unmodeled_kind() || !self.has_modeled_kind(),
}
}
pub fn web_app_data(&self) -> Option<&WebAppData> {
if let Some(message) = self.message.as_deref() {
return message.web_app_data();
}
if let Some(message) = self.edited_message.as_deref() {
return message.web_app_data();
}
if let Some(message) = self.channel_post.as_deref() {
return message.web_app_data();
}
if let Some(message) = self.edited_channel_post.as_deref() {
return message.web_app_data();
}
if let Some(message) = self.business_message.as_deref() {
return message.web_app_data();
}
if let Some(message) = self.edited_business_message.as_deref() {
return message.web_app_data();
}
if let Some(message) = self.guest_message.as_deref() {
return message.web_app_data();
}
self.callback_query
.as_ref()
.and_then(|query| query.message.as_deref())
.and_then(|message| message.accessible())
.and_then(|message| message.web_app_data())
}
pub fn chat_join_request(&self) -> Option<&ChatJoinRequest> {
self.chat_join_request.as_ref()
}
pub fn business_connection(&self) -> Option<&BusinessConnection> {
self.business_connection.as_ref()
}
pub fn deleted_business_messages(&self) -> Option<&BusinessMessagesDeleted> {
self.deleted_business_messages.as_ref()
}
pub fn message_reaction(&self) -> Option<&MessageReactionUpdated> {
self.message_reaction.as_ref()
}
pub fn message_reaction_count(&self) -> Option<&MessageReactionCountUpdated> {
self.message_reaction_count.as_ref()
}
pub fn shipping_query(&self) -> Option<&ShippingQuery> {
self.shipping_query.as_ref()
}
pub fn pre_checkout_query(&self) -> Option<&PreCheckoutQuery> {
self.pre_checkout_query.as_ref()
}
pub fn purchased_paid_media(&self) -> Option<&PaidMediaPurchased> {
self.purchased_paid_media.as_ref()
}
pub fn my_chat_member(&self) -> Option<&ChatMemberUpdated> {
self.my_chat_member.as_ref()
}
pub fn chat_member(&self) -> Option<&ChatMemberUpdated> {
self.chat_member.as_ref()
}
pub fn chat_boost(&self) -> Option<&ChatBoostUpdated> {
self.chat_boost.as_ref()
}
pub fn removed_chat_boost(&self) -> Option<&ChatBoostRemoved> {
self.removed_chat_boost.as_ref()
}
pub fn managed_bot(&self) -> Option<&ManagedBotUpdated> {
self.managed_bot.as_ref()
}
}
const fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct GetUpdatesRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub offset: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_updates: Option<Vec<AllowedUpdate>>,
}
impl GetUpdatesRequest {
pub fn with_timeout(timeout_seconds: u16) -> Self {
Self {
timeout: Some(timeout_seconds),
..Self::default()
}
}
pub fn allowed_updates(
mut self,
allowed_updates: impl IntoIterator<Item = AllowedUpdate>,
) -> Self {
self.set_allowed_updates(allowed_updates);
self
}
pub fn allowed_update_kinds(
mut self,
kinds: impl IntoIterator<Item = UpdateKind>,
) -> Result<Self> {
self.set_allowed_update_kinds(kinds)?;
Ok(self)
}
pub fn set_allowed_updates(
&mut self,
allowed_updates: impl IntoIterator<Item = AllowedUpdate>,
) -> &mut Self {
self.allowed_updates = Some(allowed_updates.into_iter().collect());
self
}
pub fn set_allowed_update_kinds(
&mut self,
kinds: impl IntoIterator<Item = UpdateKind>,
) -> Result<&mut Self> {
self.allowed_updates = Some(AllowedUpdate::from_kinds(kinds)?);
Ok(self)
}
pub fn clear_allowed_updates(&mut self) -> &mut Self {
self.allowed_updates = None;
self
}
pub fn validate(&self) -> Result<()> {
if self
.limit
.is_some_and(|limit| limit == 0 || limit > MAX_GET_UPDATES_LIMIT)
{
return Err(invalid_request(format!(
"getUpdates limit must be 1-{MAX_GET_UPDATES_LIMIT}"
)));
}
if let Some(allowed_updates) = self.allowed_updates.as_ref() {
validate_allowed_updates(allowed_updates)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Serialize)]
pub struct AnswerCallbackQueryRequest {
pub callback_query_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_alert: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_time: Option<u32>,
}
impl AnswerCallbackQueryRequest {
pub fn validate(&self) -> Result<()> {
validate_non_empty_id("callback_query_id", &self.callback_query_id)?;
if let Some(text) = self.text.as_deref() {
validate_optional_text_limit(
"callback answer text",
text,
MAX_CALLBACK_ANSWER_TEXT_CHARS,
)?;
}
if let Some(url) = self.url.as_deref() {
validate_http_url("callback answer url", url)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Serialize)]
pub struct AnswerInlineQueryRequest {
pub inline_query_id: String,
pub results: Vec<InlineQueryResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_time: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_personal: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_offset: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub button: Option<InlineQueryResultsButton>,
}
impl AnswerInlineQueryRequest {
pub fn new(
inline_query_id: impl Into<String>,
results: impl IntoIterator<Item = InlineQueryResult>,
) -> Self {
Self {
inline_query_id: inline_query_id.into(),
results: results.into_iter().collect(),
cache_time: None,
is_personal: None,
next_offset: None,
button: None,
}
}
pub fn add_result(mut self, result: impl Into<InlineQueryResult>) -> Self {
self.results.push(result.into());
self
}
pub fn cache_time(mut self, cache_time: u32) -> Self {
self.cache_time = Some(cache_time);
self
}
pub fn is_personal(mut self, is_personal: bool) -> Self {
self.is_personal = Some(is_personal);
self
}
pub fn next_offset(mut self, next_offset: impl Into<String>) -> Self {
self.next_offset = Some(next_offset.into());
self
}
pub fn button(mut self, button: InlineQueryResultsButton) -> Self {
self.button = Some(button);
self
}
pub fn validate(&self) -> Result<()> {
validate_non_empty_id("inline_query_id", &self.inline_query_id)?;
if self.results.len() > MAX_INLINE_QUERY_RESULTS {
return Err(invalid_request(format!(
"answerInlineQuery accepts at most {MAX_INLINE_QUERY_RESULTS} results"
)));
}
for result in &self.results {
result.validate()?;
}
if let Some(next_offset) = self.next_offset.as_deref() {
if next_offset.len() > MAX_INLINE_NEXT_OFFSET_BYTES {
return Err(invalid_request(format!(
"next_offset exceeds {MAX_INLINE_NEXT_OFFSET_BYTES} bytes"
)));
}
if next_offset.chars().any(char::is_control) {
return Err(invalid_request(
"next_offset must not contain control characters",
));
}
}
if let Some(button) = self.button.as_ref() {
button.validate()?;
}
Ok(())
}
}
fn invalid_request(reason: impl Into<String>) -> Error {
Error::InvalidRequest {
reason: reason.into(),
}
}
fn validate_non_empty_id(field: &str, value: &str) -> Result<()> {
if value.trim().is_empty() {
return Err(invalid_request(format!("{field} cannot be empty")));
}
if value.chars().any(char::is_control) {
return Err(invalid_request(format!(
"{field} must not contain control characters"
)));
}
Ok(())
}
fn validate_optional_text_limit(field: &str, value: &str, max_chars: usize) -> Result<()> {
if value.chars().count() > max_chars {
return Err(invalid_request(format!(
"{field} must be at most {max_chars} characters"
)));
}
if value.chars().any(char::is_control) {
return Err(invalid_request(format!(
"{field} must not contain control characters"
)));
}
Ok(())
}
fn validate_http_url(field: &str, value: &str) -> Result<()> {
let parsed = url::Url::parse(value)
.map_err(|source| invalid_request(format!("invalid {field}: {source}")))?;
match parsed.scheme() {
"http" | "https" => Ok(()),
scheme => Err(invalid_request(format!(
"{field} must use http or https scheme, got `{scheme}`"
))),
}
}
fn validate_allowed_update_name(update: &str) -> Result<()> {
if update.trim().is_empty() {
return Err(invalid_request(
"allowed_updates must not contain empty values",
));
}
if update.chars().any(char::is_control) {
return Err(invalid_request(
"allowed_updates must not contain control characters",
));
}
if update.chars().any(char::is_whitespace) {
return Err(invalid_request(
"allowed_updates must not contain whitespace",
));
}
Ok(())
}
pub(crate) fn validate_allowed_updates(allowed_updates: &[AllowedUpdate]) -> Result<()> {
let mut seen = BTreeSet::new();
for update in allowed_updates {
validate_allowed_update_name(update.as_str())?;
if !seen.insert(update.as_str()) {
return Err(invalid_request(format!(
"allowed_updates contains duplicate value `{update}`"
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::error::Error as StdError;
use serde_json::json;
use super::*;
#[test]
fn detects_primary_update_kind() -> std::result::Result<(), Box<dyn StdError>> {
let update: Update = serde_json::from_value(json!({
"update_id": 1,
"message": {
"message_id": 10,
"date": 1700000000,
"chat": {"id": 1, "type": "private"},
"text": "hello"
}
}))?;
assert_eq!(update.kind(), UpdateKind::Message);
assert_eq!(update.kinds(), vec![UpdateKind::Message]);
assert!(update.has_kind(UpdateKind::Message));
Ok(())
}
#[test]
fn supports_multi_kind_updates() -> std::result::Result<(), Box<dyn StdError>> {
let update: Update = serde_json::from_value(json!({
"update_id": 2,
"message": {
"message_id": 11,
"date": 1700000001,
"chat": {"id": 1, "type": "private"},
"text": "hello"
},
"callback_query": {
"id": "cb-1",
"from": {
"id": 1,
"is_bot": false,
"first_name": "test"
},
"chat_instance": "ci-1",
"data": "payload"
}
}))?;
assert_eq!(
update.kinds(),
vec![UpdateKind::Message, UpdateKind::CallbackQuery]
);
assert!(update.has_kind(UpdateKind::CallbackQuery));
Ok(())
}
#[test]
fn callback_query_accepts_inaccessible_message() -> std::result::Result<(), Box<dyn StdError>> {
let update: Update = serde_json::from_value(json!({
"update_id": 21,
"callback_query": {
"id": "cb-inaccessible",
"from": {"id": 1, "is_bot": false, "first_name": "tester"},
"message": {
"message_id": 55,
"date": 0,
"chat": {"id": -10010, "type": "supergroup", "title": "mods"}
},
"chat_instance": "ci",
"data": "payload"
}
}))?;
let message = update
.callback_query
.as_ref()
.and_then(|query| query.message.as_deref())
.ok_or("missing callback message")?;
assert!(!message.is_accessible());
assert_eq!(message.chat().id, -10010);
assert_eq!(message.message_id(), MessageId(55));
assert_eq!(update.web_app_data(), None);
Ok(())
}
#[test]
fn returns_unknown_for_unmodeled_payload() -> std::result::Result<(), Box<dyn StdError>> {
let update: Update = serde_json::from_value(json!({
"update_id": 3,
"new_kind_payload": {"foo": "bar"}
}))?;
assert_eq!(update.kind(), UpdateKind::Unknown);
assert_eq!(update.kinds(), vec![UpdateKind::Unknown]);
assert!(update.has_kind(UpdateKind::Unknown));
Ok(())
}
#[test]
fn keeps_unknown_alongside_modeled_kind() -> std::result::Result<(), Box<dyn StdError>> {
let update: Update = serde_json::from_value(json!({
"update_id": 4,
"message": {
"message_id": 12,
"date": 1700000004,
"chat": {"id": 1, "type": "private"},
"text": "hello"
},
"new_kind_payload": {"foo": "bar"}
}))?;
assert_eq!(update.kind(), UpdateKind::Message);
assert_eq!(
update.kinds(),
vec![UpdateKind::Message, UpdateKind::Unknown]
);
assert!(update.has_kind(UpdateKind::Unknown));
Ok(())
}
fn update_for_kind(kind: UpdateKind) -> std::result::Result<Update, Box<dyn StdError>> {
let payload = match kind {
UpdateKind::Message => json!({
"update_id": 100,
"message": {
"message_id": 10,
"date": 1700000100,
"chat": {"id": 1, "type": "private"},
"text": "hello"
}
}),
UpdateKind::EditedMessage => json!({
"update_id": 101,
"edited_message": {
"message_id": 10,
"date": 1700000101,
"chat": {"id": 1, "type": "private"},
"text": "hello"
}
}),
UpdateKind::ChannelPost => json!({
"update_id": 102,
"channel_post": {
"message_id": 10,
"date": 1700000102,
"chat": {"id": -1001, "type": "channel"},
"text": "post"
}
}),
UpdateKind::EditedChannelPost => json!({
"update_id": 103,
"edited_channel_post": {
"message_id": 10,
"date": 1700000103,
"chat": {"id": -1001, "type": "channel"},
"text": "post"
}
}),
UpdateKind::BusinessConnection => json!({
"update_id": 117,
"business_connection": {
"id": "business-1",
"user": {"id": 7001, "is_bot": false, "first_name": "owner"},
"user_chat_id": 7001,
"date": 1700000117,
"rights": {
"can_reply": true,
"can_read_messages": true
},
"is_enabled": true
}
}),
UpdateKind::BusinessMessage => json!({
"update_id": 113,
"business_message": {
"message_id": 20,
"business_connection_id": "business-1",
"date": 1700000113,
"chat": {"id": 7001, "type": "private", "first_name": "customer"},
"from": {"id": 7001, "is_bot": false, "first_name": "customer"},
"text": "business hello"
}
}),
UpdateKind::EditedBusinessMessage => json!({
"update_id": 114,
"edited_business_message": {
"message_id": 21,
"business_connection_id": "business-1",
"date": 1700000114,
"chat": {"id": 7001, "type": "private", "first_name": "customer"},
"from": {"id": 7001, "is_bot": false, "first_name": "customer"},
"text": "business edited"
}
}),
UpdateKind::DeletedBusinessMessages => json!({
"update_id": 115,
"deleted_business_messages": {
"business_connection_id": "business-1",
"chat": {"id": 7001, "type": "private", "first_name": "customer"},
"message_ids": [20, 21]
}
}),
UpdateKind::GuestMessage => json!({
"update_id": 116,
"guest_message": {
"message_id": 22,
"guest_query_id": "guest-1",
"date": 1700000116,
"chat": {"id": 8001, "type": "private", "first_name": "guest"},
"from": {"id": 8001, "is_bot": false, "first_name": "guest"},
"text": "guest hello"
}
}),
UpdateKind::MessageReaction => json!({
"update_id": 118,
"message_reaction": {
"chat": {"id": -1001, "type": "supergroup", "title": "mods"},
"message_id": 20,
"user": {"id": 1, "is_bot": false, "first_name": "reactor"},
"date": 1700000118,
"old_reaction": [],
"new_reaction": [{"type": "emoji", "emoji": "👍"}]
}
}),
UpdateKind::MessageReactionCount => json!({
"update_id": 119,
"message_reaction_count": {
"chat": {"id": -1001, "type": "supergroup", "title": "mods"},
"message_id": 20,
"date": 1700000119,
"reactions": [{
"type": {"type": "emoji", "emoji": "👍"},
"total_count": 3
}]
}
}),
UpdateKind::CallbackQuery => json!({
"update_id": 104,
"callback_query": {
"id": "cb-104",
"from": {"id": 1, "is_bot": false, "first_name": "tester"},
"chat_instance": "ci",
"data": "payload"
}
}),
UpdateKind::InlineQuery => json!({
"update_id": 105,
"inline_query": {
"id": "iq-105",
"from": {"id": 1, "is_bot": false, "first_name": "tester"},
"query": "search",
"offset": ""
}
}),
UpdateKind::ChosenInlineResult => json!({
"update_id": 106,
"chosen_inline_result": {
"result_id": "res-106",
"from": {"id": 1, "is_bot": false, "first_name": "tester"},
"query": "search"
}
}),
UpdateKind::ShippingQuery => json!({
"update_id": 120,
"shipping_query": {
"id": "shipping-1",
"from": {"id": 1, "is_bot": false, "first_name": "buyer"},
"invoice_payload": "invoice-payload",
"shipping_address": {
"country_code": "US",
"state": "CA",
"city": "San Francisco",
"street_line1": "1 Market St",
"street_line2": "Suite 1",
"post_code": "94105"
}
}
}),
UpdateKind::PreCheckoutQuery => json!({
"update_id": 121,
"pre_checkout_query": {
"id": "checkout-1",
"from": {"id": 1, "is_bot": false, "first_name": "buyer"},
"currency": "USD",
"total_amount": 145,
"invoice_payload": "invoice-payload",
"shipping_option_id": "ground",
"order_info": {
"name": "Buyer",
"email": "buyer@example.com"
}
}
}),
UpdateKind::PurchasedPaidMedia => json!({
"update_id": 122,
"purchased_paid_media": {
"from": {"id": 1, "is_bot": false, "first_name": "buyer"},
"paid_media_payload": "paid-media-payload"
}
}),
UpdateKind::Poll => json!({
"update_id": 107,
"poll": {
"id": "poll-107",
"question": "q?",
"options": [{"text": "a", "voter_count": 1}],
"total_voter_count": 1,
"is_closed": false,
"is_anonymous": false,
"type": "regular",
"allows_multiple_answers": false
}
}),
UpdateKind::PollAnswer => json!({
"update_id": 108,
"poll_answer": {
"poll_id": "poll-107",
"user": {"id": 1, "is_bot": false, "first_name": "tester"},
"option_ids": [0]
}
}),
UpdateKind::MyChatMember => json!({
"update_id": 109,
"my_chat_member": {
"chat": {"id": -1001, "type": "supergroup", "title": "mods"},
"from": {"id": 1, "is_bot": false, "first_name": "admin"},
"date": 1700000109,
"old_chat_member": {
"status": "member",
"user": {"id": 9, "is_bot": true, "first_name": "tele"}
},
"new_chat_member": {
"status": "administrator",
"user": {"id": 9, "is_bot": true, "first_name": "tele"},
"can_manage_chat": true
}
}
}),
UpdateKind::ChatMember => json!({
"update_id": 110,
"chat_member": {
"chat": {"id": -1001, "type": "supergroup", "title": "mods"},
"from": {"id": 1, "is_bot": false, "first_name": "admin"},
"date": 1700000110,
"old_chat_member": {
"status": "left",
"user": {"id": 55, "is_bot": false, "first_name": "member"}
},
"new_chat_member": {
"status": "member",
"user": {"id": 55, "is_bot": false, "first_name": "member"}
},
"via_join_request": true
}
}),
UpdateKind::ChatJoinRequest => json!({
"update_id": 111,
"chat_join_request": {
"chat": {"id": -1001, "type": "supergroup", "title": "mods"},
"from": {"id": 99, "is_bot": false, "first_name": "applicant"},
"user_chat_id": 9001,
"date": 1700000111,
"bio": "hello there"
}
}),
UpdateKind::ChatBoost => json!({
"update_id": 123,
"chat_boost": {
"chat": {"id": -1001, "type": "supergroup", "title": "mods"},
"boost": {
"boost_id": "boost-1",
"add_date": 1700000123,
"expiration_date": 1700086523,
"source": {
"source": "premium",
"user": {"id": 1, "is_bot": false, "first_name": "booster"}
}
}
}
}),
UpdateKind::RemovedChatBoost => json!({
"update_id": 124,
"removed_chat_boost": {
"chat": {"id": -1001, "type": "supergroup", "title": "mods"},
"boost_id": "boost-1",
"remove_date": 1700000124,
"source": {
"source": "premium",
"user": {"id": 1, "is_bot": false, "first_name": "booster"}
}
}
}),
UpdateKind::ManagedBot => json!({
"update_id": 125,
"managed_bot": {
"user": {"id": 1, "is_bot": false, "first_name": "owner"},
"bot": {"id": 2, "is_bot": true, "first_name": "managed"}
}
}),
UpdateKind::Unknown => json!({
"update_id": 112,
"new_kind_payload": {"foo": "bar"}
}),
};
Ok(serde_json::from_value(payload)?)
}
#[test]
fn update_kind_matrix_stays_in_sync() -> std::result::Result<(), Box<dyn StdError>> {
for kind in KNOWN_UPDATE_KINDS {
let update = update_for_kind(kind)?;
assert!(
update.has_kind(kind),
"missing has_kind mapping for {kind:?}"
);
assert!(
update.kinds().contains(&kind),
"missing kinds mapping for {kind:?}"
);
}
Ok(())
}
#[test]
fn unknown_update_kind_matrix_stays_in_sync() -> std::result::Result<(), Box<dyn StdError>> {
let update = update_for_kind(UpdateKind::Unknown)?;
assert_eq!(update.kind(), UpdateKind::Unknown);
assert!(update.has_kind(UpdateKind::Unknown));
assert_eq!(update.kinds(), vec![UpdateKind::Unknown]);
Ok(())
}
#[test]
fn parses_chat_join_request_as_typed_model() -> std::result::Result<(), Box<dyn StdError>> {
let update = update_for_kind(UpdateKind::ChatJoinRequest)?;
let Some(join_request) = update.chat_join_request() else {
return Err("missing typed join request".into());
};
assert_eq!(join_request.chat_id(), -1001);
assert_eq!(join_request.user_id(), 99);
assert_eq!(join_request.bio.as_deref(), Some("hello there"));
Ok(())
}
#[test]
fn parses_business_and_guest_updates_as_message_like_models()
-> std::result::Result<(), Box<dyn StdError>> {
let connection = update_for_kind(UpdateKind::BusinessConnection)?;
let connection = connection
.business_connection()
.ok_or("missing business connection")?;
assert_eq!(connection.id, "business-1");
assert_eq!(connection.user_id(), 7001);
assert!(
connection
.rights
.as_ref()
.is_some_and(|rights| rights.can_reply)
);
let business = update_for_kind(UpdateKind::BusinessMessage)?;
assert_eq!(business.kind(), UpdateKind::BusinessMessage);
let message = business
.business_message
.as_deref()
.ok_or("missing business message")?;
assert_eq!(message.message_id, MessageId(20));
assert_eq!(message.chat.id, 7001);
assert_eq!(
message.business_connection_id.as_deref(),
Some("business-1")
);
let edited = update_for_kind(UpdateKind::EditedBusinessMessage)?;
assert_eq!(edited.kind(), UpdateKind::EditedBusinessMessage);
assert_eq!(
edited
.edited_business_message
.as_deref()
.and_then(|message| message.text.as_deref()),
Some("business edited")
);
let deleted = update_for_kind(UpdateKind::DeletedBusinessMessages)?;
let deleted = deleted
.deleted_business_messages()
.ok_or("missing deleted business messages")?;
assert_eq!(deleted.business_connection_id, "business-1");
assert_eq!(deleted.chat_id(), 7001);
assert_eq!(deleted.message_ids(), &[MessageId(20), MessageId(21)]);
let guest = update_for_kind(UpdateKind::GuestMessage)?;
assert_eq!(guest.kind(), UpdateKind::GuestMessage);
let guest_message = guest
.guest_message
.as_deref()
.ok_or("missing guest message")?;
assert_eq!(guest_message.text.as_deref(), Some("guest hello"));
assert_eq!(guest_message.guest_query_id.as_deref(), Some("guest-1"));
Ok(())
}
#[test]
fn parses_modern_update_payloads_as_typed_models() -> std::result::Result<(), Box<dyn StdError>>
{
let reaction = update_for_kind(UpdateKind::MessageReaction)?;
let reaction = reaction
.message_reaction()
.ok_or("missing message reaction")?;
assert_eq!(reaction.chat.id, -1001);
assert_eq!(reaction.message_id, MessageId(20));
assert_eq!(reaction.user.as_ref().map(|user| user.id.0), Some(1));
assert_eq!(reaction.new_reaction.len(), 1);
let reaction_count = update_for_kind(UpdateKind::MessageReactionCount)?;
let reaction_count = reaction_count
.message_reaction_count()
.ok_or("missing message reaction count")?;
assert_eq!(reaction_count.reactions.len(), 1);
assert_eq!(reaction_count.reactions[0].total_count, 3);
let shipping = update_for_kind(UpdateKind::ShippingQuery)?;
let shipping = shipping.shipping_query().ok_or("missing shipping query")?;
assert_eq!(shipping.from.id.0, 1);
assert_eq!(shipping.shipping_address.country_code, "US");
let checkout = update_for_kind(UpdateKind::PreCheckoutQuery)?;
let checkout = checkout
.pre_checkout_query()
.ok_or("missing pre-checkout query")?;
assert_eq!(checkout.total_amount, 145);
assert_eq!(
checkout
.order_info
.as_ref()
.and_then(|info| info.email.as_deref()),
Some("buyer@example.com")
);
let purchase = update_for_kind(UpdateKind::PurchasedPaidMedia)?;
let purchase = purchase
.purchased_paid_media()
.ok_or("missing paid media purchase")?;
assert_eq!(purchase.paid_media_payload, "paid-media-payload");
let boost = update_for_kind(UpdateKind::ChatBoost)?;
let boost = boost.chat_boost().ok_or("missing chat boost")?;
assert_eq!(boost.chat.id, -1001);
assert_eq!(
boost.boost.source.user.as_ref().map(|user| user.id.0),
Some(1)
);
let removed = update_for_kind(UpdateKind::RemovedChatBoost)?;
let removed = removed
.removed_chat_boost()
.ok_or("missing removed chat boost")?;
assert_eq!(removed.boost_id, "boost-1");
let managed = update_for_kind(UpdateKind::ManagedBot)?;
let managed = managed.managed_bot().ok_or("missing managed bot")?;
assert_eq!(managed.user.id.0, 1);
assert_eq!(managed.bot.id.0, 2);
Ok(())
}
#[test]
fn parses_chat_member_updates_as_typed_model() -> std::result::Result<(), Box<dyn StdError>> {
let member_update = update_for_kind(UpdateKind::ChatMember)?;
let my_member_update = update_for_kind(UpdateKind::MyChatMember)?;
let Some(chat_member) = member_update.chat_member() else {
return Err("missing chat_member update".into());
};
assert_eq!(chat_member.chat_id(), -1001);
assert_eq!(chat_member.actor_id(), 1);
assert_eq!(chat_member.subject_id(), 55);
assert!(chat_member.via_join_request);
let Some(my_chat_member) = my_member_update.my_chat_member() else {
return Err("missing my_chat_member update".into());
};
assert_eq!(my_chat_member.chat_id(), -1001);
assert_eq!(my_chat_member.actor_id(), 1);
assert!(my_chat_member.member().is_admin());
Ok(())
}
#[test]
fn update_kind_names_round_trip() {
for kind in UpdateKind::all_known() {
assert_eq!(UpdateKind::from_name(kind.as_str()), Some(*kind));
assert_eq!(kind.allowed_update_name(), Some(kind.as_str()));
}
assert_eq!(UpdateKind::Unknown.as_str(), "unknown");
assert_eq!(UpdateKind::Unknown.allowed_update_name(), None);
assert_eq!(UpdateKind::from_name("unknown"), Some(UpdateKind::Unknown));
assert_eq!(UpdateKind::from_name("future_update"), None);
}
#[test]
fn allowed_update_values_are_validated() -> std::result::Result<(), Box<dyn StdError>> {
assert_eq!(
AllowedUpdate::from_kind(UpdateKind::CallbackQuery)?.as_str(),
"callback_query"
);
assert_eq!(
serde_json::from_str::<AllowedUpdate>("\"message\"")?.as_str(),
"message"
);
assert_eq!(
AllowedUpdate::from_kinds([UpdateKind::Message, UpdateKind::ChatBoost])?,
vec![
AllowedUpdate::new("message")?,
AllowedUpdate::new("chat_boost")?
]
);
assert!(matches!(
AllowedUpdate::from_kind(UpdateKind::Unknown),
Err(Error::InvalidRequest { .. })
));
assert!(matches!(
AllowedUpdate::new(""),
Err(Error::InvalidRequest { .. })
));
assert!(matches!(
AllowedUpdate::new("message\n"),
Err(Error::InvalidRequest { .. })
));
assert!(matches!(
AllowedUpdate::new(" message "),
Err(Error::InvalidRequest { .. })
));
assert!(serde_json::from_str::<AllowedUpdate>("\" message \"").is_err());
Ok(())
}
#[test]
fn validates_get_updates_request_bounds() -> Result<()> {
let valid = GetUpdatesRequest {
limit: Some(100),
allowed_updates: Some(vec![
AllowedUpdate::new("message")?,
AllowedUpdate::new("callback_query")?,
]),
..GetUpdatesRequest::default()
};
assert!(valid.validate().is_ok());
let invalid_limit = GetUpdatesRequest {
limit: Some(0),
..GetUpdatesRequest::default()
};
assert!(matches!(
invalid_limit.validate(),
Err(Error::InvalidRequest { .. })
));
let duplicate_update = GetUpdatesRequest {
allowed_updates: Some(vec![
AllowedUpdate::new("message")?,
AllowedUpdate::new("message")?,
]),
..GetUpdatesRequest::default()
};
assert!(matches!(
duplicate_update.validate(),
Err(Error::InvalidRequest { .. })
));
Ok(())
}
#[test]
fn validates_callback_and_inline_answers() -> Result<()> {
let valid_callback = AnswerCallbackQueryRequest {
callback_query_id: "callback-1".to_owned(),
text: Some("ok".to_owned()),
show_alert: None,
url: Some("https://example.com".to_owned()),
cache_time: None,
};
assert!(valid_callback.validate().is_ok());
let invalid_callback = AnswerCallbackQueryRequest {
callback_query_id: String::new(),
text: None,
show_alert: None,
url: None,
cache_time: None,
};
assert!(matches!(
invalid_callback.validate(),
Err(Error::InvalidRequest { .. })
));
let too_many_results = (0..=MAX_INLINE_QUERY_RESULTS)
.map(|index| {
InlineQueryResult::new(json!({"type": "article", "id": index.to_string()}))
})
.collect::<Result<Vec<_>>>()?;
let invalid_inline = AnswerInlineQueryRequest::new("inline-1", too_many_results);
assert!(matches!(
invalid_inline.validate(),
Err(Error::InvalidRequest { .. })
));
let invalid_offset = AnswerInlineQueryRequest::new("inline-1", Vec::new())
.next_offset("x".repeat(MAX_INLINE_NEXT_OFFSET_BYTES + 1));
assert!(matches!(
invalid_offset.validate(),
Err(Error::InvalidRequest { .. })
));
let invalid_button = AnswerInlineQueryRequest::new("inline-1", Vec::new())
.button(InlineQueryResultsButton::new("Open"));
assert!(matches!(
invalid_button.validate(),
Err(Error::InvalidRequest { .. })
));
Ok(())
}
}