stat_summoner/module/lolstats/
utils.rs

1use mongodb::Collection;
2use poise::CreateReply;
3use reqwest::Client;
4use serde_json::{Map, Value};
5use std::collections::HashMap;
6
7use crate::embed::create_embed;
8use crate::models::constants::QUEUE_ID_MAP;
9use crate::models::data::{Data, EmojiId};
10use crate::models::error::Error;
11use crate::models::modal::LolStatsModal;
12use crate::riot_api::get_matchs_info;
13use crate::utils::{get_emoji, is_valid_game_mode, seconds_to_time, time_since_game_ended};
14
15/// ⚙️ **Function**: Fetches data and creates an embed displaying League of Legends player stats and match details.
16///
17/// This function orchestrates the process of fetching rank, champion, and match data, and formats this information
18/// into an embed message. The embed is then prepared for sending in a Discord channel.
19///
20/// # Parameters:
21/// - `modal_data`: A modal containing the player's in-game name and tag, used to personalize the embed title.
22/// - `summoner_id`: The unique ID of the summoner (player) whose data is being fetched. This is used to query relevant match and rank data.
23/// - `solo_rank`: A HashMap containing the player's Solo/Duo rank information, such as tier, LP, wins, losses, and winrate.
24/// - `flex_rank`: A HashMap containing the player's Flex rank information, structured similarly to `solo_rank`.
25/// - `champions`: A vector of HashMaps, where each HashMap contains information about the player's top champions (e.g., champion level and mastery points).
26/// - `match_ids`: A vector of match IDs representing recent matches played by the user.
27/// - `ctx`: The application context, which includes methods for interacting with Discord and accessing API keys for fetching data.
28///
29/// # Returns:
30/// - `CreateReply`: A formatted reply containing the embed message, ready to be sent to a Discord channel.
31///
32/// # ⚠️ Notes:
33/// - The function fetches champion data from Data Dragon and match data from the Riot API, ensuring that up-to-date information is displayed.
34/// - If no match details are found, the embed will indicate that no recent ranked or normal matches were played.
35/// - The function extracts and formats data for Solo/Duo and Flex ranks, as well as champion and match details.
36///
37/// # Example:
38/// ```rust
39/// let embed_reply = create_and_send_embed_lolstats(modal_data, summoner_id, &solo_rank, &flex_rank, champions, match_ids, &ctx).await;
40/// ctx.send(embed_reply).await?;
41/// ```
42///
43/// The resulting embed message will contain player stats like:
44/// ```text
45/// 📊 Stats for Faker#1234
46/// 🔱 **Solo/Duo Rank**: Gold I (100 LP)
47/// 🌀 **Flex Rank**: Silver IV (50 LP)
48/// 💥 **Top Champions**:
49/// Yasuo - Level: 7 - Points: 123456
50/// 📜 **Match Details**:
51/// Victory - **Yasuo**, 2 hours ago (Ranked Solo/Duo):
52/// K/D/A: **10/2/8** | **200 CS** | Duration: **30:45**
53/// ⏳ Played: **2 hours ago**
54/// ```
55pub async fn create_and_send_embed_lolstats(
56    modal_data: &LolStatsModal,
57    summoner_id: String,
58    solo_rank: &HashMap<String, Value>,
59    flex_rank: &HashMap<String, Value>,
60    champions: Vec<HashMap<String, Value>>,
61    match_ids: Vec<String>,
62    ctx: &poise::ApplicationContext<'_, Data, Error>,
63    collection_emoji: Collection<EmojiId>,
64) -> CreateReply {
65    let dd_json = &*ctx.data().dd_json.read().await;
66    let champions_data = dd_json["data"].as_object().unwrap();
67
68    let solo_rank = extract_rank_info(solo_rank);
69    let flex_rank = extract_rank_info(flex_rank);
70    let champions_info =
71        extract_champions_info(champions, champions_data, collection_emoji.clone()).await;
72    let match_details = extract_match_info(match_ids, ctx, summoner_id).await;
73
74    let embed = create_embed(
75        modal_data,
76        solo_rank,
77        flex_rank,
78        champions_info,
79        match_details,
80        collection_emoji.clone(),
81    )
82    .await
83    .unwrap();
84
85    CreateReply {
86        embeds: vec![embed],
87        ..Default::default()
88    }
89}
90
91/// ⚙️ **Function**: Extracts and returns League of Legends rank information.
92///
93/// This function processes rank data to extract key details such as tier, division, league points (LP),
94/// wins, losses, and winrate. The resulting information is formatted into a JSON-like value for use in
95/// other parts of the application, such as creating embeds for Discord.
96///
97/// # Parameters:
98/// - `rank_data`: A HashMap containing the player's rank information, typically fetched from the Riot API.
99///   This data includes keys such as `"tier"`, `"rank"`, `"leaguePoints"`, `"wins"`, and `"losses"`.
100///
101/// # Returns:
102/// - `Value`: A JSON-like value containing the extracted rank information:
103///     - `tier`: The rank tier (e.g., "Gold", "Platinum"), defaults to "Unranked" if not present.
104///     - `division`: The rank division (e.g., "I", "II"), empty if not present.
105///     - `lp`: League points, defaults to 0 if not present.
106///     - `wins`: Number of wins, defaults to 0 if not present.
107///     - `losses`: Number of losses, defaults to 0 if not present.
108///     - `winrate`: The player's winrate, calculated as `wins / (wins + losses)`, defaults to 0 if no games are played.
109///
110/// # ⚠️ Notes:
111/// - If the player is unranked or data is missing, the function will return default values such as `"Unranked"` for
112///   the tier, and `0` for LP, wins, and losses.
113/// - The winrate is calculated as a percentage and will return `0.0%` if there are no games played (i.e., wins + losses = 0).
114///
115/// # Example:
116/// ```rust
117/// let rank_data = some_function_fetching_rank_data();
118/// let rank_info = extract_rank_info(&rank_data);
119/// ```
120///
121/// The resulting `rank_info` will be in the following format:
122/// ```json
123/// {
124///     "tier": "Gold",
125///     "division": "II",
126///     "lp": 45,
127///     "wins": 20,
128///     "losses": 15,
129///     "winrate": 57.14
130/// }
131/// ```
132fn extract_rank_info(rank_data: &HashMap<String, Value>) -> Value {
133    let tier = rank_data
134        .get("tier")
135        .and_then(|v| v.as_str())
136        .unwrap_or("Unranked");
137    let division = rank_data.get("rank").and_then(|v| v.as_str()).unwrap_or("");
138    let lp = rank_data
139        .get("leaguePoints")
140        .and_then(|v| v.as_i64())
141        .unwrap_or(0);
142    let wins = rank_data.get("wins").and_then(|v| v.as_i64()).unwrap_or(0);
143    let losses = rank_data
144        .get("losses")
145        .and_then(|v| v.as_i64())
146        .unwrap_or(0);
147    let winrate = if wins + losses > 0 {
148        ((wins as f64) / ((wins + losses) as f64)) * 100.0
149    } else {
150        0.0
151    };
152    return serde_json::json!({
153        "tier": tier,
154        "division": division,
155        "lp": lp,
156        "wins": wins,
157        "losses": losses,
158        "winrate": winrate
159    });
160}
161
162/// ⚙️ **Function**: Extracts and formats champion information for display.
163///
164/// This function processes a list of champion details and matches each champion ID to the corresponding
165/// champion name from the provided champion data (typically fetched from Data Dragon). It then formats
166/// and returns a string that includes each champion's name, level, and mastery points.
167///
168/// # Parameters:
169/// - `champions`: A vector of HashMaps, where each HashMap contains information about a player's champion
170///   (e.g., champion ID, level, mastery points). This is typically fetched from the Riot API.
171/// - `champions_data`: A HashMap containing the full list of champion data from Data Dragon, which is used
172///   to map champion IDs to their names.
173///
174/// # Returns:
175/// - `String`: A formatted string containing information about each champion:
176///     - Champion name
177///     - Champion level
178///     - Champion mastery points
179///
180/// The returned string will display each champion on a new line, formatted like this:
181/// ```text
182/// Yasuo - Level: 7 - Points: 123456
183/// Zed - Level: 6 - Points: 98765
184/// Lee Sin - Level: 5 - Points: 54321
185/// ```
186///
187/// # ⚠️ Notes:
188/// - If a champion's ID cannot be matched to a name in `champions_data`, the champion will be listed as "Unknown Champion".
189/// - This function assumes that every champion in the `champions` list has valid data for level and mastery points.
190///
191/// # Example:
192/// ```rust
193/// let champions = some_function_fetching_champions();
194/// let champions_data = some_function_fetching_champion_data();
195/// let formatted_champions = extract_champions_info(champions, champions_data);
196/// ```
197///
198/// The resulting `formatted_champions` string will be:
199/// ```text
200/// Yasuo - Level: 7 - Points: 123456
201/// Zed - Level: 6 - Points: 98765
202/// Lee Sin - Level: 5 - Points: 54321
203/// ```
204async fn extract_champions_info(
205    champions: Vec<HashMap<String, Value>>,
206    champions_data: &Map<String, Value>,
207    collection_emoji: Collection<EmojiId>,
208) -> String {
209    let mut champion_info_strings = Vec::new();
210
211    for champion in champions {
212        let champion_id = champion
213            .get("championId")
214            .unwrap()
215            .as_i64()
216            .unwrap()
217            .to_string();
218        let champion_name = champions_data
219            .values()
220            .find_map(|data| {
221                let champ = data.as_object().unwrap();
222                if champ.get("key").unwrap() == &Value::String(champion_id.clone()) {
223                    Some(champ.get("id").unwrap().as_str().unwrap())
224                } else {
225                    None
226                }
227            })
228            .unwrap_or("Unknown Champion");
229
230        let champion_level = champion.get("championLevel").unwrap().as_i64().unwrap();
231        let champion_points = champion.get("championPoints").unwrap().as_i64().unwrap();
232        let champion_emoji = get_emoji(collection_emoji.clone(), "champions", champion_name)
233            .await
234            .unwrap_or(champion_name.to_string());
235        champion_info_strings.push(format!(
236            "{} - Level: {} - Points: {}",
237            champion_emoji, champion_level, champion_points
238        ));
239    }
240    champion_info_strings.join("\n")
241}
242
243/// ⚙️ **Function**: Extracts detailed information from recent League of Legends matches.
244///
245/// This function processes a list of match IDs, fetching and extracting key match information
246/// such as champion played, kills, deaths, assists (K/D/A), total farm, game duration, and
247/// match outcome (victory or defeat). The extracted data is returned as a vector of JSON-like
248/// values for use in other parts of the application, such as creating embeds for Discord.
249///
250/// # Parameters:
251/// - `match_ids`: A vector of match IDs to fetch and process. Each ID corresponds to a recent match played by the user.
252/// - `ctx`: The application context, which includes the Riot API key for fetching match data and methods for interacting with Discord.
253/// - `summoner_id`: The unique ID of the summoner (player) whose match data is being processed. This is used to find the player's data within each match.
254///
255/// # Returns:
256/// - `Vec<Value>`: A vector of JSON-like values, where each entry contains information about a match:
257///     - `champion_name`: The name of the champion played in the match.
258///     - `K/D/A`: The player's kills, deaths, and assists in the match.
259///     - `Farm`: The total number of minions and neutral monsters killed.
260///     - `Result`: The outcome of the match (Victory or Defeat).
261///     - `Duration`: The duration of the match in minutes and seconds.
262///     - `time_elapsed`: The time since the match ended, formatted as seconds, minutes, hours, or days ago.
263///     - `game_type`: The type of game played (e.g., Ranked Solo/Duo, ARAM).
264///
265/// # ⚠️ Notes:
266/// - Only matches with a valid game mode (as determined by `is_valid_game_mode()`) are processed.
267/// - If a match does not contain the player's data, it is skipped.
268/// - The function uses the `time_since_game_ended` utility to calculate how long ago the match was played.
269///
270/// # Example:
271/// ```rust
272/// let match_ids = vec!["EUW1_1234567890", "EUW1_0987654321"];
273/// let match_info = extract_match_info(match_ids, ctx, summoner_id).await;
274/// ```
275///
276/// The resulting `match_info` vector will contain data for each match, such as:
277/// ```json
278/// [
279///   {
280///     "champion_name": "Yasuo",
281///     "K/D/A": "10/2/8",
282///     "Farm": 220,
283///     "Result": "Victory",
284///     "Duration": "30:12",
285///     "time_elapsed": "2 hours ago",
286///     "game_type": "Ranked Solo/Duo"
287///   },
288///   {
289///     "champion_name": "Zed",
290///     "K/D/A": "7/5/10",
291///     "Farm": 180,
292///     "Result": "Defeat",
293///     "Duration": "28:45",
294///     "time_elapsed": "1 day ago",
295///     "game_type": "Ranked Flex"
296///   }
297/// ]
298/// ```
299async fn extract_match_info(
300    match_ids: Vec<String>,
301    ctx: &poise::ApplicationContext<'_, Data, Error>,
302    puuid: String,
303) -> Vec<Value> {
304    let mut match_details = Vec::<Value>::new();
305    for id in &match_ids {
306        let info = get_matchs_info(&Client::new(), id, &ctx.data().riot_api_key)
307            .await
308            .unwrap();
309        let queue_id = info["info"]["queueId"].as_i64().unwrap_or(-1);
310        if is_valid_game_mode(queue_id) {
311            let participants = info["info"]["participants"].as_array().unwrap();
312            if let Some(participant) = participants
313                .iter()
314                .find(|p| p["puuid"].as_str().unwrap() == puuid)
315            {
316                let champion_name = participant["championName"].as_str().unwrap_or("Unknown");
317                let kills = participant["kills"].as_u64().unwrap_or(0);
318                let deaths = participant["deaths"].as_u64().unwrap_or(0);
319                let assists = participant["assists"].as_u64().unwrap_or(0);
320                let total_farm = participant["totalMinionsKilled"].as_u64().unwrap_or(0)
321                    + participant["neutralMinionsKilled"].as_u64().unwrap_or(0);
322                let win = participant["win"].as_bool().unwrap_or(false);
323                let game_result = if win { "Victory" } else { "Defeat" };
324                let game_duration = info["info"]["gameDuration"].as_u64().unwrap_or(0);
325                let game_end_timestamp = info["info"]["gameEndTimestamp"].as_u64().unwrap_or(0);
326                let time_since_game_ended = time_since_game_ended(game_end_timestamp);
327                let (game_duration_minutes, game_duration_seconds) = seconds_to_time(game_duration);
328                let game_type = QUEUE_ID_MAP
329                    .iter()
330                    .find(|(id, _)| *id == queue_id)
331                    .unwrap()
332                    .1;
333                match_details.push(serde_json::json!({
334                    "champion_name": champion_name,
335                    "K/D/A": format!("{}/{}/{}", kills, deaths, assists),
336                    "Farm": total_farm,
337                    "Result": game_result,
338                    "Duration": format!("{}:{}", game_duration_minutes, game_duration_seconds),
339                    "time_elapsed": time_since_game_ended,
340                    "game_type": game_type
341                }));
342            }
343        }
344    }
345    match_details
346}