1use serde::{Deserialize, Serialize};
4
5#[aliri_braid::braid(serde)]
7pub struct UserId;
8
9#[aliri_braid::braid(serde)]
11pub struct RewardId;
12
13#[aliri_braid::braid(serde)]
15pub struct RedemptionId;
16
17pub type UserName = Nickname;
19
20pub type UserNameRef = NicknameRef;
22
23#[aliri_braid::braid(serde)]
25pub struct DisplayName;
26
27#[aliri_braid::braid(serde)]
29pub struct Nickname;
30
31#[aliri_braid::braid(serde, validator)]
33pub struct Timestamp;
34
35impl aliri_braid::Validator for Timestamp {
36 type Error = TimestampParseError;
37
38 fn validate(s: &str) -> Result<(), Self::Error> {
39 #[cfg(feature = "time")]
40 {
41 let _ = time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)?;
42 Ok(())
43 }
44 #[cfg(not(feature = "time"))]
45 {
46 if !s.chars().all(|c| {
48 c.is_numeric()
49 || c == 'T'
50 || c == 'Z'
51 || c == '+'
52 || c == '.'
53 || c == '-'
54 || c == ':'
55 }) {
56 return Err(TimestampParseError::invalid());
57 }
58 if let Some(i) = s.find('T') {
60 if i < 1 {
62 return Err(TimestampParseError::invalid());
63 };
64 let (full_date, full_time) = s.split_at(i);
65 if full_date.len() != "1900-00-00".len() {
66 return Err(TimestampParseError::invalid_s(full_date));
67 }
68 if !full_date.chars().all(|c| c.is_numeric() || c == '-') {
69 return Err(TimestampParseError::invalid_s(full_date));
70 }
71 let partial_time = if let Some(stripped) = full_time.strip_suffix('Z') {
72 stripped
73 } else {
74 return Err(TimestampParseError::Other("unsupported non-UTC timestamp, enable the `time` feature in `twitch_api2` to enable parsing these"));
75 };
76 if 2 != partial_time
77 .chars()
78 .into_iter()
79 .filter(|&b| b == ':')
80 .count()
81 {
82 return Err(TimestampParseError::invalid_s(partial_time));
83 };
84 if !partial_time.contains('.') && partial_time.len() != "T00:00:00".len() {
85 return Err(TimestampParseError::invalid_s(partial_time));
86 } else if partial_time.contains('.') {
87 let mut i = partial_time.split('.');
88 if !i
90 .next()
91 .map(|s| s.len() == "T00:00:00".len())
92 .unwrap_or_default()
93 {
94 return Err(TimestampParseError::invalid_s(partial_time));
95 }
96 }
97 } else {
98 return Err(TimestampParseError::invalid());
99 }
100 Ok(())
101 }
102 }
103}
104
105#[derive(Debug, thiserror::Error, displaydoc::Display)]
107#[ignore_extra_doc_attributes]
108#[non_exhaustive]
109pub enum TimestampParseError {
110 #[cfg(feature = "time")]
112 #[cfg_attr(nightly, doc(cfg(feature = "time")))]
113 TimeError(#[from] time::error::Parse),
114 #[cfg(feature = "time")]
116 #[cfg_attr(nightly, doc(cfg(feature = "time")))]
117 TimeFormatError(#[from] time::error::Format),
118 Other(&'static str),
120 InvalidFormat {
122 location: &'static std::panic::Location<'static>,
124 s: Option<String>,
126 },
127}
128
129impl TimestampParseError {
130 #[cfg(not(feature = "time"))]
131 #[track_caller]
132 fn invalid() -> Self {
133 Self::InvalidFormat {
134 location: std::panic::Location::caller(),
135 s: None,
136 }
137 }
138
139 #[cfg(not(feature = "time"))]
140 #[track_caller]
141 fn invalid_s(s: &str) -> Self {
142 Self::InvalidFormat {
143 location: std::panic::Location::caller(),
144 s: Some(s.to_string()),
145 }
146 }
147}
148
149impl Timestamp {
150 fn set_time(&mut self, hours: u8, minutes: u8, seconds: u8) {
156 #[cfg(feature = "time")]
157 {
158 use std::convert::TryInto;
159 let _ = std::mem::replace(
160 self,
161 self.to_fixed_offset()
162 .replace_time(
163 time::Time::from_hms(hours, minutes, seconds)
164 .expect("could not create time"),
165 )
166 .try_into()
167 .expect("could not make timestamp"),
168 );
169 }
170 #[cfg(not(feature = "time"))]
171 {
172 const ERROR_MSG: &str = "malformed timestamp";
173 assert!(hours < 24);
174 assert!(minutes < 60);
175 assert!(seconds < 60);
176
177 #[inline]
178 fn replace_len2(s: &mut str, replace: &str) {
179 assert!(replace.as_bytes().len() == 2);
180 assert!(s.as_bytes().len() == 2);
181
182 let replace = replace.as_bytes();
183 let b = unsafe { s.as_bytes_mut() };
189 b[0] = replace[0];
190 b[1] = replace[1];
191 }
192 let t = self.0.find('T').expect(ERROR_MSG);
193 let partial_time: &mut str = &mut self.0[t + 1..];
194 let mut matches = partial_time.match_indices(':');
196 let (h, m, s) = (
197 0,
198 matches.next().expect(ERROR_MSG).0 + 1,
199 matches.next().expect(ERROR_MSG).0 + 1,
200 );
201 assert!(matches.next().is_none());
202 partial_time
204 .get_mut(h..h + 2)
205 .map(|s| replace_len2(s, &format!("{:02}", hours)))
206 .expect(ERROR_MSG);
207 partial_time
208 .get_mut(m..m + 2)
209 .map(|s| replace_len2(s, &format!("{:02}", minutes)))
210 .expect(ERROR_MSG);
211 partial_time
212 .get_mut(s..s + 2)
213 .map(|s| replace_len2(s, &format!("{:02}", seconds)))
214 .expect(ERROR_MSG);
215 }
216 }
217}
218
219#[cfg(feature = "time")]
220#[cfg_attr(nightly, doc(cfg(feature = "time")))]
221impl Timestamp {
222 pub fn now() -> Timestamp {
224 use std::convert::TryInto;
225 time::OffsetDateTime::now_utc()
226 .try_into()
227 .expect("could not make timestamp")
228 }
229
230 pub fn today() -> Timestamp {
232 use std::convert::TryInto;
233 time::OffsetDateTime::now_utc()
234 .replace_time(time::Time::MIDNIGHT)
235 .try_into()
236 .expect("could not make timestamp")
237 }
238}
239
240impl TimestampRef {
241 #[allow(unreachable_code)]
255 pub fn normalize(&'_ self) -> Result<std::borrow::Cow<'_, TimestampRef>, TimestampParseError> {
256 let s = self.as_str();
257 if s.ends_with('Z') {
258 Ok(self.into())
259 } else {
260 #[cfg(feature = "time")]
261 {
262 use std::convert::TryInto;
263 let utc = self.to_utc();
264 return Ok(std::borrow::Cow::Owned(utc.try_into()?));
265 }
266 panic!("non `Z` timestamps are not possible to use without the `time` feature enabled for `twitch_api2`")
267 }
268 }
269
270 pub fn is_before<T>(&self, other: &T) -> bool
282 where Self: PartialOrd<T> {
283 self < other
284 }
285
286 pub fn to_day(&self) -> Timestamp {
297 let mut c = self.to_owned();
298 c.set_time(0, 0, 0);
299 c
300 }
301}
302
303#[cfg(feature = "time")]
304#[cfg_attr(nightly, doc(cfg(feature = "time")))]
305impl TimestampRef {
306 pub fn to_utc(&self) -> time::OffsetDateTime {
312 self.to_fixed_offset().to_offset(time::UtcOffset::UTC)
313 }
314
315 pub fn to_fixed_offset(&self) -> time::OffsetDateTime {
321 time::OffsetDateTime::parse(&self.0, &time::format_description::well_known::Rfc3339)
322 .expect("this should never fail")
323 }
324}
325
326impl PartialOrd for Timestamp {
327 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
328 let this: &TimestampRef = self.as_ref();
330 let other: &TimestampRef = other.as_ref();
331 this.partial_cmp(other)
332 }
333}
334
335impl PartialOrd<Timestamp> for TimestampRef {
336 fn partial_cmp(&self, other: &Timestamp) -> Option<std::cmp::Ordering> {
337 let other: &TimestampRef = other.as_ref();
339 self.partial_cmp(other)
340 }
341}
342
343impl PartialOrd for TimestampRef {
344 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
345 let this = self
349 .normalize()
350 .expect("normalization failed, this is a bug");
351 let other = other
352 .normalize()
353 .expect("normalization of other failed, this is a bug");
354 #[allow(clippy::if_same_then_else)]
356 if this.as_ref().as_str().contains('.') ^ other.as_ref().as_str().contains('.') {
357 #[cfg(feature = "tracing")]
358 tracing::trace!("comparing two `Timestamps` with differing punctuation");
359 return None;
360 } else if this.0.len() != other.0.len() {
361 #[cfg(feature = "tracing")]
362 tracing::trace!("comparing two `Timestamps` with differing length");
363 return None;
364 }
365 this.as_str().partial_cmp(other.as_str())
366 }
367}
368
369#[cfg(feature = "time")]
370#[cfg_attr(nightly, doc(cfg(feature = "time")))]
371impl PartialEq<time::OffsetDateTime> for Timestamp {
372 fn eq(&self, other: &time::OffsetDateTime) -> bool {
373 let this: &TimestampRef = self.as_ref();
375 this.eq(other)
376 }
377}
378
379#[cfg(feature = "time")]
380#[cfg_attr(nightly, doc(cfg(feature = "time")))]
381impl PartialOrd<time::OffsetDateTime> for Timestamp {
382 fn partial_cmp(&self, other: &time::OffsetDateTime) -> Option<std::cmp::Ordering> {
383 let this: &TimestampRef = self.as_ref();
385 this.partial_cmp(other)
386 }
387}
388
389#[cfg(feature = "time")]
390#[cfg_attr(nightly, doc(cfg(feature = "time")))]
391impl PartialEq<time::OffsetDateTime> for TimestampRef {
392 fn eq(&self, other: &time::OffsetDateTime) -> bool { &self.to_utc() == other }
393}
394
395#[cfg(feature = "time")]
396#[cfg_attr(nightly, doc(cfg(feature = "time")))]
397impl PartialOrd<time::OffsetDateTime> for TimestampRef {
398 fn partial_cmp(&self, other: &time::OffsetDateTime) -> Option<std::cmp::Ordering> {
399 self.to_utc().partial_cmp(other)
400 }
401}
402
403#[cfg(feature = "time")]
404#[cfg_attr(nightly, doc(cfg(feature = "time")))]
405impl std::convert::TryFrom<time::OffsetDateTime> for Timestamp {
406 type Error = time::error::Format;
407
408 fn try_from(value: time::OffsetDateTime) -> Result<Self, Self::Error> {
409 Ok(Timestamp(
410 value.format(&time::format_description::well_known::Rfc3339)?,
411 ))
412 }
413}
414
415#[aliri_braid::braid(serde)]
417pub struct BlockedTermId;
418
419#[aliri_braid::braid(serde)]
421pub struct CategoryId;
422
423#[aliri_braid::braid(serde)]
425pub struct TagId;
426
427#[aliri_braid::braid(serde)]
429pub struct VideoId;
430
431#[aliri_braid::braid(serde)]
433pub struct EventSubId;
434
435#[aliri_braid::braid(serde)]
437pub struct TeamId;
438
439#[aliri_braid::braid(serde)]
441pub struct StreamId;
442
443#[aliri_braid::braid(serde)]
445pub struct MsgId;
446
447#[aliri_braid::braid(serde)]
449pub struct PollId;
450
451#[aliri_braid::braid(serde)]
453pub struct PollChoiceId;
454
455#[aliri_braid::braid(serde)]
457pub struct PredictionId;
458
459#[aliri_braid::braid(serde)]
461pub struct PredictionOutcomeId;
462
463#[aliri_braid::braid(serde)]
465pub struct BadgeSetId;
466
467#[aliri_braid::braid(serde)]
469pub struct ChatBadgeId;
470
471#[aliri_braid::braid(serde)]
473pub struct EmoteId;
474
475impl EmoteIdRef {
476 pub fn default_render(&self) -> String {
480 EmoteUrlBuilder {
481 id: self.into(),
482 animation_setting: None,
483 theme_mode: EmoteThemeMode::Light,
484 scale: EmoteScale::Size1_0,
485 template: EMOTE_V2_URL_TEMPLATE.into(),
486 }
487 .render()
488 }
489
490 pub fn url(&self) -> EmoteUrlBuilder<'_> { EmoteUrlBuilder::new(self) }
492}
493
494pub(crate) static EMOTE_V2_URL_TEMPLATE: &str =
495 "https://static-cdn.jtvnw.net/emoticons/v2/{{id}}/{{format}}/{{theme_mode}}/{{scale}}";
496
497#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
499#[serde(rename_all = "lowercase")]
500pub enum EmoteAnimationSetting {
501 Static,
503 Animated,
505}
506
507impl std::fmt::Display for EmoteAnimationSetting {
508 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.serialize(f) }
509}
510
511#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
513#[serde(rename_all = "lowercase")]
514pub enum EmoteThemeMode {
515 Light,
517 Dark,
519}
520
521impl Default for EmoteThemeMode {
522 fn default() -> Self { Self::Light }
523}
524
525impl std::fmt::Display for EmoteThemeMode {
526 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.serialize(f) }
527}
528
529#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
531pub enum EmoteScale {
532 #[serde(rename = "1.0")]
534 Size1_0,
535 #[serde(rename = "2.0")]
537 Size2_0,
538 #[serde(rename = "3.0")]
540 Size3_0,
541}
542
543impl Default for EmoteScale {
544 fn default() -> Self { Self::Size1_0 }
545}
546
547impl std::fmt::Display for EmoteScale {
548 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.serialize(f) }
549}
550
551#[derive(Debug, Clone)]
561pub struct EmoteUrlBuilder<'a> {
562 pub(crate) id: std::borrow::Cow<'a, EmoteIdRef>,
563 pub(crate) animation_setting: Option<EmoteAnimationSetting>,
564 pub(crate) theme_mode: EmoteThemeMode,
565 pub(crate) scale: EmoteScale,
566 pub(crate) template: std::borrow::Cow<'a, str>,
567}
568
569impl EmoteUrlBuilder<'_> {
570 pub fn new(id: &EmoteIdRef) -> EmoteUrlBuilder<'_> {
575 EmoteUrlBuilder {
576 id: id.into(),
577 animation_setting: <_>::default(),
578 theme_mode: <_>::default(),
579 scale: <_>::default(),
580 template: EMOTE_V2_URL_TEMPLATE.into(),
581 }
582 }
583
584 pub fn size_1x(mut self) -> Self {
586 self.scale = EmoteScale::Size1_0;
587 self
588 }
589
590 pub fn size_2x(mut self) -> Self {
592 self.scale = EmoteScale::Size2_0;
593 self
594 }
595
596 pub fn size_3x(mut self) -> Self {
598 self.scale = EmoteScale::Size3_0;
599 self
600 }
601
602 pub fn dark_mode(mut self) -> Self {
604 self.theme_mode = EmoteThemeMode::Dark;
605 self
606 }
607
608 pub fn light_mode(mut self) -> Self {
610 self.theme_mode = EmoteThemeMode::Light;
611 self
612 }
613
614 pub fn animation_default(mut self) -> Self {
616 self.animation_setting = None;
617 self
618 }
619
620 pub fn animation_static(mut self) -> Self {
622 self.animation_setting = Some(EmoteAnimationSetting::Static);
623 self
624 }
625
626 pub fn animation_animated(mut self) -> Self {
628 self.animation_setting = Some(EmoteAnimationSetting::Animated);
629 self
630 }
631
632 pub fn render(self) -> String {
634 if self.template != "https://static-cdn.jtvnw.net/emoticons/v2/{{id}}/{{format}}/{{theme_mode}}/{{scale}}" {
635 let custom_template = |builder: &EmoteUrlBuilder| -> Option<String> {
636 let mut template = self.template.clone().into_owned();
637 let emote_id_range = template.find("{{id}}")?;
638 eprintln!("id");
639 template.replace_range(emote_id_range..emote_id_range+"{{id}}".len(), builder.id.as_str());
640 eprintln!("format");
641 let format_range = template.find("{{format}}")?;
642 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")));
643 eprintln!("theme_mode");
644 let theme_mode_range = template.find("{{theme_mode}}")?;
645 template.replace_range(theme_mode_range..theme_mode_range+"{{theme_mode}}".len(), &builder.theme_mode.to_string());
646 eprintln!("scale");
647 let scale_range = template.find("{{scale}}")?;
648 template.replace_range(scale_range..scale_range+"{{scale}}".len(), &builder.scale.to_string());
649 if template.contains("{{") || template.contains("}}") {
650 None
651 } else {
652 Some(template)
653 }
654 };
655 if let Some(template) = custom_template(&self) {
656 return template
657 } else {
658 #[cfg(feature = "tracing")]
659 tracing::warn!(template = %self.template, "emote builder was supplied an invalid or unknown template url, falling back to standard builder");
660 }
661 }
662 format!("https://static-cdn.jtvnw.net/emoticons/v2/{emote_id}/{animation_setting}/{theme_mode}/{scale}",
664 emote_id = self.id,
665 animation_setting = self.animation_setting.as_ref().map(|s| s.to_string()).unwrap_or_else(|| String::from("default")),
666 theme_mode = self.theme_mode,
667 scale = self.scale,
668 )
669 }
670}
671
672#[aliri_braid::braid(serde)]
674pub struct EmoteSetId;
675
676#[aliri_braid::braid(serde)]
678pub struct StreamSegmentId;
679
680#[aliri_braid::braid(serde)]
682pub struct HypeTrainId;
683
684#[aliri_braid::braid(serde)]
686pub struct CreatorGoalId;
687
688#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)]
690#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
691#[non_exhaustive]
692pub struct ResubscriptionEmote {
693 pub begin: i64,
695 pub end: i64,
697 pub id: EmoteId,
699}
700
701impl std::fmt::Display for ResubscriptionEmote {
702 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
703 write!(f, "{}:{}-{}", self.id, self.begin, self.end)
704 }
705}
706
707#[derive(PartialEq, Deserialize, Serialize, Debug, Clone)]
709#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
710#[non_exhaustive]
711pub struct TwitchCategory {
712 pub box_art_url: String,
714 pub id: CategoryId,
716 pub name: String,
718}
719
720#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
722#[serde(field_identifier)]
723pub enum SubscriptionTier {
724 #[serde(rename = "1000")]
726 Tier1,
727 #[serde(rename = "2000")]
729 Tier2,
730 #[serde(rename = "3000")]
732 Tier3,
733 Prime,
735 Other(String),
737}
738
739impl Serialize for SubscriptionTier {
740 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
741 where S: serde::Serializer {
742 serializer.serialize_str(match self {
743 SubscriptionTier::Tier1 => "1000",
744 SubscriptionTier::Tier2 => "2000",
745 SubscriptionTier::Tier3 => "3000",
746 SubscriptionTier::Prime => "Prime",
747 SubscriptionTier::Other(o) => o,
748 })
749 }
750}
751
752#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
754pub enum BroadcasterType {
755 #[serde(rename = "partner")]
757 Partner,
758 #[serde(rename = "affiliate")]
760 Affiliate,
761 #[serde(other)]
763 None,
764}
765
766impl Serialize for BroadcasterType {
767 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
768 where S: serde::Serializer {
769 serializer.serialize_str(match self {
770 BroadcasterType::Partner => "partner",
771 BroadcasterType::Affiliate => "affiliate",
772 BroadcasterType::None => "",
773 })
774 }
775}
776
777#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
779pub enum UserType {
780 #[serde(rename = "staff")]
782 Staff,
783 #[serde(rename = "admin")]
785 Admin,
786 #[serde(rename = "global_mod")]
788 GlobalMod,
789 #[serde(other)]
791 None,
792}
793
794impl Serialize for UserType {
795 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
796 where S: serde::Serializer {
797 serializer.serialize_str(match self {
798 UserType::Staff => "staff",
799 UserType::Admin => "admin",
800 UserType::GlobalMod => "global_mod",
801 UserType::None => "",
802 })
803 }
804}
805
806#[derive(PartialEq, Deserialize, Serialize, Clone, Debug)]
808#[serde(rename_all = "lowercase")]
809pub enum VideoPeriod {
810 All,
812 Day,
814 Week,
816 Month,
818}
819
820#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug)]
822#[serde(rename_all = "snake_case")]
823pub enum VideoType {
824 Live,
826 Playlist,
829 Upload,
831 Archive,
833 Highlight,
835 Premiere,
837 Rerun,
839 WatchParty,
841 WatchPartyPremiere,
843 WatchPartyRerun,
845}
846
847#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug)]
849#[serde(rename_all = "lowercase")]
850pub enum VideoPrivacy {
851 Public,
853 Private,
855}
856
857#[derive(
859 displaydoc::Display,
860 serde_repr::Serialize_repr,
861 serde_repr::Deserialize_repr,
862 Debug,
863 Clone,
864 PartialEq,
865 Eq,
866)]
867#[repr(u64)]
868#[non_exhaustive]
869pub enum CommercialLength {
870 Length30 = 30,
872 Length60 = 60,
874 Length90 = 90,
876 Length120 = 120,
878 Length150 = 150,
880 Length180 = 180,
882}
883
884impl std::convert::TryFrom<u64> for CommercialLength {
885 type Error = CommercialLengthParseError;
886
887 fn try_from(l: u64) -> Result<Self, Self::Error> {
888 match l {
889 30 => Ok(CommercialLength::Length30),
890 60 => Ok(CommercialLength::Length60),
891 90 => Ok(CommercialLength::Length90),
892 120 => Ok(CommercialLength::Length120),
893 150 => Ok(CommercialLength::Length150),
894 180 => Ok(CommercialLength::Length180),
895 other => Err(CommercialLengthParseError::InvalidLength(other)),
896 }
897 }
898}
899
900#[derive(thiserror::Error, Debug, displaydoc::Display)]
902pub enum CommercialLengthParseError {
903 InvalidLength(u64),
905}
906
907#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
909#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
910pub struct User {
911 #[serde(alias = "user_id")]
913 pub id: UserId,
914 #[serde(alias = "user_login")]
916 pub login: UserName,
917 #[serde(alias = "user_display_name", alias = "user_name")]
919 pub display_name: DisplayName,
920 #[serde(default)]
921 pub profile_image_url: Option<String>,
923}
924
925#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
927#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
928#[non_exhaustive]
929pub struct Image {
930 pub url_1x: String,
932 pub url_2x: String,
934 pub url_4x: String,
936}
937
938#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
940#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
941#[non_exhaustive]
942pub struct GlobalCooldown {
943 pub is_enabled: bool,
945 #[serde(alias = "seconds")]
947 pub global_cooldown_seconds: u32,
948}
949
950#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
952#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
953#[serde(untagged)]
954#[non_exhaustive]
955pub enum Max {
956 MaxPerStream {
958 is_enabled: bool,
960 #[serde(alias = "value")]
962 max_per_stream: u32,
963 },
964 MaxPerUserPerStream {
966 is_enabled: bool,
968 #[serde(alias = "value")]
970 max_per_user_per_stream: u32,
971 },
972}
973
974#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
976#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
977#[non_exhaustive]
978pub struct PollChoice {
979 pub id: String,
981 pub title: String,
983 pub votes: Option<i64>,
985 pub channel_points_votes: Option<i64>,
987 pub bits_votes: Option<i64>,
989}
990
991#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)]
994#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
995#[serde(rename_all = "UPPERCASE")]
996#[non_exhaustive]
997pub enum PollStatus {
998 #[serde(alias = "active")]
1000 Active,
1001 #[serde(alias = "completed")]
1003 Completed,
1004 #[serde(alias = "terminated")]
1006 Terminated,
1007 #[serde(alias = "archived")]
1009 Archived,
1010 #[serde(alias = "moderated")]
1012 Moderated,
1013 #[serde(alias = "invalid")]
1015 Invalid,
1016}
1017
1018#[derive(PartialEq, Deserialize, Serialize, Debug, Clone)]
1021#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
1022#[serde(rename_all = "UPPERCASE")]
1023#[non_exhaustive]
1024pub enum PredictionStatus {
1025 #[serde(alias = "resolved")]
1027 Resolved,
1028 #[serde(alias = "active")]
1030 Active,
1031 #[serde(alias = "canceled")]
1033 Canceled,
1034 #[serde(alias = "locked")]
1036 Locked,
1037}
1038
1039#[derive(PartialEq, Deserialize, Serialize, Debug, Clone)]
1041#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
1042#[non_exhaustive]
1043pub struct PredictionOutcome {
1044 pub id: String,
1046 pub title: String,
1048 pub users: Option<i64>,
1050 pub channel_points: Option<i64>,
1052 pub top_predictors: Option<Vec<PredictionTopPredictors>>,
1054 pub color: String,
1056}
1057
1058#[derive(PartialEq, Deserialize, Serialize, Debug, Clone)]
1061#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
1062#[non_exhaustive]
1063pub struct PredictionTopPredictors {
1064 #[serde(alias = "user_id")]
1066 pub id: UserId,
1067 #[serde(alias = "user_name")]
1069 pub name: DisplayName,
1070 #[serde(alias = "user_login")]
1072 pub login: UserName,
1073 pub channel_points_used: i64,
1075 pub channel_points_won: Option<i64>,
1079}
1080
1081#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)]
1083#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
1084#[serde(rename_all = "UPPERCASE")]
1085#[non_exhaustive]
1086pub enum AutomodStatus {
1087 Pending,
1089 Allowed,
1091 Denied,
1093 Expired,
1095}
1096
1097#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)]
1099#[serde(rename_all = "lowercase")]
1100#[non_exhaustive]
1101pub enum CreatorGoalType {
1102 Follower,
1104 Subscription,
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110 use super::*;
1111
1112 #[test]
1113 pub fn time_test() {
1114 let mut time1 = Timestamp::new("2021-11-11T10:00:00Z").unwrap();
1115 time1.set_time(10, 0, 32);
1116 let time2 = Timestamp::new("2021-11-10T10:00:00Z").unwrap();
1117 assert!(time2.is_before(&time1));
1118 dbg!(time1.normalize().unwrap());
1119 #[cfg(feature = "time")]
1120 let time = Timestamp::new("2021-11-11T13:37:00-01:00").unwrap();
1121 #[cfg(feature = "time")]
1122 dbg!(time.normalize().unwrap());
1123 }
1124}