twitch_types/
emote.rs

1manual_braid! {
2    /// A Badge set ID
3    pub struct BadgeSetId;
4    pub struct BadgeSetIdRef;
5}
6impl_extra!(BadgeSetId, BadgeSetIdRef);
7
8manual_braid! {
9    /// A channel chat badge ID
10    pub struct ChatBadgeId;
11    pub struct ChatBadgeIdRef;
12}
13impl_extra!(ChatBadgeId, ChatBadgeIdRef);
14
15manual_braid! {
16    /// A chat Emote ID
17    pub struct EmoteId;
18    pub struct EmoteIdRef;
19}
20impl_extra!(EmoteId, EmoteIdRef);
21
22impl EmoteIdRef {
23    /// Generates url for this emote.
24    ///
25    /// Generated URL will be `"https://static-cdn.jtvnw.net/emoticons/v2/{emote_id}/default/light/1.0"`
26    pub fn default_render(&self) -> String {
27        EmoteUrlBuilder {
28            id: self.into(),
29            animation_setting: None,
30            theme_mode: EmoteThemeMode::Light,
31            scale: EmoteScale::Size1_0,
32            template: EMOTE_V2_URL_TEMPLATE.into(),
33        }
34        .render()
35    }
36
37    /// Create a [`EmoteUrlBuilder`] for this emote
38    pub fn url(&self) -> EmoteUrlBuilder<'_> { EmoteUrlBuilder::new(self) }
39}
40
41/// Emote url template
42pub(crate) static EMOTE_V2_URL_TEMPLATE: &str =
43    "https://static-cdn.jtvnw.net/emoticons/v2/{{id}}/{{format}}/{{theme_mode}}/{{scale}}";
44
45/// Formats for an emote.
46#[derive(Debug, Clone, PartialEq, Eq)]
47#[cfg_attr(
48    feature = "serde",
49    derive(serde_derive::Serialize, serde_derive::Deserialize)
50)]
51#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
52pub enum EmoteAnimationSetting {
53    /// Static
54    Static,
55    /// Animated
56    Animated,
57}
58
59impl std::fmt::Display for EmoteAnimationSetting {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.write_str(match self {
62            EmoteAnimationSetting::Static => "static",
63            EmoteAnimationSetting::Animated => "animated",
64        })
65    }
66}
67
68/// Background themes available for an emote.
69#[derive(Debug, Clone, PartialEq, Eq)]
70#[cfg_attr(
71    feature = "serde",
72    derive(serde_derive::Serialize, serde_derive::Deserialize)
73)]
74#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
75pub enum EmoteThemeMode {
76    /// Light
77    Light,
78    /// Dark
79    Dark,
80}
81
82impl Default for EmoteThemeMode {
83    fn default() -> Self { Self::Light }
84}
85
86impl std::fmt::Display for EmoteThemeMode {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.write_str(match self {
89            EmoteThemeMode::Light => "light",
90            EmoteThemeMode::Dark => "dark",
91        })
92    }
93}
94
95/// Scales available for an emote.
96#[derive(Debug, Clone, PartialEq, Eq)]
97#[cfg_attr(
98    feature = "serde",
99    derive(serde_derive::Serialize, serde_derive::Deserialize)
100)]
101pub enum EmoteScale {
102    /// 1.0
103    #[cfg_attr(feature = "serde", serde(rename = "1.0"))]
104    Size1_0,
105    /// 2.0
106    #[cfg_attr(feature = "serde", serde(rename = "2.0"))]
107    Size2_0,
108    /// 3.0
109    #[cfg_attr(feature = "serde", serde(rename = "3.0"))]
110    Size3_0,
111}
112
113impl Default for EmoteScale {
114    fn default() -> Self { Self::Size1_0 }
115}
116
117impl std::fmt::Display for EmoteScale {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.write_str(match self {
120            EmoteScale::Size1_0 => "1.0",
121            EmoteScale::Size2_0 => "2.0",
122            EmoteScale::Size3_0 => "3.0",
123        })
124    }
125}
126
127/// Builder for [emote URLs](https://dev.twitch.tv/docs/irc/emotes#emote-cdn-url-format).
128///
129/// # Examples
130///
131/// ```rust
132/// # use twitch_types::EmoteId;
133/// let emote_id = EmoteId::from("emotesv2_dc24652ada1e4c84a5e3ceebae4de709");
134/// assert_eq!(emote_id.url().size_3x().dark_mode().render(), "https://static-cdn.jtvnw.net/emoticons/v2/emotesv2_dc24652ada1e4c84a5e3ceebae4de709/default/dark/3.0")
135/// ```
136#[derive(Debug, Clone)]
137pub struct EmoteUrlBuilder<'a> {
138    pub(crate) id: std::borrow::Cow<'a, EmoteIdRef>,
139    pub(crate) animation_setting: Option<EmoteAnimationSetting>,
140    pub(crate) theme_mode: EmoteThemeMode,
141    pub(crate) scale: EmoteScale,
142    pub(crate) template: std::borrow::Cow<'a, str>,
143}
144
145impl EmoteUrlBuilder<'_> {
146    // FIXME: AsRef
147    /// Construct a new [`EmoteUrlBuilder`] from a [`EmoteId`]
148    ///
149    /// Defaults to `1.0` scale, `default` animation and `light` theme.
150    pub fn new(id: &EmoteIdRef) -> EmoteUrlBuilder<'_> {
151        EmoteUrlBuilder {
152            id: id.into(),
153            animation_setting: <_>::default(),
154            theme_mode: <_>::default(),
155            scale: <_>::default(),
156            template: EMOTE_V2_URL_TEMPLATE.into(),
157        }
158    }
159
160    /// Set size to 1.0
161    pub fn size_1x(mut self) -> Self {
162        self.scale = EmoteScale::Size1_0;
163        self
164    }
165
166    /// Set size to 2.0
167    pub fn size_2x(mut self) -> Self {
168        self.scale = EmoteScale::Size2_0;
169        self
170    }
171
172    /// Set size to 3.0
173    pub fn size_3x(mut self) -> Self {
174        self.scale = EmoteScale::Size3_0;
175        self
176    }
177
178    /// Set theme to dark mode
179    pub fn dark_mode(mut self) -> Self {
180        self.theme_mode = EmoteThemeMode::Dark;
181        self
182    }
183
184    /// Set theme to light mode
185    pub fn light_mode(mut self) -> Self {
186        self.theme_mode = EmoteThemeMode::Light;
187        self
188    }
189
190    /// Set animation mode to default
191    pub fn animation_default(mut self) -> Self {
192        self.animation_setting = None;
193        self
194    }
195
196    /// Set animation mode to static
197    pub fn animation_static(mut self) -> Self {
198        self.animation_setting = Some(EmoteAnimationSetting::Static);
199        self
200    }
201
202    /// Set animation mode to animate
203    pub fn animation_animated(mut self) -> Self {
204        self.animation_setting = Some(EmoteAnimationSetting::Animated);
205        self
206    }
207
208    /// Create the URL for this emote.
209    pub fn render(self) -> String {
210        if self.template != "https://static-cdn.jtvnw.net/emoticons/v2/{{id}}/{{format}}/{{theme_mode}}/{{scale}}" {
211            let custom_template = |builder: &EmoteUrlBuilder| -> Option<String> {
212                let mut template = self.template.clone().into_owned();
213                let emote_id_range = template.find("{{id}}")?;
214                template.replace_range(emote_id_range..emote_id_range+"{{id}}".len(), builder.id.as_str());
215                let format_range = template.find("{{format}}")?;
216                template.replace_range(format_range..format_range+"{{format}}".len(), &builder.animation_setting.as_ref().map(|s| s.to_string()).unwrap_or_else(|| String::from("default")));
217                let theme_mode_range = template.find("{{theme_mode}}")?;
218                template.replace_range(theme_mode_range..theme_mode_range+"{{theme_mode}}".len(), &builder.theme_mode.to_string());
219                let scale_range = template.find("{{scale}}")?;
220                template.replace_range(scale_range..scale_range+"{{scale}}".len(), &builder.scale.to_string());
221                if template.contains("{{") || template.contains("}}") {
222                    None
223                } else {
224                    Some(template)
225                }
226            };
227            if let Some(template) = custom_template(&self) {
228                return template
229            } else {
230                #[cfg(feature = "tracing")]
231                tracing::warn!(template = %self.template, "emote builder was supplied an invalid or unknown template url, falling back to standard builder");
232            }
233        }
234        // fallback to known working template
235        format!("https://static-cdn.jtvnw.net/emoticons/v2/{emote_id}/{animation_setting}/{theme_mode}/{scale}",
236            emote_id = self.id,
237            animation_setting = self.animation_setting.as_ref().map(|s| s.to_string()).unwrap_or_else(|| String::from("default")),
238            theme_mode = self.theme_mode,
239            scale = self.scale,
240        )
241    }
242}
243
244manual_braid! {
245    /// An Emote Set ID
246    pub struct EmoteSetId;
247    pub struct EmoteSetIdRef;
248}
249impl_extra!(EmoteSetId, EmoteSetIdRef);
250
251/// An emote index as defined by eventsub, similar to IRC `emotes` twitch tag.
252#[derive(PartialEq, Eq, Debug, Clone)]
253#[cfg_attr(
254    feature = "serde",
255    derive(serde_derive::Serialize, serde_derive::Deserialize)
256)]
257#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
258#[non_exhaustive]
259pub struct EmoteOccurrence {
260    /// The index of where the Emote starts in the text.
261    pub begin: i64,
262    /// The index of where the Emote ends in the text.
263    pub end: i64,
264    /// The emote ID.
265    pub id: EmoteId,
266}
267
268impl std::fmt::Display for EmoteOccurrence {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        write!(f, "{}:{}-{}", self.id, self.begin, self.end)
271    }
272}
273
274/// An emote index as defined by eventsub, similar to IRC `emotes` twitch tag.
275#[deprecated(since = "0.4.8", note = "Use EmoteOccurrence instead")]
276pub type ResubscriptionEmote = EmoteOccurrence;
277
278/// Links to the same image of different sizes
279#[derive(Clone, Debug, PartialEq, Eq)]
280#[cfg_attr(
281    feature = "serde",
282    derive(serde_derive::Serialize, serde_derive::Deserialize)
283)]
284#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
285#[non_exhaustive]
286pub struct Image {
287    /// URL to png of size 28x28
288    pub url_1x: String,
289    /// URL to png of size 56x56
290    pub url_2x: String,
291    /// URL to png of size 112x112
292    pub url_4x: String,
293}