1#[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#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
19#[derive(Clone, Copy, PartialEq, Eq)]
20enum ImageHashInner {
21 Normal { hash: [u8; 16], is_animated: bool },
22 Clyde,
23}
24
25#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
39#[derive(Clone, Copy, PartialEq, Eq)]
40pub struct ImageHash(ImageHashInner);
41
42impl ImageHash {
43 #[must_use]
53 pub fn is_animated(&self) -> bool {
54 match &self.0 {
55 ImageHashInner::Normal {
56 is_animated, ..
57 } => *is_animated,
58 ImageHashInner::Clyde => true,
59 }
60 }
61
62 #[must_use]
63 fn into_arraystring(self) -> ArrayString<34> {
64 let ImageHashInner::Normal {
65 hash,
66 is_animated,
67 } = &self.0
68 else {
69 return ArrayString::from_str("clyde").expect("the string clyde is less than 34 chars");
70 };
71
72 let mut out = ArrayString::new();
73 if *is_animated {
74 out.push_str("a_");
75 }
76
77 for byte in hash {
78 write!(out, "{byte:02x}").expect("ImageHash should fit into 34 char ArrayString");
79 }
80
81 out
82 }
83}
84
85impl std::fmt::Debug for ImageHash {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 f.write_str("\"")?;
88 <Self as std::fmt::Display>::fmt(self, f)?;
89 f.write_str("\"")
90 }
91}
92
93impl serde::Serialize for ImageHash {
94 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
95 self.into_arraystring().serialize(serializer)
96 }
97}
98
99impl<'de> serde::Deserialize<'de> for ImageHash {
100 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101 let helper = ArrayString::<34>::deserialize(deserializer)?;
102 Self::from_str(&helper).map_err(serde::de::Error::custom)
103 }
104}
105
106impl std::fmt::Display for ImageHash {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 self.into_arraystring().fmt(f)
109 }
110}
111
112#[derive(Debug, Clone)]
114pub enum ImageHashParseError {
115 InvalidLength(usize),
117}
118
119impl std::error::Error for ImageHashParseError {}
120
121impl std::fmt::Display for ImageHashParseError {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 match self {
124 Self::InvalidLength(length) => {
125 write!(f, "Invalid length {length}, expected 32 or 34 characters")
126 },
127 }
128 }
129}
130
131impl std::str::FromStr for ImageHash {
132 type Err = ImageHashParseError;
133
134 fn from_str(s: &str) -> StdResult<Self, Self::Err> {
135 let (hex, is_animated) = if s.len() == 34 && s.starts_with("a_") {
136 (&s[2..], true)
137 } else if s.len() == 32 {
138 (s, false)
139 } else if s == "clyde" {
140 return Ok(Self(ImageHashInner::Clyde));
141 } else {
142 return Err(Self::Err::InvalidLength(s.len()));
143 };
144
145 let mut hash = [0u8; 16];
146 for i in (0..hex.len()).step_by(2) {
147 let hex_byte = &hex[i..i + 2];
148 hash[i / 2] = u8::from_str_radix(hex_byte, 16).unwrap_or_else(|err| {
149 tracing::warn!("Invalid byte in ImageHash ({s}): {err}");
150 0
151 });
152 }
153
154 Ok(Self(ImageHashInner::Normal {
155 is_animated,
156 hash,
157 }))
158 }
159}
160
161#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
165#[non_exhaustive]
166pub struct EmojiIdentifier {
167 pub animated: bool,
169 pub id: EmojiId,
171 pub name: String,
174}
175
176#[cfg(all(feature = "model", feature = "utils"))]
177impl EmojiIdentifier {
178 #[must_use]
180 pub fn url(&self) -> String {
181 let ext = if self.animated { "gif" } else { "png" };
182
183 cdn!("/emojis/{}.{}", self.id, ext)
184 }
185}
186
187#[cfg(all(feature = "model", feature = "utils"))]
188impl fmt::Display for EmojiIdentifier {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 if self.animated {
191 f.write_str("<a:")?;
192 } else {
193 f.write_str("<:")?;
194 }
195
196 f.write_str(&self.name)?;
197
198 f.write_char(':')?;
199 fmt::Display::fmt(&self.id, f)?;
200 f.write_char('>')
201 }
202}
203
204#[derive(Debug)]
205#[cfg(all(feature = "model", feature = "utils"))]
206pub struct EmojiIdentifierParseError {
207 parsed_string: String,
208}
209
210#[cfg(all(feature = "model", feature = "utils"))]
211impl fmt::Display for EmojiIdentifierParseError {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 write!(f, "`{}` is not a valid emoji identifier", self.parsed_string)
214 }
215}
216
217#[cfg(all(feature = "model", feature = "utils"))]
218impl StdError for EmojiIdentifierParseError {}
219
220#[cfg(all(feature = "model", feature = "utils"))]
221impl FromStr for EmojiIdentifier {
222 type Err = EmojiIdentifierParseError;
223
224 fn from_str(s: &str) -> StdResult<Self, Self::Err> {
225 utils::parse_emoji(s).ok_or_else(|| EmojiIdentifierParseError {
226 parsed_string: s.to_owned(),
227 })
228 }
229}
230
231#[derive(Clone, Debug, Deserialize, Serialize)]
237#[non_exhaustive]
238pub struct Incident {
239 pub created_at: String,
240 pub id: String,
241 pub impact: String,
242 pub incident_updates: Vec<IncidentUpdate>,
243 pub monitoring_at: Option<String>,
244 pub name: String,
245 pub page_id: String,
246 pub resolved_at: Option<String>,
247 pub shortlink: String,
248 pub status: String,
249 pub updated_at: String,
250}
251
252#[derive(Clone, Debug, Deserialize, Serialize)]
258#[non_exhaustive]
259pub struct IncidentUpdate {
260 pub body: String,
261 pub created_at: String,
262 pub display_at: String,
263 pub id: String,
264 pub incident_id: String,
265 pub status: String,
266 pub updated_at: String,
267}
268
269#[derive(Clone, Debug, Deserialize, Serialize)]
274#[non_exhaustive]
275pub struct Maintenance {
276 pub created_at: String,
277 pub id: String,
278 pub impact: String,
279 pub incident_updates: Vec<IncidentUpdate>,
280 pub monitoring_at: Option<String>,
281 pub name: String,
282 pub page_id: String,
283 pub resolved_at: Option<String>,
284 pub scheduled_for: String,
285 pub scheduled_until: String,
286 pub shortlink: String,
287 pub status: String,
288 pub updated_at: String,
289}
290
291#[cfg(test)]
292mod test {
293 use crate::model::prelude::*;
294
295 #[test]
296 fn test_formatters() {
297 assert_eq!(ChannelId::new(1).to_string(), "1");
298 assert_eq!(EmojiId::new(2).to_string(), "2");
299 assert_eq!(GuildId::new(3).to_string(), "3");
300 assert_eq!(RoleId::new(4).to_string(), "4");
301 assert_eq!(UserId::new(5).to_string(), "5");
302 }
303}