1use bitflags::bitflags;
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
5pub enum EmoteProvider {
6 Twitch,
7 BetterTtv,
8 SevenTv,
9 FrankerFaceZ,
10 Other(String),
11}
12
13#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct EmoteId {
15 pub provider: EmoteProvider,
16 pub value: String,
17}
18
19impl EmoteId {
20 pub fn new(provider: EmoteProvider, value: impl Into<String>) -> Self {
21 Self {
22 provider,
23 value: value.into(),
24 }
25 }
26}
27
28#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum EmoteImageFormat {
30 Static,
31 Animated,
32 Other(String),
33}
34
35impl EmoteImageFormat {
36 pub fn from_animated(animated: bool) -> Self {
37 if animated {
38 Self::Animated
39 } else {
40 Self::Static
41 }
42 }
43
44 pub fn parse(value: &str) -> Self {
45 match value {
46 "static" => Self::Static,
47 "animated" => Self::Animated,
48 other => Self::Other(other.to_string()),
49 }
50 }
51
52 pub fn as_str(&self) -> &str {
53 match self {
54 Self::Static => "static",
55 Self::Animated => "animated",
56 Self::Other(value) => value.as_str(),
57 }
58 }
59}
60
61#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
62pub enum EmoteThemeMode {
63 Light,
64 Dark,
65 Other(String),
66}
67
68impl EmoteThemeMode {
69 pub fn parse(value: &str) -> Self {
70 match value {
71 "light" => Self::Light,
72 "dark" => Self::Dark,
73 other => Self::Other(other.to_string()),
74 }
75 }
76
77 pub fn as_str(&self) -> &str {
78 match self {
79 Self::Light => "light",
80 Self::Dark => "dark",
81 Self::Other(value) => value.as_str(),
82 }
83 }
84}
85
86#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
87pub enum EmoteImageScale {
88 One,
89 Two,
90 Three,
91 Other(String),
92}
93
94impl EmoteImageScale {
95 pub fn parse(value: &str) -> Self {
96 match value {
97 "1" | "1.0" => Self::One,
98 "2" | "2.0" => Self::Two,
99 "3" | "3.0" => Self::Three,
100 other => Self::Other(other.to_string()),
101 }
102 }
103
104 pub fn as_str(&self) -> &str {
105 match self {
106 Self::One => "1.0",
107 Self::Two => "2.0",
108 Self::Three => "3.0",
109 Self::Other(value) => value.as_str(),
110 }
111 }
112}
113
114#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
115pub struct EmoteImage {
116 pub format: EmoteImageFormat,
117 pub theme_mode: EmoteThemeMode,
118 pub scale: EmoteImageScale,
119 pub url: String,
120}
121
122bitflags! {
123 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
124 pub struct EmoteModifierFlags: u32 {
125 const HIDDEN = 1 << 0;
126 const FLIP_X = 1 << 1;
127 const FLIP_Y = 1 << 2;
128 const GROW_X = 1 << 3;
129 const RAINBOW = 1 << 11;
130 const HYPER_RED = 1 << 12;
131 const HYPER_SHAKE = 1 << 13;
132 const CURSED = 1 << 14;
133 const JAM = 1 << 15;
134 const BOUNCE = 1 << 16;
135 }
136}
137
138#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
139pub struct EmoteModifier {
140 pub flags: EmoteModifierFlags,
141 pub mask_images: Vec<EmoteImage>,
142}
143
144#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
145pub struct Emote {
146 pub id: EmoteId,
147 pub code: String,
148 pub images: Vec<EmoteImage>,
149 pub modifier: Option<EmoteModifier>,
150}
151
152impl Emote {
153 pub fn new(id: EmoteId, code: impl Into<String>, images: Vec<EmoteImage>) -> Self {
154 Self {
155 id,
156 code: code.into(),
157 images,
158 modifier: None,
159 }
160 }
161
162 pub fn with_modifier(mut self, modifier: EmoteModifier) -> Self {
163 self.modifier = Some(modifier);
164 self
165 }
166
167 pub fn is_animated(&self) -> bool {
168 self.images
169 .iter()
170 .any(|image| matches!(image.format, EmoteImageFormat::Animated))
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn image_format_parse_round_trips_known_values() {
180 assert_eq!(EmoteImageFormat::parse("static").as_str(), "static");
181 assert_eq!(EmoteImageFormat::parse("animated").as_str(), "animated");
182 }
183
184 #[test]
185 fn image_scale_parses_provider_variants() {
186 assert_eq!(EmoteImageScale::parse("1"), EmoteImageScale::One);
187 assert_eq!(EmoteImageScale::parse("2.0"), EmoteImageScale::Two);
188 assert_eq!(EmoteImageScale::parse("3"), EmoteImageScale::Three);
189 }
190
191 #[test]
192 fn emote_modifier_preserves_flags() {
193 let emote = Emote::new(
194 EmoteId::new(EmoteProvider::SevenTv, "123"),
195 "Wave",
196 Vec::new(),
197 )
198 .with_modifier(EmoteModifier {
199 flags: EmoteModifierFlags::RAINBOW,
200 mask_images: Vec::new(),
201 });
202
203 assert!(!emote.is_animated());
204 assert!(
205 emote
206 .modifier
207 .as_ref()
208 .expect("modifier should exist")
209 .flags
210 .contains(EmoteModifierFlags::RAINBOW)
211 );
212 }
213}