Skip to main content

serenity/model/
misc.rs

1//! Miscellaneous helper traits, enums, and structs for models.
2
3#[cfg(all(feature = "model", feature = "utils"))]
4use std::error::Error as StdError;
5use std::fmt;
6use std::fmt::Write;
7#[cfg(all(feature = "model", feature = "utils"))]
8use std::result::Result as StdResult;
9use std::str::FromStr;
10
11use arrayvec::ArrayString;
12
13use super::prelude::*;
14#[cfg(all(feature = "model", any(feature = "cache", feature = "utils")))]
15use crate::utils;
16
17/// Hides the implementation detail of ImageHash as an enum.
18#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
19#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
20enum ImageHashInner {
21    Normal { hash: [u8; 16], is_animated: bool },
22    Clyde,
23}
24
25/// An image hash returned from the Discord API.
26///
27/// This type can be constructed via it's [`FromStr`] implementation, and can be turned into it's
28/// cannonical representation via [`std::fmt::Display`] or [`serde::Serialize`].
29///
30/// # Example
31/// ```rust
32/// use serenity::model::misc::ImageHash;
33///
34/// let image_hash: ImageHash = "f1eff024d9c85339c877985229ed8fec".parse().unwrap();
35/// assert_eq!(image_hash.to_string(), String::from("f1eff024d9c85339c877985229ed8fec"));
36/// ```
37#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
38#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
39pub struct ImageHash(ImageHashInner);
40
41impl ImageHash {
42    /// Returns if the linked image is animated, which means the hash starts with `a_`.
43    ///
44    /// # Example
45    /// ```rust
46    /// use serenity::model::misc::ImageHash;
47    ///
48    /// let animated_hash: ImageHash = "a_e3c0db7f38777778fb43081f8746ebc9".parse().unwrap();
49    /// assert!(animated_hash.is_animated());
50    /// ```
51    #[must_use]
52    pub fn is_animated(&self) -> bool {
53        match &self.0 {
54            ImageHashInner::Normal {
55                is_animated, ..
56            } => *is_animated,
57            ImageHashInner::Clyde => true,
58        }
59    }
60
61    #[must_use]
62    fn into_arraystring(self) -> ArrayString<34> {
63        let ImageHashInner::Normal {
64            hash,
65            is_animated,
66        } = &self.0
67        else {
68            return ArrayString::from_str("clyde").expect("the string clyde is less than 34 chars");
69        };
70
71        let mut out = ArrayString::new();
72        if *is_animated {
73            out.push_str("a_");
74        }
75
76        for byte in hash {
77            write!(out, "{byte:02x}").expect("ImageHash should fit into 34 char ArrayString");
78        }
79
80        out
81    }
82}
83
84impl std::fmt::Debug for ImageHash {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str("\"")?;
87        <Self as std::fmt::Display>::fmt(self, f)?;
88        f.write_str("\"")
89    }
90}
91
92impl serde::Serialize for ImageHash {
93    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
94        self.into_arraystring().serialize(serializer)
95    }
96}
97
98impl<'de> serde::Deserialize<'de> for ImageHash {
99    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
100        let helper = ArrayString::<34>::deserialize(deserializer)?;
101        Self::from_str(&helper).map_err(serde::de::Error::custom)
102    }
103}
104
105impl std::fmt::Display for ImageHash {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        self.into_arraystring().fmt(f)
108    }
109}
110
111/// An error returned when [`ImageHash`] is passed an erronous value.
112#[derive(Debug, Clone)]
113pub enum ImageHashParseError {
114    /// The given hash was not a valid [`ImageHash`] length, containing the invalid length.
115    InvalidLength(usize),
116}
117
118impl std::error::Error for ImageHashParseError {}
119
120impl std::fmt::Display for ImageHashParseError {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        match self {
123            Self::InvalidLength(length) => {
124                write!(f, "Invalid length {length}, expected 32 or 34 characters")
125            },
126        }
127    }
128}
129
130impl std::str::FromStr for ImageHash {
131    type Err = ImageHashParseError;
132
133    fn from_str(s: &str) -> StdResult<Self, Self::Err> {
134        let (hex, is_animated) = if s.len() == 34 && s.starts_with("a_") {
135            (&s[2..], true)
136        } else if s.len() == 32 {
137            (s, false)
138        } else if s == "clyde" {
139            return Ok(Self(ImageHashInner::Clyde));
140        } else {
141            return Err(Self::Err::InvalidLength(s.len()));
142        };
143
144        let mut hash = [0u8; 16];
145        for i in (0..hex.len()).step_by(2) {
146            let hex_byte = &hex[i..i + 2];
147            hash[i / 2] = u8::from_str_radix(hex_byte, 16).unwrap_or_else(|err| {
148                tracing::warn!("Invalid byte in ImageHash ({s}): {err}");
149                0
150            });
151        }
152
153        Ok(Self(ImageHashInner::Normal {
154            is_animated,
155            hash,
156        }))
157    }
158}
159
160/// A version of an emoji used only when solely the animated state, Id, and name are known.
161///
162/// [Discord docs](https://discord.com/developers/docs/topics/gateway#activity-object-activity-emoji).
163#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
164#[non_exhaustive]
165pub struct EmojiIdentifier {
166    /// Whether the emoji is animated
167    pub animated: bool,
168    /// The Id of the emoji.
169    pub id: EmojiId,
170    /// The name of the emoji. It must be at least 2 characters long and can only contain
171    /// alphanumeric characters and underscores.
172    pub name: String,
173}
174
175#[cfg(all(feature = "model", feature = "utils"))]
176impl EmojiIdentifier {
177    /// Generates a URL to the emoji's image.
178    #[must_use]
179    pub fn url(&self) -> String {
180        let ext = if self.animated { "gif" } else { "png" };
181
182        cdn!("/emojis/{}.{}", self.id, ext)
183    }
184}
185
186#[cfg(all(feature = "model", feature = "utils"))]
187impl fmt::Display for EmojiIdentifier {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        if self.animated {
190            f.write_str("<a:")?;
191        } else {
192            f.write_str("<:")?;
193        }
194
195        f.write_str(&self.name)?;
196
197        f.write_char(':')?;
198        fmt::Display::fmt(&self.id, f)?;
199        f.write_char('>')
200    }
201}
202
203#[derive(Debug)]
204#[cfg(all(feature = "model", feature = "utils"))]
205pub struct EmojiIdentifierParseError {
206    parsed_string: String,
207}
208
209#[cfg(all(feature = "model", feature = "utils"))]
210impl fmt::Display for EmojiIdentifierParseError {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        write!(f, "`{}` is not a valid emoji identifier", self.parsed_string)
213    }
214}
215
216#[cfg(all(feature = "model", feature = "utils"))]
217impl StdError for EmojiIdentifierParseError {}
218
219#[cfg(all(feature = "model", feature = "utils"))]
220impl FromStr for EmojiIdentifier {
221    type Err = EmojiIdentifierParseError;
222
223    fn from_str(s: &str) -> StdResult<Self, Self::Err> {
224        utils::parse_emoji(s).ok_or_else(|| EmojiIdentifierParseError {
225            parsed_string: s.to_owned(),
226        })
227    }
228}
229
230/// An incident retrieved from the Discord status page.
231///
232/// This is not necessarily a representation of an ongoing incident.
233///
234/// [Discord docs](https://discordstatus.com/api) (see "Unresolved incident" example)
235#[derive(Clone, Debug, Deserialize, Serialize)]
236#[non_exhaustive]
237pub struct Incident {
238    pub created_at: String,
239    pub id: String,
240    pub impact: String,
241    pub incident_updates: Vec<IncidentUpdate>,
242    pub monitoring_at: Option<String>,
243    pub name: String,
244    pub page_id: String,
245    pub resolved_at: Option<String>,
246    pub shortlink: String,
247    pub status: String,
248    pub updated_at: String,
249}
250
251/// An update to an incident from the Discord status page.
252///
253/// This will typically state what new information has been discovered about an incident.
254///
255/// [Discord docs](https://discordstatus.com/api) (see "Unresolved incident" example)
256#[derive(Clone, Debug, Deserialize, Serialize)]
257#[non_exhaustive]
258pub struct IncidentUpdate {
259    pub body: String,
260    pub created_at: String,
261    pub display_at: String,
262    pub id: String,
263    pub incident_id: String,
264    pub status: String,
265    pub updated_at: String,
266}
267
268/// A Discord status maintenance message. This can be either for active maintenances or for
269/// scheduled maintenances.
270///
271/// [Discord docs](https://discordstatus.com/api) (see "scheduled maintenances" examples)
272#[derive(Clone, Debug, Deserialize, Serialize)]
273#[non_exhaustive]
274pub struct Maintenance {
275    pub created_at: String,
276    pub id: String,
277    pub impact: String,
278    pub incident_updates: Vec<IncidentUpdate>,
279    pub monitoring_at: Option<String>,
280    pub name: String,
281    pub page_id: String,
282    pub resolved_at: Option<String>,
283    pub scheduled_for: String,
284    pub scheduled_until: String,
285    pub shortlink: String,
286    pub status: String,
287    pub updated_at: String,
288}
289
290#[cfg(test)]
291mod test {
292    use crate::model::prelude::*;
293
294    #[test]
295    fn test_formatters() {
296        assert_eq!(ChannelId::new(1).to_string(), "1");
297        assert_eq!(EmojiId::new(2).to_string(), "2");
298        assert_eq!(GuildId::new(3).to_string(), "3");
299        assert_eq!(RoleId::new(4).to_string(), "4");
300        assert_eq!(UserId::new(5).to_string(), "5");
301    }
302}