Update to the new website api for schedule
This commit is contained in:
parent
c53a5f1f2c
commit
91e195ddbf
|
@ -1,205 +1,91 @@
|
||||||
use crate::{Context, Error};
|
use crate::{commands::utils, Context, Error};
|
||||||
use cached::proc_macro::cached;
|
use cached::proc_macro::cached;
|
||||||
use futures::{Stream, StreamExt};
|
use chrono::{DateTime, Utc};
|
||||||
use log::*;
|
use log::{warn};
|
||||||
|
|
||||||
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
|
|
||||||
use poise::serenity_prelude::CreateEmbed;
|
|
||||||
use reqwest::header::AUTHORIZATION;
|
use reqwest::header::AUTHORIZATION;
|
||||||
use reqwest::Error as reqError;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::cmp::Ordering::{Greater, Less};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
struct Event {
|
struct MSReq {
|
||||||
name: String,
|
containers: Vec<MSEvent>,
|
||||||
|
#[serde(rename = "eventTitle")]
|
||||||
|
event_title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
struct MSEvent {
|
||||||
|
id: String,
|
||||||
|
// #[serde(rename = "longDescription")]
|
||||||
|
// description: String,
|
||||||
|
// country: String,
|
||||||
|
metadata: MSMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MSEvent {
|
||||||
|
fn get_title(&self) -> String {
|
||||||
|
let title = &self.metadata.attributes.series.replace("FORMULA", "F");
|
||||||
|
format!("**{}: {}**", title, self.metadata.brief)
|
||||||
|
// format!("", sport=self.content.format.sport, title=self.content.title, synopsis=self.content.synopsis, start=self.times.start.timestamp(), end=self.times.end.timestamp(), desc=self.content.description, id=self.system.product_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_value(&self, high_tier: bool) -> String {
|
||||||
|
let link = if high_tier {
|
||||||
|
format!("[{id}](https://morningstreams.com/hightier/f1/session/{id})\n", id=self.id)
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
format!("{link}Start: <t:{start}:R>\nEnd: <t:{end}:R>", link=link, start=self.metadata.attributes.start.timestamp(), end=self.metadata.attributes.end.timestamp())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
struct MSMetadata {
|
||||||
|
// id: String,
|
||||||
|
// system: System,
|
||||||
|
#[serde(rename="emfAttributes")]
|
||||||
|
attributes: EmfAttributes,
|
||||||
|
#[serde(rename="titleBrief")]
|
||||||
|
brief: String,
|
||||||
|
// #[serde(rename="Series")]
|
||||||
|
// series: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
struct EmfAttributes {
|
||||||
|
#[serde(with = "ms_date")]
|
||||||
|
#[serde(rename="sessionStartDate")]
|
||||||
|
start: DateTime<Utc>,
|
||||||
|
#[serde(with = "ms_date")]
|
||||||
|
#[serde(rename="sessionEndDate")]
|
||||||
|
end: DateTime<Utc>,
|
||||||
|
#[serde(rename="Series")]
|
||||||
series: String,
|
series: String,
|
||||||
_lower_series: String,
|
|
||||||
session: String,
|
|
||||||
_lower_session: String,
|
|
||||||
date: DateTime<Utc>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct F1Data {
|
|
||||||
#[serde(rename = "seasonContext")]
|
|
||||||
season_context: SeasonContext,
|
|
||||||
race: Race,
|
|
||||||
}
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct SeasonContext {
|
|
||||||
timetables: Vec<TimeTable>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct Race {
|
|
||||||
#[serde(rename = "meetingOfficialName")]
|
|
||||||
name: String,
|
|
||||||
#[serde(rename = "meetingCountryName")]
|
|
||||||
country: String,
|
|
||||||
}
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct TimeTable {
|
|
||||||
state: String,
|
|
||||||
session: String,
|
|
||||||
#[serde(rename = "gmtOffset")]
|
|
||||||
offset: String,
|
|
||||||
#[serde(rename = "startTime")]
|
|
||||||
start: String,
|
|
||||||
#[serde(rename = "endTime")]
|
|
||||||
end: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Event {
|
|
||||||
fn create(name: String, series: String, session: String, date: DateTime<Utc>) -> Event {
|
|
||||||
Event {
|
|
||||||
name,
|
|
||||||
_lower_series: series.to_ascii_lowercase(),
|
|
||||||
series,
|
|
||||||
_lower_session: session.to_ascii_lowercase(),
|
|
||||||
session,
|
|
||||||
date,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deref(&self) -> Event {
|
mod ms_date {
|
||||||
Event {
|
use chrono::{DateTime, Utc, NaiveDateTime};
|
||||||
name: self.name.to_string(),
|
use serde::{self, Deserialize, Deserializer};
|
||||||
_lower_series: self._lower_series.to_string(),
|
|
||||||
series: self.series.to_string(),
|
|
||||||
_lower_session: self._lower_session.to_string(),
|
|
||||||
session: self.session.to_string(),
|
|
||||||
date: self.date.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_series(&self, series: String) -> bool {
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
|
||||||
return self._lower_series.contains(&series);
|
where
|
||||||
}
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let n = i64::deserialize(deserializer)?/1000;
|
||||||
|
// let s = String::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
Ok(DateTime::from_utc(NaiveDateTime::from_timestamp(n, 0), Utc))
|
||||||
|
|
||||||
fn check_session(&self, session: String) -> bool {
|
|
||||||
return self._lower_session.contains(&session);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn check(&self, series: String, session: String) -> bool {
|
|
||||||
if self._lower_session.contains(&session) {
|
|
||||||
if self._lower_series.contains(&series) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_time(time: String) -> Option<DateTime<Utc>> {
|
|
||||||
let tim = NaiveDateTime::parse_from_str(&*time, "%Y-%m-%dT%H:%M:%S.%fZ");
|
|
||||||
match tim {
|
|
||||||
Ok(t) => Some(Utc.from_utc_datetime(&t)),
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Error on parsing time: {}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_f1_time(mut time: String, offset: &String) -> Option<DateTime<Utc>> {
|
|
||||||
time.push_str(offset);
|
|
||||||
let tim = DateTime::parse_from_str(&time, "%Y-%m-%dT%H:%M:%S%:z");
|
|
||||||
match tim {
|
|
||||||
Ok(t) => Some(t.with_timezone(&Utc)),
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Error on parsing time: {}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use cached to cache the requests, don't repeatedly redo this call. Caches for 1 hour.
|
|
||||||
#[cached(time = 3600)]
|
#[cached(time = 3600)]
|
||||||
async fn get_api_events() -> Vec<Event> {
|
async fn get_schedule() -> Option<MSReq> {
|
||||||
let token = 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/events"))
|
|
||||||
.header(AUTHORIZATION, token)
|
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct Data {
|
|
||||||
category: String,
|
|
||||||
name: String,
|
|
||||||
session: String,
|
|
||||||
date: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: Option<Vec<Event>> = match req {
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Error getting schedule {}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Ok(req) => {
|
|
||||||
info!("Did MS event request status code {}", req.status().as_u16());
|
|
||||||
if req.status().as_u16() == 404 {
|
|
||||||
warn!("404 on getting MS events");
|
|
||||||
None
|
|
||||||
} else if req.status().as_u16() == 200 {
|
|
||||||
let data: Result<Vec<Data>, reqError> = req.json().await;
|
|
||||||
match data {
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Error parsing morningstreams event: {}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Ok(dat) => {
|
|
||||||
let mut result: Vec<Event> = Vec::new();
|
|
||||||
for d in dat {
|
|
||||||
let t = parse_time(d.date.to_string());
|
|
||||||
if let Some(tim) = t {
|
|
||||||
result.push(Event::create(
|
|
||||||
d.name.to_string(),
|
|
||||||
d.category.to_string(),
|
|
||||||
d.session.to_string(),
|
|
||||||
tim,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if result.len() > 0 {
|
|
||||||
result.sort_by(|a, b| a.date.cmp(&b.date));
|
|
||||||
Some(result)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match result {
|
|
||||||
None => {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
Some(events) => return events,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[cached(time = 3600)]
|
|
||||||
async fn get_f1_events() -> Option<Vec<Event>> {
|
|
||||||
let token = super::super::SETTINGS
|
let token = super::super::SETTINGS
|
||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -213,202 +99,61 @@ async fn get_f1_events() -> Option<Vec<Event>> {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let req = client
|
let req = client
|
||||||
.get(format!(
|
.get(format!(
|
||||||
"https://api.morningstreams.com/api/events/f1/event-tracker"
|
"https://api.morningstreams.com/api/hightier/f1/next-event"
|
||||||
))
|
))
|
||||||
.header(AUTHORIZATION, token)
|
.header(AUTHORIZATION, token)
|
||||||
.send()
|
.send()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let result: Option<Vec<Event>> = match req {
|
let result: Option<MSReq> = match req {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Error getting schedule {}", e);
|
warn!("Error getting Viaplay schedule {}", e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Ok(req) => {
|
Ok(req) if req.status().as_u16() == 404 => {
|
||||||
info!("Did MS F1 request status code {}", req.status().as_u16());
|
warn!("404 on getting VP events");
|
||||||
if req.status().as_u16() == 404 {
|
None
|
||||||
warn!("404 on getting F1 events");
|
}
|
||||||
return None;
|
Ok(req) if req.status().as_u16() == 200 => {
|
||||||
} else if req.status().as_u16() == 200 {
|
let data = req.json::<MSReq>().await;
|
||||||
let data: Result<F1Data, reqError> = req.json().await;
|
match data {
|
||||||
match data {
|
Ok(d) => Some(d),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Error parsing morningstreams event: {}", e);
|
warn!("Error getting Viaplay schedule {}", e);
|
||||||
None
|
None
|
||||||
}
|
|
||||||
Ok(dat) => {
|
|
||||||
// return Some(dat);
|
|
||||||
let mut events: Vec<Event> = Vec::new();
|
|
||||||
// let mut sessions: Vec<F1Session> = Vec::new();
|
|
||||||
for ses in dat.season_context.timetables {
|
|
||||||
if let Some(start) = parse_f1_time(ses.start, &ses.offset) {
|
|
||||||
events.push(Event::create(
|
|
||||||
dat.race.name.to_string(),
|
|
||||||
"Formula 1".to_string(),
|
|
||||||
ses.session,
|
|
||||||
start,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Some(events);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(req) => {
|
||||||
|
warn!(
|
||||||
|
"Unhandled status when parsing viaplay request {}",
|
||||||
|
req.status()
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return result;
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cached(size = 5, time = 3600)]
|
|
||||||
async fn filter_events(series: String, session: String) -> (Option<Event>, Option<Event>) {
|
|
||||||
let mut events: Vec<Event> = get_api_events().await;
|
|
||||||
|
|
||||||
if let Some(mut e) = get_f1_events().await {
|
|
||||||
events.append(&mut e);
|
|
||||||
}
|
|
||||||
if events.len() == 0 {
|
|
||||||
return (None, None);
|
|
||||||
} else {
|
|
||||||
let mut next_event: Option<&Event> = None;
|
|
||||||
let mut previous_event: Option<&Event> = None;
|
|
||||||
let now = Utc::now();
|
|
||||||
for e in &events {
|
|
||||||
if e.check_series(series.to_string()) && e.check_session(session.to_string()) {
|
|
||||||
match now.cmp(&e.date) {
|
|
||||||
// Now is greater (after) event
|
|
||||||
Greater => {
|
|
||||||
if let Some(p) = previous_event {
|
|
||||||
if p.date.cmp(&e.date) == Less {
|
|
||||||
previous_event = Some(e)
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
previous_event = Some(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Less => {
|
|
||||||
if let Some(f) = next_event {
|
|
||||||
if f.date.cmp(&e.date) == Greater {
|
|
||||||
next_event = Some(e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next_event = Some(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
next_event = Some(e);
|
|
||||||
previous_event = Some(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let first: Option<Event> = match previous_event {
|
|
||||||
None => None,
|
|
||||||
Some(e) => Some(e.deref()),
|
|
||||||
};
|
|
||||||
let second: Option<Event> = match next_event {
|
|
||||||
None => None,
|
|
||||||
Some(e) => Some(e.deref()),
|
|
||||||
};
|
|
||||||
(first, second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn build_embed<'a>(event: Event, e: &'a mut CreateEmbed) -> &'a mut CreateEmbed {
|
|
||||||
e.title(format!("{} | {}", event.series, event.name));
|
|
||||||
|
|
||||||
// e.description(format!("{}", event.session));
|
|
||||||
e.field("Session", &event.session, true);
|
|
||||||
e.field(
|
|
||||||
"Starts in",
|
|
||||||
format!("<t:{}:R>", &event.date.timestamp()),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
e.field(
|
|
||||||
"Date and time",
|
|
||||||
format!("<t:{}>", &event.date.timestamp()),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
e.timestamp(event.date);
|
|
||||||
e
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn autocomplete_series<'a>(
|
|
||||||
_ctx: Context<'_>,
|
|
||||||
partial: &'a str,
|
|
||||||
) -> impl Stream<Item = String> + 'a {
|
|
||||||
futures::stream::iter(&["Formula 1", "MotoGP", "IndyCar"])
|
|
||||||
.filter(move |name| futures::future::ready(name.starts_with(&partial)))
|
|
||||||
.map(|name| name.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn autocomplete_session<'a>(
|
|
||||||
_ctx: Context<'_>,
|
|
||||||
partial: &'a str,
|
|
||||||
) -> impl Stream<Item = String> + 'a {
|
|
||||||
futures::stream::iter(&[
|
|
||||||
"Race",
|
|
||||||
"Qualifying",
|
|
||||||
"Free practice 3",
|
|
||||||
"Free practice 2",
|
|
||||||
"Free practice 1",
|
|
||||||
"Sprint race",
|
|
||||||
])
|
|
||||||
.filter(move |name| futures::future::ready(name.starts_with(&partial)))
|
|
||||||
.map(|name| name.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// F1 schedules
|
|
||||||
#[poise::command(slash_command)]
|
#[poise::command(slash_command)]
|
||||||
pub async fn schedule(
|
pub async fn schedule(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Which series to look for"]
|
|
||||||
#[autocomplete = "autocomplete_series"]
|
|
||||||
series: Option<String>,
|
|
||||||
#[description = "Which session to look for"]
|
|
||||||
#[autocomplete = "autocomplete_session"]
|
|
||||||
session: Option<String>,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let serie: String = match series {
|
let events: Option<MSReq> = get_schedule().await;
|
||||||
None => "".to_string(),
|
let ht: bool = utils::high_tier(ctx).await;
|
||||||
Some(ser) => {
|
match events {
|
||||||
if vec![
|
None => {ctx.say("Error on fetching events :(").await?;},
|
||||||
"f1".to_string(),
|
Some(evs) => {
|
||||||
"formula 1".to_string(),
|
ctx.send(|b| {b.embed(|e| {
|
||||||
"formula1".to_string(),
|
e.title(format!("F1 schedule: {}", evs.event_title));
|
||||||
]
|
for event in evs.containers {
|
||||||
.contains(&ser.to_ascii_lowercase())
|
e.field(event.get_title(), event.get_value(ht), true);
|
||||||
{
|
};
|
||||||
"formula 1".to_string()
|
e
|
||||||
} else {
|
})}).await?;
|
||||||
ser.to_ascii_lowercase()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let session: String = match session {
|
|
||||||
None => "".to_string(),
|
|
||||||
Some(s) => s.to_ascii_lowercase(),
|
|
||||||
};
|
|
||||||
// Get the events (This will hopefully be cached)
|
|
||||||
let (previous_event, next_event) = filter_events(serie, session.to_string()).await;
|
|
||||||
|
|
||||||
// Do the event sending thingy...
|
|
||||||
if let Some(e) = next_event {
|
|
||||||
ctx.send(|b| b.embed(|em| build_embed(e, em))).await?;
|
|
||||||
} else if let Some(e) = previous_event {
|
|
||||||
ctx.send(|b| {
|
|
||||||
b.embed(|em| build_embed(e, em));
|
|
||||||
b.content("No future events found, showing most recent")
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
ctx.say("No future events found, showing most recent")
|
|
||||||
.await?;
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
|
||||||
|
}
|
Loading…
Reference in a new issue