use serde::{Deserialize, Serialize};
#[aliri_braid::braid(serde)]
pub struct UserId;
#[aliri_braid::braid(serde)]
pub struct RewardId;
#[aliri_braid::braid(serde)]
pub struct RedemptionId;
pub type UserName = Nickname;
pub type UserNameRef = NicknameRef;
#[aliri_braid::braid(serde)]
pub struct DisplayName;
#[aliri_braid::braid(serde)]
pub struct Nickname;
#[aliri_braid::braid(serde, validator)]
pub struct Timestamp;
impl aliri_braid::Validator for Timestamp {
type Error = TimestampParseError;
fn validate(s: &str) -> Result<(), Self::Error> {
#[cfg(feature = "time")]
{
let _ = time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)?;
Ok(())
}
#[cfg(not(feature = "time"))]
{
if !s.chars().all(|c| {
c.is_numeric()
|| c == 'T'
|| c == 'Z'
|| c == '+'
|| c == '.'
|| c == '-'
|| c == ':'
}) {
return Err(TimestampParseError::invalid());
}
if let Some(i) = s.find('T') {
if i < 1 {
return Err(TimestampParseError::invalid());
};
let (full_date, full_time) = s.split_at(i);
if full_date.len() != "1900-00-00".len() {
return Err(TimestampParseError::invalid_s(full_date));
}
if !full_date.chars().all(|c| c.is_numeric() || c == '-') {
return Err(TimestampParseError::invalid_s(full_date));
}
let partial_time = if let Some(stripped) = full_time.strip_suffix('Z') {
stripped
} else {
return Err(TimestampParseError::Other("unsupported non-UTC timestamp, enable the `time` feature in `twitch_api2` to enable parsing these"));
};
if 2 != partial_time
.chars()
.into_iter()
.filter(|&b| b == ':')
.count()
{
return Err(TimestampParseError::invalid_s(partial_time));
};
if !partial_time.contains('.') && partial_time.len() != "T00:00:00".len() {
return Err(TimestampParseError::invalid_s(partial_time));
} else if partial_time.contains('.') {
let mut i = partial_time.split('.');
if !i
.next()
.map(|s| s.len() == "T00:00:00".len())
.unwrap_or_default()
{
return Err(TimestampParseError::invalid_s(partial_time));
}
}
} else {
return Err(TimestampParseError::invalid());
}
Ok(())
}
}
}
#[derive(Debug, thiserror::Error, displaydoc::Display)]
#[ignore_extra_doc_attributes]
#[non_exhaustive]
pub enum TimestampParseError {
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
TimeError(#[from] time::error::Parse),
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
TimeFormatError(#[from] time::error::Format),
Other(&'static str),
InvalidFormat {
location: &'static std::panic::Location<'static>,
s: Option<String>,
},
}
impl TimestampParseError {
#[cfg(not(feature = "time"))]
#[track_caller]
fn invalid() -> Self {
Self::InvalidFormat {
location: std::panic::Location::caller(),
s: None,
}
}
#[cfg(not(feature = "time"))]
#[track_caller]
fn invalid_s(s: &str) -> Self {
Self::InvalidFormat {
location: std::panic::Location::caller(),
s: Some(s.to_string()),
}
}
}
impl Timestamp {
fn set_time(&mut self, hours: u8, minutes: u8, seconds: u8) {
#[cfg(feature = "time")]
{
use std::convert::TryInto;
let _ = std::mem::replace(
self,
self.to_fixed_offset()
.replace_time(
time::Time::from_hms(hours, minutes, seconds)
.expect("could not create time"),
)
.try_into()
.expect("could not make timestamp"),
);
}
#[cfg(not(feature = "time"))]
{
const ERROR_MSG: &str = "malformed timestamp";
assert!(hours < 24);
assert!(minutes < 60);
assert!(seconds < 60);
#[inline]
fn replace_len2(s: &mut str, replace: &str) {
assert!(replace.as_bytes().len() == 2);
assert!(s.as_bytes().len() == 2);
let replace = replace.as_bytes();
let b = unsafe { s.as_bytes_mut() };
b[0] = replace[0];
b[1] = replace[1];
}
let t = self.0.find('T').expect(ERROR_MSG);
let partial_time: &mut str = &mut self.0[t + 1..];
let mut matches = partial_time.match_indices(':');
let (h, m, s) = (
0,
matches.next().expect(ERROR_MSG).0 + 1,
matches.next().expect(ERROR_MSG).0 + 1,
);
assert!(matches.next().is_none());
partial_time
.get_mut(h..h + 2)
.map(|s| replace_len2(s, &format!("{:02}", hours)))
.expect(ERROR_MSG);
partial_time
.get_mut(m..m + 2)
.map(|s| replace_len2(s, &format!("{:02}", minutes)))
.expect(ERROR_MSG);
partial_time
.get_mut(s..s + 2)
.map(|s| replace_len2(s, &format!("{:02}", seconds)))
.expect(ERROR_MSG);
}
}
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl Timestamp {
pub fn now() -> Timestamp {
use std::convert::TryInto;
time::OffsetDateTime::now_utc()
.try_into()
.expect("could not make timestamp")
}
pub fn today() -> Timestamp {
use std::convert::TryInto;
time::OffsetDateTime::now_utc()
.replace_time(time::Time::MIDNIGHT)
.try_into()
.expect("could not make timestamp")
}
}
impl TimestampRef {
#[allow(unreachable_code)]
pub fn normalize(&'_ self) -> Result<std::borrow::Cow<'_, TimestampRef>, TimestampParseError> {
let s = self.as_str();
if s.ends_with('Z') {
Ok(self.into())
} else {
#[cfg(feature = "time")]
{
use std::convert::TryInto;
let utc = self.to_utc();
return Ok(std::borrow::Cow::Owned(utc.try_into()?));
}
panic!("non `Z` timestamps are not possible to use without the `time` feature enabled for `twitch_api2`")
}
}
pub fn is_before<T>(&self, other: &T) -> bool
where Self: PartialOrd<T> {
self < other
}
pub fn to_day(&self) -> Timestamp {
let mut c = self.to_owned();
c.set_time(0, 0, 0);
c
}
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl TimestampRef {
pub fn to_utc(&self) -> time::OffsetDateTime {
self.to_fixed_offset().to_offset(time::UtcOffset::UTC)
}
pub fn to_fixed_offset(&self) -> time::OffsetDateTime {
time::OffsetDateTime::parse(&self.0, &time::format_description::well_known::Rfc3339)
.expect("this should never fail")
}
}
impl PartialOrd for Timestamp {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let this: &TimestampRef = self.as_ref();
let other: &TimestampRef = other.as_ref();
this.partial_cmp(other)
}
}
impl PartialOrd<Timestamp> for TimestampRef {
fn partial_cmp(&self, other: &Timestamp) -> Option<std::cmp::Ordering> {
let other: &TimestampRef = other.as_ref();
self.partial_cmp(other)
}
}
impl PartialOrd for TimestampRef {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let this = self
.normalize()
.expect("normalization failed, this is a bug");
let other = other
.normalize()
.expect("normalization of other failed, this is a bug");
#[allow(clippy::if_same_then_else)]
if this.as_ref().as_str().contains('.') ^ other.as_ref().as_str().contains('.') {
#[cfg(feature = "tracing")]
tracing::trace!("comparing two `Timestamps` with differing punctuation");
return None;
} else if this.0.len() != other.0.len() {
#[cfg(feature = "tracing")]
tracing::trace!("comparing two `Timestamps` with differing length");
return None;
}
this.as_str().partial_cmp(other.as_str())
}
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl PartialEq<time::OffsetDateTime> for Timestamp {
fn eq(&self, other: &time::OffsetDateTime) -> bool {
let this: &TimestampRef = self.as_ref();
this.eq(other)
}
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl PartialOrd<time::OffsetDateTime> for Timestamp {
fn partial_cmp(&self, other: &time::OffsetDateTime) -> Option<std::cmp::Ordering> {
let this: &TimestampRef = self.as_ref();
this.partial_cmp(other)
}
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl PartialEq<time::OffsetDateTime> for TimestampRef {
fn eq(&self, other: &time::OffsetDateTime) -> bool { &self.to_utc() == other }
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl PartialOrd<time::OffsetDateTime> for TimestampRef {
fn partial_cmp(&self, other: &time::OffsetDateTime) -> Option<std::cmp::Ordering> {
self.to_utc().partial_cmp(other)
}
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl std::convert::TryFrom<time::OffsetDateTime> for Timestamp {
type Error = time::error::Format;
fn try_from(value: time::OffsetDateTime) -> Result<Self, Self::Error> {
Ok(Timestamp(
value.format(&time::format_description::well_known::Rfc3339)?,
))
}
}
#[aliri_braid::braid(serde)]
pub struct BlockedTermId;
#[aliri_braid::braid(serde)]
pub struct CategoryId;
#[aliri_braid::braid(serde)]
pub struct TagId;
#[aliri_braid::braid(serde)]
pub struct VideoId;
#[aliri_braid::braid(serde)]
pub struct EventSubId;
#[aliri_braid::braid(serde)]
pub struct TeamId;
#[aliri_braid::braid(serde)]
pub struct StreamId;
#[aliri_braid::braid(serde)]
pub struct MsgId;
#[aliri_braid::braid(serde)]
pub struct PollId;
#[aliri_braid::braid(serde)]
pub struct PollChoiceId;
#[aliri_braid::braid(serde)]
pub struct PredictionId;
#[aliri_braid::braid(serde)]
pub struct PredictionOutcomeId;
#[aliri_braid::braid(serde)]
pub struct BadgeSetId;
#[aliri_braid::braid(serde)]
pub struct ChatBadgeId;
#[aliri_braid::braid(serde)]
pub struct EmoteId;
impl EmoteIdRef {
pub fn default_render(&self) -> String {
EmoteUrlBuilder {
id: self.into(),
animation_setting: None,
theme_mode: EmoteThemeMode::Light,
scale: EmoteScale::Size1_0,
template: EMOTE_V2_URL_TEMPLATE.into(),
}
.render()
}
pub fn url(&self) -> EmoteUrlBuilder<'_> { EmoteUrlBuilder::new(self) }
}
pub(crate) static EMOTE_V2_URL_TEMPLATE: &str =
"https://static-cdn.jtvnw.net/emoticons/v2/{{id}}/{{format}}/{{theme_mode}}/{{scale}}";
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum EmoteAnimationSetting {
Static,
Animated,
}
impl std::fmt::Display for EmoteAnimationSetting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.serialize(f) }
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum EmoteThemeMode {
Light,
Dark,
}
impl Default for EmoteThemeMode {
fn default() -> Self { Self::Light }
}
impl std::fmt::Display for EmoteThemeMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.serialize(f) }
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub enum EmoteScale {
#[serde(rename = "1.0")]
Size1_0,
#[serde(rename = "2.0")]
Size2_0,
#[serde(rename = "3.0")]
Size3_0,
}
impl Default for EmoteScale {
fn default() -> Self { Self::Size1_0 }
}
impl std::fmt::Display for EmoteScale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.serialize(f) }
}
#[derive(Debug, Clone)]
pub struct EmoteUrlBuilder<'a> {
pub(crate) id: std::borrow::Cow<'a, EmoteIdRef>,
pub(crate) animation_setting: Option<EmoteAnimationSetting>,
pub(crate) theme_mode: EmoteThemeMode,
pub(crate) scale: EmoteScale,
pub(crate) template: std::borrow::Cow<'a, str>,
}
impl EmoteUrlBuilder<'_> {
pub fn new(id: &EmoteIdRef) -> EmoteUrlBuilder<'_> {
EmoteUrlBuilder {
id: id.into(),
animation_setting: <_>::default(),
theme_mode: <_>::default(),
scale: <_>::default(),
template: EMOTE_V2_URL_TEMPLATE.into(),
}
}
pub fn size_1x(mut self) -> Self {
self.scale = EmoteScale::Size1_0;
self
}
pub fn size_2x(mut self) -> Self {
self.scale = EmoteScale::Size2_0;
self
}
pub fn size_3x(mut self) -> Self {
self.scale = EmoteScale::Size3_0;
self
}
pub fn dark_mode(mut self) -> Self {
self.theme_mode = EmoteThemeMode::Dark;
self
}
pub fn light_mode(mut self) -> Self {
self.theme_mode = EmoteThemeMode::Light;
self
}
pub fn animation_default(mut self) -> Self {
self.animation_setting = None;
self
}
pub fn animation_static(mut self) -> Self {
self.animation_setting = Some(EmoteAnimationSetting::Static);
self
}
pub fn animation_animated(mut self) -> Self {
self.animation_setting = Some(EmoteAnimationSetting::Animated);
self
}
pub fn render(self) -> String {
if self.template != "https://static-cdn.jtvnw.net/emoticons/v2/{{id}}/{{format}}/{{theme_mode}}/{{scale}}" {
let custom_template = |builder: &EmoteUrlBuilder| -> Option<String> {
let mut template = self.template.clone().into_owned();
let emote_id_range = template.find("{{id}}")?;
eprintln!("id");
template.replace_range(emote_id_range..emote_id_range+"{{id}}".len(), builder.id.as_str());
eprintln!("format");
let format_range = template.find("{{format}}")?;
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")));
eprintln!("theme_mode");
let theme_mode_range = template.find("{{theme_mode}}")?;
template.replace_range(theme_mode_range..theme_mode_range+"{{theme_mode}}".len(), &builder.theme_mode.to_string());
eprintln!("scale");
let scale_range = template.find("{{scale}}")?;
template.replace_range(scale_range..scale_range+"{{scale}}".len(), &builder.scale.to_string());
if template.contains("{{") || template.contains("}}") {
None
} else {
Some(template)
}
};
if let Some(template) = custom_template(&self) {
return template
} else {
#[cfg(feature = "tracing")]
tracing::warn!(template = %self.template, "emote builder was supplied an invalid or unknown template url, falling back to standard builder");
}
}
format!("https://static-cdn.jtvnw.net/emoticons/v2/{emote_id}/{animation_setting}/{theme_mode}/{scale}",
emote_id = self.id,
animation_setting = self.animation_setting.as_ref().map(|s| s.to_string()).unwrap_or_else(|| String::from("default")),
theme_mode = self.theme_mode,
scale = self.scale,
)
}
}
#[aliri_braid::braid(serde)]
pub struct EmoteSetId;
#[aliri_braid::braid(serde)]
pub struct StreamSegmentId;
#[aliri_braid::braid(serde)]
pub struct HypeTrainId;
#[aliri_braid::braid(serde)]
pub struct CreatorGoalId;
#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct ResubscriptionEmote {
pub begin: i64,
pub end: i64,
pub id: EmoteId,
}
impl std::fmt::Display for ResubscriptionEmote {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}-{}", self.id, self.begin, self.end)
}
}
#[derive(PartialEq, Deserialize, Serialize, Debug, Clone)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct TwitchCategory {
pub box_art_url: String,
pub id: CategoryId,
pub name: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(field_identifier)]
pub enum SubscriptionTier {
#[serde(rename = "1000")]
Tier1,
#[serde(rename = "2000")]
Tier2,
#[serde(rename = "3000")]
Tier3,
Prime,
Other(String),
}
impl Serialize for SubscriptionTier {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
serializer.serialize_str(match self {
SubscriptionTier::Tier1 => "1000",
SubscriptionTier::Tier2 => "2000",
SubscriptionTier::Tier3 => "3000",
SubscriptionTier::Prime => "Prime",
SubscriptionTier::Other(o) => o,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum BroadcasterType {
#[serde(rename = "partner")]
Partner,
#[serde(rename = "affiliate")]
Affiliate,
#[serde(other)]
None,
}
impl Serialize for BroadcasterType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
serializer.serialize_str(match self {
BroadcasterType::Partner => "partner",
BroadcasterType::Affiliate => "affiliate",
BroadcasterType::None => "",
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum UserType {
#[serde(rename = "staff")]
Staff,
#[serde(rename = "admin")]
Admin,
#[serde(rename = "global_mod")]
GlobalMod,
#[serde(other)]
None,
}
impl Serialize for UserType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
serializer.serialize_str(match self {
UserType::Staff => "staff",
UserType::Admin => "admin",
UserType::GlobalMod => "global_mod",
UserType::None => "",
})
}
}
#[derive(PartialEq, Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum VideoPeriod {
All,
Day,
Week,
Month,
}
#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum VideoType {
Live,
Playlist,
Upload,
Archive,
Highlight,
Premiere,
Rerun,
WatchParty,
WatchPartyPremiere,
WatchPartyRerun,
}
#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum VideoPrivacy {
Public,
Private,
}
#[derive(
displaydoc::Display,
serde_repr::Serialize_repr,
serde_repr::Deserialize_repr,
Debug,
Clone,
PartialEq,
Eq,
)]
#[repr(u64)]
#[non_exhaustive]
pub enum CommercialLength {
Length30 = 30,
Length60 = 60,
Length90 = 90,
Length120 = 120,
Length150 = 150,
Length180 = 180,
}
impl std::convert::TryFrom<u64> for CommercialLength {
type Error = CommercialLengthParseError;
fn try_from(l: u64) -> Result<Self, Self::Error> {
match l {
30 => Ok(CommercialLength::Length30),
60 => Ok(CommercialLength::Length60),
90 => Ok(CommercialLength::Length90),
120 => Ok(CommercialLength::Length120),
150 => Ok(CommercialLength::Length150),
180 => Ok(CommercialLength::Length180),
other => Err(CommercialLengthParseError::InvalidLength(other)),
}
}
}
#[derive(thiserror::Error, Debug, displaydoc::Display)]
pub enum CommercialLengthParseError {
InvalidLength(u64),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
pub struct User {
#[serde(alias = "user_id")]
pub id: UserId,
#[serde(alias = "user_login")]
pub login: UserName,
#[serde(alias = "user_display_name", alias = "user_name")]
pub display_name: DisplayName,
#[serde(default)]
pub profile_image_url: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct Image {
pub url_1x: String,
pub url_2x: String,
pub url_4x: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct GlobalCooldown {
pub is_enabled: bool,
#[serde(alias = "seconds")]
pub global_cooldown_seconds: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[serde(untagged)]
#[non_exhaustive]
pub enum Max {
MaxPerStream {
is_enabled: bool,
#[serde(alias = "value")]
max_per_stream: u32,
},
MaxPerUserPerStream {
is_enabled: bool,
#[serde(alias = "value")]
max_per_user_per_stream: u32,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct PollChoice {
pub id: String,
pub title: String,
pub votes: Option<i64>,
pub channel_points_votes: Option<i64>,
pub bits_votes: Option<i64>,
}
#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum PollStatus {
#[serde(alias = "active")]
Active,
#[serde(alias = "completed")]
Completed,
#[serde(alias = "terminated")]
Terminated,
#[serde(alias = "archived")]
Archived,
#[serde(alias = "moderated")]
Moderated,
#[serde(alias = "invalid")]
Invalid,
}
#[derive(PartialEq, Deserialize, Serialize, Debug, Clone)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum PredictionStatus {
#[serde(alias = "resolved")]
Resolved,
#[serde(alias = "active")]
Active,
#[serde(alias = "canceled")]
Canceled,
#[serde(alias = "locked")]
Locked,
}
#[derive(PartialEq, Deserialize, Serialize, Debug, Clone)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct PredictionOutcome {
pub id: String,
pub title: String,
pub users: Option<i64>,
pub channel_points: Option<i64>,
pub top_predictors: Option<Vec<PredictionTopPredictors>>,
pub color: String,
}
#[derive(PartialEq, Deserialize, Serialize, Debug, Clone)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct PredictionTopPredictors {
#[serde(alias = "user_id")]
pub id: UserId,
#[serde(alias = "user_name")]
pub name: DisplayName,
#[serde(alias = "user_login")]
pub login: UserName,
pub channel_points_used: i64,
pub channel_points_won: Option<i64>,
}
#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum AutomodStatus {
Pending,
Allowed,
Denied,
Expired,
}
#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum CreatorGoalType {
Follower,
Subscription,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn time_test() {
let mut time1 = Timestamp::new("2021-11-11T10:00:00Z").unwrap();
time1.set_time(10, 0, 32);
let time2 = Timestamp::new("2021-11-10T10:00:00Z").unwrap();
assert!(time2.is_before(&time1));
dbg!(time1.normalize().unwrap());
#[cfg(feature = "time")]
let time = Timestamp::new("2021-11-11T13:37:00-01:00").unwrap();
#[cfg(feature = "time")]
dbg!(time.normalize().unwrap());
}
}