use crate::ChampInfo; use crate::Champion as ChampionLoL; use crate::data_source::{Data, DataSource, Item, Stat}; use indexmap::IndexMap; use log::error; use serde_derive::Deserialize; pub struct KBDataSource { client: ureq::Agent, token: Option, } #[derive(Deserialize)] struct ChampionResponse { //patches: Vec, champions: Vec, } #[derive(Deserialize)] struct Patch { //enabled: bool, #[serde(rename = "patchVersion")] patch_version: String, //patchid: u32, //start: String, } #[derive(Deserialize)] struct Champion { id: u32, //name: String, //#[serde(rename = "className")] //classname: String, builds: Option, } #[derive(Deserialize)] struct Position { bot: u16, jungle: u16, mid: u16, support: u16, top: u16, } #[derive(Deserialize)] struct BuildResponse { builds3: Vec, } #[derive(Deserialize)] struct KBBuild { position: PositionResponse, #[serde(rename = "itemSets")] item_sets: Vec, #[serde(rename = "strItemSets")] str_item_sets: Vec, #[serde(rename = "skillSets")] skill_sets: Vec, wins: u32, games: u32, kda: f32, summoner: Summoner, patch: Patch, } #[derive(Deserialize)] struct PositionResponse { #[serde(rename = "positionName")] position_name: String, } #[derive(Deserialize)] struct SkillSet { #[serde(rename = "skillOrder")] skill_order: String, } #[derive(Deserialize)] struct ItemSets { item0: KBItem, item1: KBItem, item2: KBItem, item3: KBItem, item4: KBItem, item5: KBItem, } #[derive(Deserialize)] struct StrItemSets { #[serde(rename = "strItem0")] str_item0: KBItem, #[serde(rename = "strItem1")] str_item1: KBItem, #[serde(rename = "strItem2")] str_item2: KBItem, #[serde(rename = "strItem3")] str_item3: KBItem, #[serde(rename = "strItem4")] str_item4: KBItem, #[serde(rename = "strItem5")] str_item5: KBItem, } #[derive(Deserialize)] struct KBItem { #[serde(rename = "itemId")] item_id: u32, //name: String, } #[derive(Deserialize)] struct Summoner { name: String, } impl KBDataSource { pub fn new(client: &ureq::Agent) -> Self { Self { client: client.clone(), token: Self::fetch_auth_token(client), } } fn fetch_auth_token(client: &ureq::Agent) -> Option { let resp = client.get("https://koreanbuilds.net/bundle.js").call(); if let Ok(mut resp) = resp { if let Ok(bundle) = resp.body_mut().read_to_string() { if let Some(token) = Self::extract_token(&bundle) { return Some(token); } } } None } fn extract_token(bundle: &str) -> Option { let auth_marker = "Authorization"; let start = bundle.find(auth_marker)? + auth_marker.len(); let after_marker = bundle[start..].find('"')? + start + 1; let end = bundle[after_marker..].find('"')? + after_marker; Some(bundle[after_marker..end].to_owned()) } fn get_champion_response(&self) -> Option { if let Some(token) = &self.token { return match self .client .get("https://api.koreanbuilds.net/champions?patchid=-1") .header("Accept", "application/json") .header("Authorization", token.as_str()) .call() { Ok(mut resp) => resp.body_mut().read_json().unwrap_or_default(), Err(_) => None, }; } None } fn get_positions(position: Option) -> Vec { let positions = vec!["TOP", "JUNGLE", "MID", "BOT", "SUPPORT"]; if let Some(pos) = position { positions .into_iter() .zip([pos.top, pos.jungle, pos.mid, pos.bot, pos.support]) .filter_map(|(name, count)| { if count > 0 { Some(name.to_owned()) } else { None } }) .collect() } else { positions.into_iter().map(|s| s.to_owned()).collect() } } fn get_build(&self, build: &KBBuild) -> Data { let mut starting_items: Vec = vec![]; let mut blocks = vec![]; for i in 0..6 { let item_id = match i { 0 => build.str_item_sets[0].str_item0.item_id, 1 => build.str_item_sets[0].str_item1.item_id, 2 => build.str_item_sets[0].str_item2.item_id, 3 => build.str_item_sets[0].str_item3.item_id, 4 => build.str_item_sets[0].str_item4.item_id, 5 => build.str_item_sets[0].str_item5.item_id, _ => 0, }; if item_id != 0 { starting_items.push(Item { id: item_id.to_string(), count: 1, }); } } blocks.push(self.make_item_set( starting_items.iter().map(|item| item.id.clone()).collect(), format!( "Early game items | skillOrder : {}", build.skill_sets[0].skill_order ), )); let mut final_items: Vec = vec![]; for item in [ &build.item_sets[0].item0, &build.item_sets[0].item1, &build.item_sets[0].item2, &build.item_sets[0].item3, &build.item_sets[0].item4, &build.item_sets[0].item5, ] { if item.item_id != 0 { final_items.push(Item { id: item.item_id.to_string(), count: 1, }); } } blocks.push(self.make_item_set( final_items.iter().map(|item| item.id.clone()).collect(), format!( "Item order by time finished | Summoner : {}", build.summoner.name ), )); Data { position: build.position.position_name.to_uppercase(), items: blocks, stat: Stat { win_rate: (build.wins as f32 / build.games as f32) * 100f32, games: build.games, kda: build.kda, patch: build.patch.patch_version.to_owned(), }, } } } impl DataSource for KBDataSource { fn get_alias(&self) -> &str { "KB" } fn get_champs_with_positions(&self, _champion: &ChampionLoL) -> IndexMap> { let mut champions = IndexMap::new(); let data: ChampionResponse = match self.get_champion_response() { Some(val) => val, None => { return champions; } }; for champ in data.champions { champions.insert(champ.id, KBDataSource::get_positions(champ.builds)); } champions } fn get_champ_data_with_win_pourcentage( &self, champ: &ChampInfo, position: &[String], ) -> Vec { let mut champ_data = vec![]; if let Some(token) = &self.token { let url = format!( "https://api.koreanbuilds.net/builds?chmpname={}&patchid=-2&position=COMPOSITE", champ.name.replace(" ", "%20") ); let data: BuildResponse = match self .client .get(&url) .header("Accept", "application/json") .header("Authorization", token.as_str()) .call() { Ok(mut resp) => match resp.body_mut().read_json() { Ok(val) => val, Err(x) => { error!("Cant json: {x}"); return vec![]; } }, Err(x) => { error!("Call failed for URL: {url}, error: {x}"); return vec![]; } }; for pos in position { for b in &data.builds3 { if b.position.position_name.to_uppercase() == *pos { champ_data.push(self.get_build(b)); break; } } } } champ_data } } #[cfg(test)] mod tests { use super::*; use crate::create_http_client; use std::sync::LazyLock; static DATASOURCE: LazyLock = LazyLock::new(|| KBDataSource::new(&create_http_client())); #[test] fn test_get_auth_token() { match &DATASOURCE.token { Some(token) => assert!(token.len() > 0), None => assert!(false), }; } #[test] fn test_get_champs_with_positions_and_patch() { let champion = ChampionLoL { data: IndexMap::new(), }; let champs_with_positions = DATASOURCE.get_champs_with_positions(&champion); assert!(champs_with_positions.len() > 0); } #[test] fn test_get_champ_data_with_win_pourcentage() { let champ = ChampInfo { id: String::from("Annie"), name: String::from("Annie"), key: 1, }; let result = DATASOURCE.get_champ_data_with_win_pourcentage(&champ, &vec!["MID".to_owned()]); assert!(!result.is_empty()); assert!(!result[0].items.is_empty()); assert!(result[0].stat.win_rate > 0.); } }