use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{Error, Result};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct UserId(pub i64);
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct MessageId(pub i64);
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct NumericChatId(pub i64);
impl From<i64> for NumericChatId {
fn from(value: i64) -> Self {
Self(value)
}
}
impl From<i64> for UserId {
fn from(value: i64) -> Self {
Self(value)
}
}
impl From<i64> for MessageId {
fn from(value: i64) -> Self {
Self(value)
}
}
impl UserId {
pub fn validate(&self) -> Result<()> {
if self.0 <= 0 {
return Err(invalid_request("user_id must be greater than 0"));
}
Ok(())
}
}
impl MessageId {
pub fn validate(&self) -> Result<()> {
if self.0 <= 0 {
return Err(invalid_request("message_id must be greater than 0"));
}
Ok(())
}
}
impl NumericChatId {
pub fn validate(&self) -> Result<()> {
if self.0 == 0 {
return Err(invalid_request("chat_id cannot be 0"));
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ChatId {
Id(i64),
Username(String),
}
impl From<i64> for ChatId {
fn from(value: i64) -> Self {
Self::Id(value)
}
}
impl From<String> for ChatId {
fn from(value: String) -> Self {
Self::Username(value)
}
}
impl From<&str> for ChatId {
fn from(value: &str) -> Self {
Self::Username(value.to_owned())
}
}
impl ChatId {
pub fn validate(&self) -> Result<()> {
match self {
Self::Id(0) => Err(invalid_request("chat_id cannot be 0")),
Self::Id(_) => Ok(()),
Self::Username(username) => validate_chat_username(username),
}
}
}
fn validate_chat_username(username: &str) -> Result<()> {
let Some(handle) = username.strip_prefix('@') else {
return Err(invalid_request(
"chat_id username must start with `@` when provided as a string",
));
};
if handle.is_empty() {
return Err(invalid_request("chat_id username cannot be empty"));
}
if !(5..=32).contains(&handle.len()) {
return Err(invalid_request(
"chat_id username must be 5-32 ASCII characters after `@`",
));
}
if !handle
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
{
return Err(invalid_request(
"chat_id username may only contain ASCII letters, digits or `_` after `@`",
));
}
Ok(())
}
fn invalid_request(reason: impl Into<String>) -> Error {
Error::InvalidRequest {
reason: reason.into(),
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum ParseMode {
#[serde(rename = "Markdown")]
Markdown,
#[serde(rename = "MarkdownV2")]
MarkdownV2,
#[serde(rename = "HTML")]
Html,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
#[non_exhaustive]
pub struct ResponseParameters {
#[serde(default)]
pub migrate_to_chat_id: Option<i64>,
#[serde(default)]
pub retry_after: Option<u64>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validates_chat_id_values() {
assert!(ChatId::from(1_i64).validate().is_ok());
assert!(ChatId::from(-100_i64).validate().is_ok());
assert!(ChatId::from("@channel_name").validate().is_ok());
for chat_id in [
ChatId::from(0_i64),
ChatId::from("channel"),
ChatId::from("@"),
ChatId::from("@abcd"),
ChatId::from("@bad-name"),
ChatId::from("@bad name"),
ChatId::from("@abcdefghijklmnopqrstuvwxyzabcdefg"),
] {
assert!(matches!(
chat_id.validate(),
Err(Error::InvalidRequest { .. })
));
}
}
#[test]
fn validates_user_and_message_ids() {
assert!(UserId::from(1_i64).validate().is_ok());
assert!(MessageId::from(1_i64).validate().is_ok());
assert!(NumericChatId::from(1_i64).validate().is_ok());
assert!(NumericChatId::from(-100_i64).validate().is_ok());
for user_id in [UserId::from(0_i64), UserId::from(-1_i64)] {
assert!(matches!(
user_id.validate(),
Err(Error::InvalidRequest { .. })
));
}
for message_id in [MessageId::from(0_i64), MessageId::from(-1_i64)] {
assert!(matches!(
message_id.validate(),
Err(Error::InvalidRequest { .. })
));
}
assert!(matches!(
NumericChatId::from(0_i64).validate(),
Err(Error::InvalidRequest { .. })
));
}
#[test]
fn response_parameters_preserve_future_fields()
-> std::result::Result<(), Box<dyn std::error::Error>> {
let parameters: ResponseParameters = serde_json::from_value(serde_json::json!({
"migrate_to_chat_id": -100,
"retry_after": 5,
"future_parameter": "kept"
}))?;
assert_eq!(parameters.migrate_to_chat_id, Some(-100));
assert_eq!(parameters.retry_after, Some(5));
assert_eq!(parameters.extra["future_parameter"], "kept");
Ok(())
}
}