Skip to main content

webserver_base/telegram/
chat_id.rs

1use std::fmt::{Display, Formatter};
2
3use serde::{Serialize, Serializer};
4
5/// Identifies the chat a message is sent to.
6///
7/// Telegram accepts either a numeric id or an `@channelusername`, and this type
8/// serializes both as a JSON string, which Telegram accepts for either form.
9///
10/// This is also the key the rate limiter paces against, since Telegram's
11/// tightest documented limit — roughly one message per second — is per chat.
12#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
13pub enum ChatId {
14    /// A numeric chat id. Negative for groups, supergroups, and channels.
15    Id(i64),
16    /// An `@channelusername`, stored with its leading `@`.
17    Username(String),
18}
19
20impl ChatId {
21    /// Creates a [`ChatId`] from a numeric id.
22    #[must_use]
23    pub const fn id(id: i64) -> Self {
24        Self::Id(id)
25    }
26
27    /// Creates a [`ChatId`] from a channel username, adding the leading `@` if absent.
28    #[must_use]
29    pub fn username(username: impl AsRef<str>) -> Self {
30        let username: &str = username.as_ref();
31
32        if username.starts_with('@') {
33            Self::Username(username.to_string())
34        } else {
35            Self::Username(format!("@{username}"))
36        }
37    }
38}
39
40impl Display for ChatId {
41    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
42        match self {
43            Self::Id(id) => write!(f, "{id}"),
44            Self::Username(username) => write!(f, "{username}"),
45        }
46    }
47}
48
49impl Serialize for ChatId {
50    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
51        serializer.serialize_str(&self.to_string())
52    }
53}
54
55impl From<i64> for ChatId {
56    fn from(id: i64) -> Self {
57        Self::Id(id)
58    }
59}
60
61impl From<&str> for ChatId {
62    /// Parses a numeric id if possible, otherwise treats the value as a username.
63    fn from(value: &str) -> Self {
64        let value: &str = value.trim();
65
66        value
67            .parse::<i64>()
68            .map_or_else(|_| Self::username(value), Self::Id)
69    }
70}
71
72impl From<String> for ChatId {
73    fn from(value: String) -> Self {
74        Self::from(value.as_str())
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::ChatId;
81
82    #[test]
83    fn numeric_id_round_trips() {
84        let expected: ChatId = ChatId::Id(7_518_714_136);
85        let actual: ChatId = ChatId::from(7_518_714_136_i64);
86        assert_eq!(expected, actual);
87    }
88
89    #[test]
90    fn negative_group_id_is_preserved() {
91        let expected: ChatId = ChatId::Id(-1_001_234_567_890);
92        let actual: ChatId = ChatId::from(-1_001_234_567_890_i64);
93        assert_eq!(expected, actual);
94    }
95
96    #[test]
97    fn username_gains_a_leading_at_sign() {
98        let expected: ChatId = ChatId::Username(String::from("@mychannel"));
99        let actual: ChatId = ChatId::username("mychannel");
100        assert_eq!(expected, actual);
101    }
102
103    #[test]
104    fn username_keeps_an_existing_at_sign() {
105        let expected: ChatId = ChatId::Username(String::from("@mychannel"));
106        let actual: ChatId = ChatId::username("@mychannel");
107        assert_eq!(expected, actual);
108    }
109
110    #[test]
111    fn numeric_string_parses_as_an_id() {
112        let expected: ChatId = ChatId::Id(7_518_714_136);
113        let actual: ChatId = ChatId::from("7518714136");
114        assert_eq!(expected, actual);
115    }
116
117    #[test]
118    fn non_numeric_string_parses_as_a_username() {
119        let expected: ChatId = ChatId::Username(String::from("@mychannel"));
120        let actual: ChatId = ChatId::from("@mychannel");
121        assert_eq!(expected, actual);
122    }
123
124    #[test]
125    fn numeric_id_serializes_as_a_string() {
126        let expected: String = String::from("\"7518714136\"");
127        let actual: String =
128            serde_json::to_string(&ChatId::Id(7_518_714_136)).expect("chat id should serialize");
129        assert_eq!(expected, actual);
130    }
131
132    #[test]
133    fn username_serializes_as_a_string() {
134        let expected: String = String::from("\"@mychannel\"");
135        let actual: String = serde_json::to_string(&ChatId::username("mychannel"))
136            .expect("chat id should serialize");
137        assert_eq!(expected, actual);
138    }
139}