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, Eq, Hash, Ord, PartialEq, PartialOrd)]
20enum ImageHashInner {
21 Normal { hash: [u8; 16], is_animated: bool },
22 Clyde,
23}
24
25#[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 #[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#[derive(Debug, Clone)]
113pub enum ImageHashParseError {
114 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
164#[non_exhaustive]
165pub struct EmojiIdentifier {
166 pub animated: bool,
168 pub id: EmojiId,
170 pub name: String,
173}
174
175#[cfg(all(feature = "model", feature = "utils"))]
176impl EmojiIdentifier {
177 #[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#[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#[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#[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}