diff --git a/src/cgg_data_source.rs b/src/cgg_data_source.rs index c0bee42..d9f9ca3 100644 --- a/src/cgg_data_source.rs +++ b/src/cgg_data_source.rs @@ -1,14 +1,68 @@ use indexmap::IndexMap; +use lazy_static::lazy_static; use regex::Regex; use select::document::Document; use select::predicate::{Class, Name}; -use serde_json::Value; -use lazy_static::{lazy_static}; +use serde_json::{json, Value}; use crate::data_source::DataSource; +const CONSUMABLES: [u32; 9] = [2003, 2004, 2055, 2031, 2032, 2033, 2138, 2140, 2139]; +const TRINKETS: [u32; 3] = [3340, 3364, 3363]; +const ITEM_TYPES: &'static [(&str, [&str; 2]); 4] = &[ + ("Most Frequent Starters", ["firstItems", "mostGames"]), + ( + "Highest Win % Starters", + ["firstItems", "highestWinPercent"], + ), + ("Most Frequent Core Build", ["items", "mostGames"]), + ("Highest Win % Core Build", ["items", "highestWinPercent"]), +]; + pub struct CGGDataSource; + +impl CGGDataSource { + fn make_item_set(&self, data: &Value, label: &str) -> Value { + json!({ + "items": data["items"].as_array().unwrap().iter().map(|x| json!({"id": x["id"].as_str(), "count": 1})).collect::>(), + "type": format!("{} ({:.2}% - {} games)", label, data["winPercent"].as_f64().unwrap() * 100., data["games"].as_u64().unwrap()) + }) + } + + fn make_item_set_from_list( + &self, + list: &Vec, + label: &str, + key: &str, + data: &Value, + ) -> Value { + let mut key_order = String::new(); + if !data["skills"].get("skillInfo").is_none() { + key_order = data["skills"][key]["order"] + .as_array() + .unwrap() + .iter() + .map(|x| { + data["skills"]["skillInfo"].as_array().unwrap() + [x.as_str().unwrap().parse::().unwrap() - 1]["key"] + .as_str() + .unwrap() + }) + .collect::>() + .join("."); + } + json!({ + "items": list.iter().map(|x| json!({"id": x.to_string(), "count": 1})).collect::>(), + "type": format!("{} {}", label, key_order) + }) + } +} + impl DataSource for CGGDataSource { + fn get_alias(&self) -> &str { + "CGG" + } + fn get_champs_with_positions_and_patch( &self, client: &reqwest::Client, @@ -60,7 +114,12 @@ impl DataSource for CGGDataSource { (champions, patch) } - fn get_champ_data(&self, id: &str, position: &str, client: &reqwest::Client) -> Option { + fn get_champ_data_with_win_pourcentage( + &self, + id: &str, + position: &str, + client: &reqwest::Client, + ) -> Option<(Vec, f64)> { let mut req = client .get(&format!( "https://champion.gg/champion/{}/{}?league=", @@ -73,7 +132,27 @@ impl DataSource for CGGDataSource { static ref RE: Regex = Regex::new(r"(?m)^\s+matchupData\.championData = (.*)$").unwrap(); } - serde_json::from_str(&RE.captures(&req.text().unwrap())?[1]).unwrap() + let data: Value = serde_json::from_str(&RE.captures(&req.text().unwrap())?[1]).unwrap(); + let mut blocks = vec![]; + for (label, path) in ITEM_TYPES.iter() { + if !data[&path[0]].get(&path[1]).is_none() { + blocks.push(self.make_item_set(&data[&path[0]][&path[1]], label)); + } + } + + blocks.push(self.make_item_set_from_list( + &CONSUMABLES.to_vec(), + "Consumables | Frequent:", + "mostGames", + &data, + )); + blocks.push(self.make_item_set_from_list( + &TRINKETS.to_vec(), + "Trinkets | Wins:", + "highestWinPercent", + &data, + )); + Some((blocks, data["stats"]["winRate"].as_f64().unwrap() * 100.)) } else { None } diff --git a/src/data_source.rs b/src/data_source.rs index 6264ec7..ccd9cfa 100644 --- a/src/data_source.rs +++ b/src/data_source.rs @@ -1,9 +1,9 @@ use indexmap::IndexMap; -use serde_json::{Value, json}; +use log::{error, info}; +use serde_derive::{Deserialize, Serialize}; +use serde_json::Value; use std::fs; use std::path::PathBuf; -use log::{info, error}; -use serde_derive::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] struct ItemSet { @@ -17,59 +17,34 @@ struct ItemSet { blocks: Vec, } -const CONSUMABLES: [u32; 9] = [2003, 2004, 2055, 2031, 2032, 2033, 2138, 2140, 2139]; -const TRINKETS: [u32; 3] = [3340, 3364, 3363]; -const ITEM_TYPES: &'static [(&str, [&str; 2]); 4] = &[ - ("Most Frequent Starters", ["firstItems", "mostGames"]), - ( - "Highest Win % Starters", - ["firstItems", "highestWinPercent"], - ), - ("Most Frequent Core Build", ["items", "mostGames"]), - ("Highest Win % Core Build", ["items", "highestWinPercent"]), -]; +#[derive(Serialize, Deserialize, Debug)] +pub struct Build { + #[serde(rename = "type")] + pub type_: String, + pub items: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Item { + pub id: String, + pub count: u8, +} pub trait DataSource { + fn get_alias(&self) -> &str; + fn get_champs_with_positions_and_patch( &self, client: &reqwest::Client, ) -> (IndexMap>, String); - fn get_champ_data(&self, id: &str, position: &str, client: &reqwest::Client) -> Option; - fn make_item_set(&self, data: &Value, label: &str) -> Value { - json!({ - "items": data["items"].as_array().unwrap().iter().map(|x| json!({"id": x["id"].as_str(), "count": 1})).collect::>(), - "type": format!("{} ({:.2}% - {} games)", label, data["winPercent"].as_f64().unwrap() * 100., data["games"].as_u64().unwrap()) - }) - } - - fn make_item_set_from_list( + fn get_champ_data_with_win_pourcentage( &self, - list: &Vec, - label: &str, - key: &str, - data: &Value, - ) -> Value { - let mut key_order = String::new(); - if !data["skills"].get("skillInfo").is_none() { - key_order = data["skills"][key]["order"] - .as_array() - .unwrap() - .iter() - .map(|x| { - data["skills"]["skillInfo"].as_array().unwrap() - [x.as_str().unwrap().parse::().unwrap() - 1]["key"] - .as_str() - .unwrap() - }) - .collect::>() - .join("."); - } - json!({ - "items": list.iter().map(|x| json!({"id": x.to_string(), "count": 1})).collect::>(), - "type": format!("{} {}", label, key_order) - }) - } + id: &str, + position: &str, + client: &reqwest::Client, + ) -> Option<(Vec, f64)>; + fn write_item_set( &self, id: &str, @@ -80,49 +55,23 @@ pub trait DataSource { client: &reqwest::Client, ) { info!("Retrieving data for {} at {}", name, pos); - let data = self.get_champ_data(id, pos, client); + let data = self.get_champ_data_with_win_pourcentage(id, pos, client); match data { Some(data) => { - let mut item_set = ItemSet { - title: format!( - "CGG {} {} - {:.2}%", - pos, - ver, - data["stats"]["winRate"].as_f64().unwrap() * 100. - ), + let item_set = ItemSet { + title: format!("{} {} {} - {:.2}%", self.get_alias(), pos, ver, data.1), type_: "custom".to_string(), map: "any".to_string(), mode: "any".to_string(), priority: false, sortrank: 0, - blocks: vec![], + blocks: data.0, }; - for (label, path) in ITEM_TYPES.iter() { - if !data[&path[0]].get(&path[1]).is_none() { - item_set - .blocks - .push(self.make_item_set(&data[&path[0]][&path[1]], label)); - } - } - - item_set.blocks.push(self.make_item_set_from_list( - &CONSUMABLES.to_vec(), - "Consumables | Frequent:", - "mostGames", - &data, - )); - item_set.blocks.push(self.make_item_set_from_list( - &TRINKETS.to_vec(), - "Trinkets | Wins:", - "highestWinPercent", - &data, - )); - info!("Writing item set for {} at {}", name, pos); fs::write( - path.join(format!("CGG_{}_{}.json", id, pos)), + path.join(format!("{}_{}_{}.json", self.get_alias(), id, pos)), serde_json::to_string_pretty(&item_set).unwrap(), ) .unwrap(); diff --git a/src/kb_data_source.rs b/src/kb_data_source.rs new file mode 100644 index 0000000..67efe9f --- /dev/null +++ b/src/kb_data_source.rs @@ -0,0 +1,392 @@ +use crate::data_source::{Build, DataSource, Item}; +use indexmap::IndexMap; +use regex::Regex; +use serde_derive::Deserialize; +use serde_json::{json, Value}; +use std::time::Duration; + +pub struct KBDataSource { + token: Option, + internal_classname_mapping: IndexMap, +} + +#[derive(Deserialize, Debug)] +struct ChampionResponse { + patches: Vec, + champions: Vec, +} + +#[derive(Deserialize, Debug)] +struct Patch { + enabled: bool, + #[serde(rename = "patchVersion")] + patch_version: String, + patchid: u32, + start: String, +} + +#[derive(Deserialize, Debug)] +struct Champion { + id: u32, + name: String, + #[serde(rename = "className")] + classname: String, + builds: Option, +} + +#[derive(Deserialize, Debug)] +struct Position { + bot: u16, + jungle: u16, + mid: u16, + support: u16, + top: u16, +} + +#[derive(Deserialize, Debug)] +struct BuildResponse { + builds2: Vec, +} + +#[derive(Deserialize, Debug)] +struct KBBuild { + item0: KBItem, + item1: KBItem, + item2: KBItem, + item3: KBItem, + item4: KBItem, + item5: KBItem, + item6: KBItem, + #[serde(rename = "startItem0")] + start_item0: KBItem, + #[serde(rename = "startItem1")] + start_item1: KBItem, + #[serde(rename = "startItem2")] + start_item2: KBItem, + #[serde(rename = "startItem3")] + start_item3: KBItem, + #[serde(rename = "startItem4")] + start_item4: KBItem, + #[serde(rename = "startItem5")] + start_item5: KBItem, + #[serde(rename = "skillOrder")] + skill_order: String, + wins: f64, + games: f64, + summoner: Summoner, +} + +#[derive(Deserialize, Debug)] +struct KBItem { + #[serde(rename = "itemId")] + item_id: u32, + name: String, +} + +#[derive(Deserialize, Debug)] +struct Summoner { + name: String, +} + +impl KBDataSource { + pub fn new(client: &reqwest::Client) -> KBDataSource { + let mut datasource = KBDataSource { + token: None, + internal_classname_mapping: IndexMap::new(), + }; + datasource.token = datasource.get_auth_token(client); + datasource.internal_classname_mapping = datasource.get_classname_mapping(client); + datasource + } + + // It will be better to use Result... + fn get_auth_token(&self, client: &reqwest::Client) -> Option { + let bundle = match client.get("https://koreanbuilds.net/bundle.js").send() { + Ok(mut resp) => match resp.text() { + Ok(val) => val, + Err(_) => return None, + }, + Err(_) => return None, + }; + let regex = match Regex::new(r##"Authorization:\s*"(\w+)""##) { + Ok(reg) => reg, + Err(_) => return None, + }; + let result = match regex.captures(&bundle) { + Some(res) => res, + None => return None, + }; + return match result.get(1) { + Some(token) => Some(token.as_str().to_string()), + None => None, + }; + } + + fn get_classname_mapping(&self, client: &reqwest::Client) -> IndexMap { + let mut mapping = IndexMap::new(); + match self.get_champion_response(client) { + Some(data) => { + for champ in data.champions { + mapping.insert(champ.classname, champ.name); + } + } + None => { /* Nothing to do */ } + }; + mapping + } + + fn get_champion_response(&self, client: &reqwest::Client) -> Option { + let token = match self.token.clone() { + Some(t) => t, + None => return None, + }; + match client + .get("https://api.koreanbuilds.net/champions?patchid=-1") + .header("Accept", "application/json") + .header("Authorization", token) + .send() + { + Ok(mut resp) => match resp.json() { + Ok(val) => return val, + Err(_) => return None, + }, + Err(_) => return None, + }; + } + + fn get_positions(position: Option) -> Vec { + let mut positions = Vec::new(); + match position { + Some(pos) => { + if pos.top > 0 { + positions.push("TOP".to_owned()); + } + if pos.jungle > 0 { + positions.push("JUNGLE".to_owned()); + } + if pos.mid > 0 { + positions.push("MID".to_owned()); + } + if pos.bot > 0 { + positions.push("BOT".to_owned()); + } + if pos.support > 0 { + positions.push("SUPPORT".to_owned()); + } + } + None => { /* Nothing to do */ } + } + positions + } +} + +impl DataSource for KBDataSource { + fn get_alias(&self) -> &str { + "KB" + } + + fn get_champs_with_positions_and_patch( + &self, + client: &reqwest::Client, + ) -> (IndexMap>, String) { + let mut champions = IndexMap::new(); + let data: ChampionResponse = match self.get_champion_response(client) { + Some(val) => val, + None => { + return (champions, String::new()); + } + }; + let patch = match data.patches.get(0) { + Some(p) => p.patch_version.clone(), + None => return (champions, String::new()), + }; + for champ in data.champions { + champions.insert(champ.classname, KBDataSource::get_positions(champ.builds)); + } + (champions, patch) + } + + fn get_champ_data_with_win_pourcentage( + &self, + id: &str, + position: &str, + client: &reqwest::Client, + ) -> Option<(Vec, f64)> { + let token = match self.token.clone() { + Some(t) => t, + None => return None, + }; + let map_id = match self.internal_classname_mapping.get(id) { + Some(m_id) => m_id, + None => return None, + }; + let data: BuildResponse = match client + .get(&format!( + "https://api.koreanbuilds.net/builds?chmpname={}&patchid=-2&position={}", + map_id, position + )) + .header("Accept", "application/json") + .header("Authorization", token) + .send() + { + Ok(mut resp) => match resp.json() { + Ok(val) => val, + Err(_) => { + return None; + } + }, + Err(_) => { + return None; + } + }; + let mut blocks = vec![]; + let winrate = (data.builds2[0].wins / data.builds2[0].games) * 100.; + let mut starting_items: Vec = vec![]; + if data.builds2[0].start_item0.item_id != 0 { + starting_items.push(Item { + id: data.builds2[0].start_item0.item_id.to_string(), + count: 1, + }) + } + if data.builds2[0].start_item1.item_id != 0 { + starting_items.push(Item { + id: data.builds2[0].start_item1.item_id.to_string(), + count: 1, + }) + } + if data.builds2[0].start_item2.item_id != 0 { + starting_items.push(Item { + id: data.builds2[0].start_item2.item_id.to_string(), + count: 1, + }) + } + if data.builds2[0].start_item3.item_id != 0 { + starting_items.push(Item { + id: data.builds2[0].start_item3.item_id.to_string(), + count: 1, + }) + } + if data.builds2[0].start_item4.item_id != 0 { + starting_items.push(Item { + id: data.builds2[0].start_item4.item_id.to_string(), + count: 1, + }) + } + if data.builds2[0].start_item5.item_id != 0 { + starting_items.push(Item { + id: data.builds2[0].start_item5.item_id.to_string(), + count: 1, + }) + } + blocks.push(json!(Build { + type_: format!( + "Early game items | skillOrder : {}", + data.builds2[0].skill_order + ), + items: starting_items + })); + let mut final_items: Vec = vec![]; + if data.builds2[0].item0.item_id != 0 { + final_items.push(Item { + id: data.builds2[0].item0.item_id.to_string(), + count: 1, + }) + } + if data.builds2[0].item1.item_id != 0 { + final_items.push(Item { + id: data.builds2[0].item1.item_id.to_string(), + count: 1, + }) + } + if data.builds2[0].item2.item_id != 0 { + final_items.push(Item { + id: data.builds2[0].item2.item_id.to_string(), + count: 1, + }) + } + if data.builds2[0].item3.item_id != 0 { + final_items.push(Item { + id: data.builds2[0].item3.item_id.to_string(), + count: 1, + }) + } + if data.builds2[0].item4.item_id != 0 { + final_items.push(Item { + id: data.builds2[0].item4.item_id.to_string(), + count: 1, + }) + } + if data.builds2[0].item5.item_id != 0 { + final_items.push(Item { + id: data.builds2[0].item5.item_id.to_string(), + count: 1, + }) + } + if data.builds2[0].item6.item_id != 0 { + final_items.push(Item { + id: data.builds2[0].item6.item_id.to_string(), + count: 1, + }) + } + blocks.push(json!(Build { + type_: format!( + "Item order by time finished | Summoner : {}", + data.builds2[0].summoner.name + ), + items: final_items + })); + Some((blocks, winrate)) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_get_auth_token() { + let datasource = KBDataSource { + token: None, + internal_classname_mapping: IndexMap::new(), + }; + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(10)) + .build() + .unwrap(); + match datasource.get_auth_token(&client) { + Some(token) => assert!(token.len() > 0), + None => assert!(false), + }; + } + + #[test] + fn test_get_champs_with_positions_and_patch() { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(10)) + .build() + .unwrap(); + let datasource = KBDataSource::new(&client); + let champs_with_positions_and_patch = + datasource.get_champs_with_positions_and_patch(&client); + assert!(champs_with_positions_and_patch.0.len() > 0); + } + + #[test] + fn test_get_champ_data_with_win_pourcentage() { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(10)) + .build() + .unwrap(); + let datasource = KBDataSource::new(&client); + let result = datasource.get_champ_data_with_win_pourcentage("Aatrox", "TOP", &client); + assert!(result.is_some()); + match result { + Some(value) => { + assert!(value.0.len() > 0); + assert!(value.1 > 0.); + } + None => assert!(false), + } + } +} diff --git a/src/main.rs b/src/main.rs index d25899c..874808e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,23 @@ use indexmap::IndexMap; +use log::{error, info}; use reqwest::header; +use serde_derive::Deserialize; +use simple_logger; use std::io::{Error, ErrorKind}; use std::path::PathBuf; use std::{fs, thread, time}; use time::Duration; use winreg::RegKey; -use simple_logger; -use log::{info, error}; -use serde_derive::{Deserialize}; mod cgg_data_source; mod data_source; +mod kb_data_source; mod pb_data_source; use cgg_data_source::CGGDataSource; -use pb_data_source::PBDataSource; use data_source::DataSource; +use kb_data_source::KBDataSource; +use pb_data_source::PBDataSource; #[derive(Deserialize)] struct Realm { @@ -78,13 +80,21 @@ fn main() { .unwrap(); info!("LoL numbers of champs: {}", champion.data.len()); - let data_sources: [Box; 2] = [Box::new(PBDataSource), Box::new(CGGDataSource)]; + let data_sources: [Box; 3] = [ + Box::new(PBDataSource), + Box::new(CGGDataSource), + Box::new(KBDataSource::new(&client)), + ]; for data_source in data_sources.iter() { let (champs, patch) = data_source.get_champs_with_positions_and_patch(&client); - info!("CGG version: {}", patch); - info!("CGG numbers of champs: {}", champs.len()); + info!("{} version: {}", data_source.get_alias(), patch); + info!( + "{} numbers of champs: {}", + data_source.get_alias(), + champs.len() + ); for (id, positions) in &champs { if champion.data.contains_key(id) { diff --git a/src/pb_data_source.rs b/src/pb_data_source.rs index bf21341..aa5159f 100644 --- a/src/pb_data_source.rs +++ b/src/pb_data_source.rs @@ -5,6 +5,10 @@ use crate::data_source::DataSource; pub struct PBDataSource; impl DataSource for PBDataSource { + fn get_alias(&self) -> &str { + "PB" + } + fn get_champs_with_positions_and_patch( &self, _client: &reqwest::Client, @@ -12,12 +16,12 @@ impl DataSource for PBDataSource { (IndexMap::new(), String::new()) } - fn get_champ_data( + fn get_champ_data_with_win_pourcentage( &self, _id: &str, _position: &str, _client: &reqwest::Client, - ) -> Option { + ) -> Option<(Vec, f64)> { None } }