use {
std::collections::BTreeMap,
chrono::prelude::*,
lazy_regex::regex_captures,
serde::{
Deserialize,
Serialize,
de::{
Deserializer,
Error as _,
Unexpected,
},
},
serde_with::{
Map,
serde_as,
},
url::Url,
crate::UDuration,
};
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(tag = "type")]
pub enum Message {
#[serde(rename = "chat.history")]
ChatHistory {
messages: Vec<ChatMessage>,
},
#[serde(rename = "chat.message")]
ChatMessage {
message: ChatMessage,
},
#[serde(rename = "chat.dm")]
ChatDm {
message: String,
from_user: Option<DmUser>,
from_bot: Option<String>,
to: DmUser,
},
#[serde(rename = "chat.pin")]
ChatPin {
message: ChatMessage,
},
#[serde(rename = "chat.unpin")]
ChatUnpin {
message: ChatMessage,
},
#[serde(rename = "chat.delete")]
ChatDelete {
delete: ChatDelete,
},
#[serde(rename = "chat.purge")]
ChatPurge {
purge: ChatPurge,
},
#[serde(rename = "error")]
Error {
errors: Vec<String>,
},
#[serde(rename = "pong")]
Pong,
#[serde(rename = "race.data")]
RaceData {
race: RaceData,
},
#[serde(rename = "race.renders")]
RaceRenders,
#[serde(rename = "race.split")]
RaceSplit,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct CategoryData {
pub name: String,
pub short_name: String,
pub slug: String,
pub url: String,
pub data_url: String,
pub image: Option<Url>,
pub info: Option<String>,
pub streaming_required: bool,
pub owners: Vec<UserData>,
pub moderators: Vec<UserData>,
pub goals: Vec<String>,
pub current_races: Vec<RaceSummary>,
pub emotes: BTreeMap<String, Url>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct CategorySummary {
pub name: String,
pub short_name: String,
pub slug: String,
pub url: String,
pub data_url: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct ChatDelete {
pub id: String,
pub user: Option<UserData>,
pub bot: Option<String>,
pub is_bot: bool,
pub deleted_by: UserData,
}
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct ChatMessage {
pub id: String,
pub user: Option<UserData>,
pub bot: Option<String>,
pub posted_at: DateTime<Utc>,
pub message: String,
pub message_plain: String,
pub highlight: bool,
pub is_bot: bool,
pub is_system: Option<bool>,
pub is_pinned: bool,
#[serde_as(as = "Map<_, _>")]
pub actions: Vec<(String, ActionButton)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct ChatPurge {
pub user: UserData,
pub purged_by: UserData,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Goal {
pub name: String,
pub custom: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Entrant {
pub user: Option<MaybeAnonymizedUserData>,
pub status: EntrantStatus,
#[serde(deserialize_with = "deserialize_opt_django_uduration")]
pub finish_time: Option<UDuration>,
pub finished_at: Option<DateTime<Utc>>,
pub place: Option<u32>,
pub place_ordinal: Option<String>,
pub score: Option<u32>,
pub score_change: Option<i32>,
pub comment: Option<String>,
pub has_comment: bool,
pub stream_live: bool,
pub stream_override: bool,
pub team: Option<EntrantTeam>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct EntrantStatus {
pub value: EntrantStatusValue,
pub verbose_value: String,
pub help_text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntrantStatusValue {
Requested,
Invited,
Declined,
Ready,
NotReady,
InProgress,
Done,
Dnf,
Dq,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct EntrantTeam {
pub name: String,
pub slug: String,
pub formal: bool,
pub url: Option<String>,
pub data_url: Option<String>,
pub avatar: Option<Url>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct RaceData {
pub version: u32,
pub name: String,
pub slug: String,
pub category: CategorySummary,
pub status: RaceStatus,
pub url: String,
pub data_url: String,
pub websocket_url: String,
pub websocket_bot_url: String,
pub websocket_oauth_url: String,
pub goal: Goal,
pub info: String,
pub info_bot: Option<String>,
pub info_user: Option<String>,
pub entrants_count: u32,
pub entrants_count_finished: u32,
pub entrants_count_inactive: u32,
pub entrants: Vec<Entrant>,
pub opened_at: DateTime<Utc>,
#[serde(deserialize_with = "deserialize_django_uduration")]
pub start_delay: UDuration,
pub started_at: Option<DateTime<Utc>>,
pub ended_at: Option<DateTime<Utc>>,
pub cancelled_at: Option<DateTime<Utc>>,
pub ranked: bool,
pub unlisted: bool,
#[serde(deserialize_with = "deserialize_django_uduration")]
pub time_limit: UDuration,
pub time_limit_auto_complete: bool,
pub require_even_teams: bool,
pub streaming_required: bool,
pub auto_start: bool,
pub opened_by: Option<UserData>,
pub monitors: Vec<UserData>,
pub recordable: bool,
pub recorded: bool,
pub recorded_by: Option<UserData>,
pub disqualify_unready: bool,
pub allow_comments: bool,
pub hide_comments: bool,
pub hide_entrants: bool,
pub chat_restricted: bool,
pub allow_midrace_chat: bool,
pub allow_non_entrant_chat: bool,
#[serde(deserialize_with = "deserialize_django_uduration")]
pub chat_message_delay: UDuration,
pub bot_meta: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct RaceStatus {
pub value: RaceStatusValue,
pub verbose_value: String,
pub help_text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RaceStatusValue {
Open,
Invitational,
Pending,
InProgress,
Finished,
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct RaceSummary {
pub name: String,
pub category: Option<CategorySummary>,
pub status: RaceStatus,
pub url: String,
pub data_url: String,
pub goal: Goal,
pub info: String,
pub entrants_count: u32,
pub entrants_count_finished: u32,
pub entrants_count_inactive: u32,
pub opened_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
#[serde(deserialize_with = "deserialize_django_uduration")]
pub time_limit: UDuration,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct MaybeAnonymizedUserData {
pub id: String,
pub full_name: String,
pub name: String,
pub discriminator: Option<String>,
pub url: Option<String>,
pub avatar: Option<String>,
pub pronouns: Option<String>,
pub flair: String,
pub twitch_name: Option<String>,
pub twitch_display_name: Option<String>,
pub twitch_channel: Option<Url>,
pub can_moderate: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct UserData {
pub id: String,
pub full_name: String,
pub name: String,
pub discriminator: Option<String>,
pub url: String,
pub avatar: Option<String>,
pub pronouns: Option<String>,
pub flair: String,
pub twitch_name: Option<String>,
pub twitch_display_name: Option<String>,
pub twitch_channel: Option<Url>,
pub can_moderate: bool,
}
#[derive(Debug, Clone, Copy, thiserror::Error)]
#[error("entrant is anonymized")]
pub struct AnonymousError;
impl TryFrom<MaybeAnonymizedUserData> for UserData {
type Error = AnonymousError;
fn try_from(value: MaybeAnonymizedUserData) -> Result<Self, Self::Error> {
Ok(Self {
id: value.id,
full_name: value.full_name,
name: value.name,
discriminator: value.discriminator,
url: value.url.ok_or(AnonymousError)?,
avatar: value.avatar,
pronouns: value.pronouns,
flair: value.flair,
twitch_name: value.twitch_name,
twitch_display_name: value.twitch_display_name,
twitch_channel: value.twitch_channel,
can_moderate: value.can_moderate,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct UserProfile {
pub id: String,
pub full_name: String,
pub name: String,
pub discriminator: Option<String>,
pub url: String,
pub avatar: Option<String>,
pub pronouns: Option<String>,
pub flair: String,
pub twitch_name: Option<String>,
pub twitch_display_name: Option<String>,
pub twitch_channel: Option<Url>,
pub can_moderate: bool,
pub teams: Vec<EntrantTeam>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct DmUser {
pub id: String,
pub full_name: String,
pub name: String,
pub discriminator: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ActionButton {
Message {
message: String,
help_text: Option<String>,
survey: Option<Vec<SurveyQuestion>>,
submit: Option<String>,
},
Url {
url: Url,
help_text: Option<String>,
}
}
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct SurveyQuestion {
pub name: String,
pub label: String,
pub default: Option<serde_json::Value>,
#[serde(rename = "help")]
pub help_text: Option<String>,
#[serde(rename = "type")]
pub kind: SurveyQuestionKind,
pub placeholder: Option<String>,
#[serde_as(as = "Map<_, _>")]
#[serde(default)]
pub options: Vec<(String, String)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SurveyQuestionKind {
Input,
Bool,
Radio,
Select,
}
fn deserialize_django_uduration<'de, D: Deserializer<'de>>(deserializer: D) -> Result<UDuration, D::Error> {
let s = String::deserialize(deserializer)?;
if let Some((_, days, hours, minutes, seconds, ms)) = regex_captures!("^P([0-9]+)DT([0-9]+)H([0-9]+)M([0-9]+)(?:\\.([0-9]+))?S$", &s) {
Ok(UDuration::from_secs(days.parse::<u64>().map_err(D::Error::custom)? * 60 * 60 * 24)
+ UDuration::from_secs(hours.parse::<u64>().map_err(D::Error::custom)? * 60 * 60)
+ UDuration::from_secs(minutes.parse::<u64>().map_err(D::Error::custom)? * 60)
+ UDuration::from_secs(seconds.parse().map_err(D::Error::custom)?)
+ UDuration::from_micros(if ms.is_empty() { 0 } else { ms.parse().map_err(D::Error::custom)? }))
} else {
Err(D::Error::invalid_value(Unexpected::Str(&s), &"a duration as produced by Django"))
}
}
fn deserialize_opt_django_uduration<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<UDuration>, D::Error> {
if let Some(s) = Option::<String>::deserialize(deserializer)? {
if let Some((_, days, hours, minutes, seconds, ms)) = regex_captures!("^P([0-9]+)DT([0-9]+)H([0-9]+)M([0-9]+)(?:\\.([0-9]+))?S$", &s) {
Ok(Some(UDuration::from_secs(days.parse::<u64>().map_err(D::Error::custom)? * 60 * 60 * 24)
+ UDuration::from_secs(hours.parse::<u64>().map_err(D::Error::custom)? * 60 * 60)
+ UDuration::from_secs(minutes.parse::<u64>().map_err(D::Error::custom)? * 60)
+ UDuration::from_secs(seconds.parse().map_err(D::Error::custom)?)
+ UDuration::from_micros(if ms.is_empty() { 0 } else { ms.parse().map_err(D::Error::custom)? })))
} else {
Err(D::Error::invalid_value(Unexpected::Str(&s), &"a duration as produced by Django"))
}
} else {
Ok(None)
}
}