Update links stuff

This commit is contained in:
Tom 2022-08-17 23:02:13 +02:00
parent 4aa918a966
commit ed9330667b
3 changed files with 446 additions and 26 deletions

View file

@ -1,8 +1,10 @@
use crate::{commands::utils, Context, Error};
pub mod eurosport;
pub mod viaplay;
pub mod wrc;
#[derive(Debug, poise::SlashChoiceParameter)]
#[derive(Debug, poise::ChoiceParameter)]
pub enum Timeframe {
#[name = "Currently happening"]
Current,
@ -14,33 +16,52 @@ pub enum Timeframe {
Everything,
}
#[derive(Debug, poise::SlashChoiceParameter)]
pub enum Source {
#[name = "Get links for the Eurosport player"]
Eurosport,
#[name = "Get links for the apocalympics Eurosport player"]
Olympics,
}
// #[derive(Debug, poise::ChoiceParameter)]
// pub enum Source {
// #[name = "Get links for the Eurosport player"]
// Eurosport,
// // #[name = "Get links for the apocalympics Eurosport player"]
// // Olympics,
// #[name = "Get WRC links for the WRC player"]
// WRC,
// #[name = "Get Viaplay links for the Viaplay player"]
// Viaplay,
// // #[name = "F1 content for the weekend"]
// // F1,
// }
/// Get links for high tier commands.
#[poise::command(slash_command)]
pub async fn links(
ctx: Context<'_>,
#[description = "Where to git the juicy links from?"] source: Source,
#[description = "Filter sessions for when they are/were happening, defaults to future"]
timeframe: Option<Timeframe>,
#[description = "Content to filter on"] filter: Option<String>,
) -> Result<(), Error> {
if !utils::high_tier(ctx).await {
ctx.say("This command can only be used in high tier channels for security")
.await?;
return Ok(());
}
// #[poise::command(slash_command)]
// pub async fn links2(
// ctx: Context<'_>,
// #[description = "Where to git the juicy links from?"] source: Source,
// #[description = "Filter sessions for when they are/were happening, defaults to future"]
// timeframe: Option<Timeframe>,
// #[description = "Content to filter on"] filter: Option<String>,
// ) -> Result<(), Error> {
// if !utils::high_tier(ctx).await {
// ctx.say("This command can only be used in high tier channels for security")
// .await?;
// return Ok(());
// }
match source {
Source::Eurosport => eurosport::proc_eurosport(ctx, timeframe, filter).await,
Source::Olympics => eurosport::proc_olympics(ctx, timeframe, filter).await,
}
// match source {
// Source::Eurosport => eurosport::proc_eurosport(ctx, timeframe, filter).await,
// // Source::Olympics => eurosport::proc_olympics(ctx, timeframe, filter).await,
// Source::WRC => wrc::wrc(ctx).await,
// Source::Viaplay => viaplay::viaplay(ctx, timeframe, filter).await,
// // Source::F1 => f1::proc_f1(ctx, timeframe, filter).await,
// }
// Ok(())
// // Ok(())
// }
#[poise::command(
prefix_command,
slash_command,
subcommands("viaplay::viaplay", "eurosport::eurosport", "wrc::wrc")
)]
pub async fn links(ctx: Context<'_>) -> Result<(), Error> {
ctx.say("Hello there!").await?;
Ok(())
}

View file

@ -0,0 +1,281 @@
use std::{collections::HashSet, fmt};
use crate::{commands::utils, Context, Error};
use cached::proc_macro::cached;
use chrono::{DateTime, Utc};
use futures::{Stream, StreamExt};
use log::{info, warn};
use reqwest::header::AUTHORIZATION;
use serde::Deserialize;
use super::Timeframe;
// const translations: HashMap<String, String> = HashMap::from([
// ("Valioliiga", "Premier league"),
// ("Gjensidige Kvindeliga", "Mutual Women's League (Norway)"),
// ("Tanskan 1. divisioona", "Danish 1st Division"),
// ("Bundesliiga", "Bundesliga"),
// ("2. Bundesliiga", "2. Bundesliga"),
// ]);
// #[derive(Deserialize, Clone)]
// struct ViaplaySchedule {
// events: Vec<ViaplayEvent>,
// }
#[derive(Deserialize, Clone)]
struct ViaplayEvent {
content: Content,
#[serde(rename = "epg")]
times: EPG,
system: System,
}
impl ViaplayEvent {
fn filter(&self, filter: &str) -> bool {
if filter.is_empty() {
return true;
};
if self.content.format.sport.contains(filter) {
return true;
}
false
}
fn comp(&self, when: &Option<Timeframe>) -> bool {
let now = Utc::now();
match when {
Some(Timeframe::Everything) => true,
Some(Timeframe::Current) => self.times.start <= now && self.times.end >= now,
Some(Timeframe::Future) => self.times.end >= now,
Some(Timeframe::Past) => self.times.end <= now,
_ => self.times.end >= now,
}
}
fn to_string(&self) -> String {
format!("```md\n[{sport}]({title}) {synopsis}```(<t:{start}:R>-<t:{end}:R>) {desc}\nhttps://tom.al/ms/vp/{id}", 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 check_sport(&self, sport: &Option<String>) -> bool {
match sport {
None => true,
Some(s) => self.content.format.sport.contains(s),
}
}
}
impl fmt::Display for ViaplayEvent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Use `self.number` to refer to each positional data point.
// write!(f, "```md\n[{sport}]({title}) {synopsis}```(<t:{start}:R>-<t:{end}:R>) {desc}\nhttps://tom.al/ms/vp/{id}", 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)
write!(f, "{}", self.to_string())
}
}
#[derive(Deserialize, Clone)]
struct Content {
format: Format,
title: String,
#[serde(rename = "originalTitle")]
#[serde(default)]
description: String,
#[serde(default)]
synopsis: String,
}
fn default_sport() -> String {
"Unknown".to_string()
}
#[derive(Deserialize, Clone)]
struct Format {
#[serde(rename = "title")]
#[serde(default = "default_sport")]
#[serde(with = "viaplay_sport")]
sport: String,
}
#[derive(Deserialize, Clone)]
struct EPG {
#[serde(with = "viaplay_date")]
start: DateTime<Utc>,
#[serde(with = "viaplay_date")]
end: DateTime<Utc>,
}
#[derive(Deserialize, Clone)]
struct System {
#[serde(rename = "productKey")]
product_key: String,
}
mod viaplay_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<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Utc.datetime_from_str(&s, FORMAT)
.map_err(serde::de::Error::custom)
}
}
mod viaplay_sport {
use serde::{self, Deserialize, Deserializer};
pub fn deserialize<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"Valioliiga" => Ok("Premier league".to_string()),
"Gjensidige Kvindeliga" => Ok("Mutual Women's League (Norway)".to_string()),
"Tanskan 1. divisioona" => Ok("Danish 1st Division".to_string()),
"Bundesliiga" => Ok("Bundesliga".to_string()),
"2. Bundesliiga" => Ok("2. Bundesliga".to_string()),
"Hevosurheilu" => Ok("Equestrian sport".to_string()),
_ => Ok(s.to_string()),
}
}
}
#[cached(time = 3600)]
async fn get_schedule() -> Option<Vec<ViaplayEvent>> {
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/viaplay/schedule"
))
.header(AUTHORIZATION, token)
.send()
.await;
let result: Option<Vec<ViaplayEvent>> = match req {
Err(e) => {
warn!("Error getting Viaplay schedule {}", e);
None
}
Ok(req) if req.status().as_u16() == 404 => {
warn!("404 on getting VP events");
None
}
Ok(req) if req.status().as_u16() == 200 => {
let data = req.json::<Vec<ViaplayEvent>>().await;
match data {
Ok(d) => Some(d),
Err(e) => {
warn!("Error getting Viaplay schedule {}", e);
None
}
}
}
Ok(req) => {
warn!(
"Unhandled status when parsing viaplay request {}",
req.status()
);
None
}
};
result
}
#[cached(time = 3600)]
pub async fn get_sports() -> Vec<String> {
// let events = get_schedule();
if let Some(events) = get_schedule().await {
let mut result: HashSet<String> = HashSet::new();
for event in events {
result.insert(event.content.format.sport);
}
return Vec::from_iter(result);
} else {
return vec![];
}
}
// #[allow(dead_code)]
// async fn autocomplete_sport(_ctx: Context<'_>, partial: String) -> impl Stream<Item = String> {
// // futures::stream::iter(get_sports().iter())
// // .filter(move |name| futures::future::ready(name.contains(&partial)))
// // .map(|name| name.to_string())
// futures::stream::iter(get_sports().await)
// .filter(move |name| futures::future::ready(name.contains(&partial)))
// }
async fn autocomplete_sport<'a>(
_ctx: Context<'_>,
partial: &'a str,
) -> impl Stream<Item = String> + 'a {
futures::stream::iter(get_sports().await)
.filter(move |name| futures::future::ready(name.contains(&partial)))
.map(|name| name.to_string())
}
//Viaplay events listing
#[poise::command(slash_command)]
pub async fn viaplay(
ctx: Context<'_>,
#[description = "Filter sessions for when they are/were happening, defaults to future"]
timeframe: Option<super::Timeframe>,
#[description = "Content to filter on"] filter: Option<String>,
#[description = "Filter for which sport to list"]
#[autocomplete = "autocomplete_sport"]
sport: Option<String>,
) -> Result<(), Error> {
let events: Option<Vec<ViaplayEvent>> = get_schedule().await;
match events {
None => {
ctx.say("Unable to get the events, try again later (it's cached so wait a bit...)")
.await?;
}
Some(evs) => {
info!("Found {} events from viaplay", evs.len());
let filtered: Vec<String> = evs
.into_iter()
.filter(|e| e.comp(&timeframe))
.filter(|e| e.check_sport(&sport))
.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(())
}
// /// Another subcommand of `parent`
// #[poise::command(slash_command)]
// pub async fn child3(
// ctx: Context<'_>,
// #[description = "Where to git the juicy links from?"]
// _source: super::Source,
// #[description = "Filter sessions for when they are/were happening, defaults to future"]
// _timeframe: Option<super::Timeframe>,
// #[description = "Content to filter on"] filter: Option<String>,
// #[description = "Filter for which sport to list (only Viaplay)"]
// #[autocomplete = "autocomplete_sport"]
// sport: Option<String>,
// ) -> Result<(), Error> {
// ctx.say("You invoked the second child command!").await?;
// Ok(())
// }

118
src/commands/links/wrc.rs Normal file
View file

@ -0,0 +1,118 @@
use crate::{commands::utils, Context, Error};
use chrono::{DateTime, Utc};
use log::*;
use serde::Deserialize;
mod wrc_date {
use chrono::{DateTime, TimeZone, Utc};
use serde::{self, Deserialize, Deserializer};
const FORMAT: &'static str = "%Y-%m-%dT%H:%M:%S%:z";
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, 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)]
struct WRC {
// #[serde(rename = "earliestPlayableStart")]
// #[serde(with = "es_date_time")]
// earliest_playable_start: DateTime<Utc>,
// id: u32,
name: String,
#[serde(rename = "eventDays")]
days: Vec<WRCDays>,
}
#[derive(Deserialize)]
struct WRCDays {
// id: u32,
#[serde(rename = "eventDay")]
event_day: String,
#[serde(rename = "spottChannel")]
spott_channel: WRCChannel,
}
#[derive(Deserialize)]
struct WRCChannel {
// id: u32,
// #[serde(rename = "displayName")]
// name: String,
assets: Vec<WRCAssets>,
}
#[derive(Deserialize)]
struct WRCAssets {
// id: u32,
#[serde(with = "wrc_date")]
start: DateTime<Utc>,
#[serde(with = "wrc_date")]
end: DateTime<Utc>,
// duration: u32,
content: WRCContent,
}
#[derive(Deserialize)]
struct WRCContent {
title: String,
id: u32,
#[serde(default)]
description: Option<String>,
}
async fn get_schedule() -> Result<Option<WRC>, Error> {
let client = reqwest::Client::new();
let req = client
.get("https://api.wrc.com/sdb/rallyevent/active/")
.send()
.await?;
if req.status().as_u16() == 404 {
info!("Error 404 on getting wrc schedule");
Ok(None)
} else if req.status().as_u16() == 200 {
let data: WRC = req.json().await?;
Ok(Some(data))
} else {
Ok(None)
}
}
// WRC sessions
#[poise::command(slash_command)]
pub async fn wrc(
ctx: Context<'_>,
// #[description = "Filter sessions for when they are/were happening, defaults to future"]
// timeframe: Option<super::Timeframe>,
// #[description = "Content to filter on"] filter: Option<String>,
) -> Result<(), Error> {
let wrc = get_schedule().await?;
match wrc {
None => {
ctx.say("No WRC sessions found").await?;
Ok(())
}
Some(wrc_data) => {
let mut events: Vec<String> = vec![];
for day in wrc_data.days {
for session in day.spott_channel.assets {
let desc = if let Some(d) = session.content.description {
d
} else {
"".to_string()
};
events.push(format!("{}: <t:{}:R>- <t:{}:R>: [{}](https://morningstreams.com/wrcplayer.html?id={}) {} {}", day.event_day, session.start.timestamp(), session.end.timestamp(), session.content.id, session.content.id, session.content.title, desc));
}
}
let pages = utils::paginator(events, 2400, "\n".to_owned());
utils::paginate_string_embed(ctx, format!("WRC {}", wrc_data.name), pages).await?;
Ok(())
}
}
}