diff --git a/src/commands/links/mod.rs b/src/commands/links/mod.rs index c8622ee..e4f264b 100644 --- a/src/commands/links/mod.rs +++ b/src/commands/links/mod.rs @@ -2,6 +2,7 @@ use crate::{Context, Error}; mod eurosport; mod f1; +mod nfl; mod viaplay; mod wrc; @@ -59,7 +60,7 @@ pub enum Timeframe { #[poise::command( slash_command, - subcommands("viaplay::viaplay", "eurosport::eurosport", "wrc::wrc", "f1::f1") + subcommands("viaplay::viaplay", "eurosport::eurosport", "wrc::wrc", "f1::f1", "nfl::nfl") )] pub async fn links(ctx: Context<'_>) -> Result<(), Error> { ctx.say("Hello there!").await?; diff --git a/src/commands/links/nfl.rs b/src/commands/links/nfl.rs new file mode 100644 index 0000000..5c175e2 --- /dev/null +++ b/src/commands/links/nfl.rs @@ -0,0 +1,229 @@ +use cached::proc_macro::cached; +use chrono::{DateTime, Duration, Utc}; +use log::{warn, info}; +use reqwest::header::AUTHORIZATION; +use serde::Deserialize; +use crate::{commands::utils, Context, Error}; + +use super::Timeframe; + +mod str_to_u8 { + use serde::{self, Deserialize, Deserializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(s.parse::().unwrap()) + } +} + +mod nfl_date { + use chrono::{DateTime, TimeZone, Utc}; + use serde::{self, Deserialize, Deserializer}; + + const FORMAT: &'static str = "%Y-%m-%dT%H:%M:%S.%fZ"; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Utc.datetime_from_str(&s, FORMAT) + .map_err(serde::de::Error::custom) + } +} + +#[derive(Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +struct NFLContext { + // current_season: String, + // current_season_type: String, + #[serde(with = "str_to_u8")] + current_week: u8, +} + +#[derive(Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +struct NFLEvent { + home_nick_name: String, + visitor_nick_name: String, + // game_id: String, + #[serde(with = "nfl_date")] + game_date_time_utc: DateTime, + video: NFLVideo, +} + +#[derive(Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +struct NFLVideo { + title: String, + // video_status: String, + video_id: String, + // schedule_date: String +} + +impl NFLEvent { + fn filter(&self, filter: &str) -> bool { + if filter.is_empty() { + return true; + }; + if self.video.title.contains(filter) { + return true; + } + false + } + + fn comp(&self, when: &Option) -> bool { + let now = Utc::now(); + match when { + Some(Timeframe::Everything) => true, + Some(Timeframe::Current) => { + self.game_date_time_utc <= now && (self.game_date_time_utc + Duration::minutes(240)) >= now + } + Some(Timeframe::Future) => (self.game_date_time_utc + Duration::minutes(240)) >= now, + Some(Timeframe::Past) => self.game_date_time_utc <= now, + _ => (self.game_date_time_utc + Duration::minutes(240)) >= now, + } + } + + fn to_string(&self) -> String { + format!( + "```fix\n{home}-{away} ||{title}``` https://tom.al/ms/nfl/{id}", + title = self.video.title, + id = self.video.video_id, + time = self.game_date_time_utc.timestamp(), + home= self.home_nick_name, + away = self.visitor_nick_name, + ) + } +} + +#[cached(time = 3600)] +async fn get_week() -> Option { + let token = super::super::super::SETTINGS + .read() + .unwrap() + .get_table("morningstreams") + .unwrap() + .get("token") + .expect("Config error, please set the morningstreams[token] value") + .clone() + .into_string() + .expect("Config error, please make sure morningstreams[token] is a string"); + let client = reqwest::Client::new(); + let req = client + .get("https://api.morningstreams.com/api/hightier/nfl/current-context") + .header(AUTHORIZATION, token) + .send() + .await; + + match req { + Err(e) => { + warn!("Error getting NFL context {}", e); + None + } + Ok(req) => match req.json::().await { + Ok(data) => Some(data), + Err(e) => { + warn!("Error parsing NFL context {}", e); + None + } + }, + } +} + +#[cached(time = 3600)] +async fn get_schedule(week: u8) -> Option> { + let token = super::super::super::SETTINGS + .read() + .unwrap() + .get_table("morningstreams") + .unwrap() + .get("token") + .expect("Config error, please set the morningstreams[token] value") + .clone() + .into_string() + .expect("Config error, please make sure morningstreams[token] is a string"); + let client = reqwest::Client::new(); + let req = client + .get(format!( + "https://api.morningstreams.com/api/hightier/nfl/schedule/{}", + week + )) + .header(AUTHORIZATION, token) + .send() + .await; + + let result: Option> = match req { + Err(e) => { + warn!("Error getting NFL schedule {}", e); + None + } + Ok(req) if req.status().as_u16() == 404 => { + warn!("404 on getting NFL events"); + None + } + Ok(req) if req.status().as_u16() == 200 => { + let data = req.json::>().await; + match data { + Ok(d) => Some(d), + Err(e) => { + warn!("Error getting NFL schedule {}", e); + None + } + } + } + Ok(req) => { + warn!("Unhandled status when parsing NFL request {}", req.status()); + None + } + }; + result +} + + +//NFL events listing +#[poise::command(slash_command)] +pub async fn nfl( + ctx: Context<'_>, + #[description = "Filter sessions for when they are/were happening, defaults to future"] + timeframe: Option, + #[description = "Content to filter on"] filter: Option, + #[description = "Which game week? (Defaults to current)"] + week: Option, +) -> Result<(), Error> { + let get_week: u8 = match week{ + Some(w) => w, + None => { + match get_week().await { + None => { ctx.say("Error getting current week data, try setting one manually").await?; return Ok(())}, + Some(w) => w.current_week, + } + } + }; + + let events: Option> = get_schedule(get_week).await; + match events { + None => { + ctx.say("Unable to get the events, try a different game week or try again later (it's cached so wait a bit...)") + .await?; + } + Some(evs) => { + info!("Found {} events from NFL", evs.len()); + let filtered: Vec = evs + .into_iter() + .filter(|e| e.comp(&timeframe)) + .filter(|e| match &filter { + None => true, + Some(f) => e.filter(f.as_str()), + }) + .map(|e| e.to_string()) + .collect(); + let pages = utils::paginator(filtered, 1900, "\n".to_string()); + utils::paginate_string(ctx, pages).await?; + } + }; + Ok(()) +}