stat_summoner/utils.rs
1use crate::models::constants::QUEUE_ID_MAP;
2use crate::models::data::{EmojiId, User};
3use crate::models::region::Region;
4use chrono::{NaiveDateTime, Utc};
5use mongodb::bson::{doc, Bson};
6use mongodb::{Client, Collection};
7use serde::de::value::Error;
8use serde_json::Value;
9use std::collections::HashMap;
10
11/// ⚙️ **Function**: Checks if a given queue ID corresponds to a valid game mode.
12///
13/// This function verifies if the provided `queue_id` matches any valid game modes listed in the `QUEUE_ID_MAP`.
14/// The `QUEUE_ID_MAP` contains a predefined set of game modes such as ranked, normal, and ARAM.
15///
16/// # Parameters:
17/// - `queue_id`: The ID of the game queue (e.g., Ranked Solo/Duo, ARAM) to validate.
18///
19/// # Returns:
20/// - `bool`: Returns `true` if the `queue_id` matches a valid game mode in `QUEUE_ID_MAP`, otherwise returns `false`.
21///
22/// # ⚠️ Notes:
23/// - `QUEUE_ID_MAP` contains predefined game modes, so any queue ID not included in this map will return `false`.
24/// - This function is useful for filtering out game modes that aren't relevant or valid for certain statistics (e.g., custom games).
25///
26/// # Example:
27/// ```rust
28/// let is_valid = is_valid_game_mode(420); // Ranked Solo/Duo
29/// if is_valid {
30/// println!("This is a valid game mode.");
31/// }
32/// ```
33///
34/// If `queue_id` is valid, such as `420` for Ranked Solo/Duo, the result will be:
35/// ```text
36/// true
37/// ```
38pub fn is_valid_game_mode(queue_id: i64) -> bool {
39 QUEUE_ID_MAP.iter().any(|&(id, _)| id == queue_id)
40}
41
42/// ⚙️ **Function**: Calculates the time elapsed since a game ended and returns it as a human-readable string.
43///
44/// This function computes the duration between the game's end timestamp and the current time. It returns a string
45/// representing how much time has passed, formatted in seconds, minutes, hours, days, months, or years, depending on the duration.
46///
47/// # Parameters:
48/// - `game_end_timestamp`: A UNIX timestamp (in milliseconds) representing when the game ended.
49///
50/// # Returns:
51/// - `String`: A human-readable string representing how long ago the game ended (e.g., "5 minutes ago", "2 hours ago").
52///
53/// # ⚠️ Notes:
54/// - The function converts the timestamp from milliseconds to seconds before performing the calculation.
55/// - If the duration is less than 60 seconds, the result will be in seconds. If it's less than 24 hours, the result will be in minutes or hours, and so on.
56///
57/// # Example:
58/// ```rust
59/// let time_elapsed = time_since_game_ended(1625000000000);
60/// println!("{}", time_elapsed); // Output: "5 hours ago"
61/// ```
62///
63/// The resulting string will vary depending on the duration since the game ended:
64/// ```text
65/// "2 minutes ago"
66/// "5 days ago"
67/// "1 year ago"
68/// ```
69pub fn time_since_game_ended(game_end_timestamp: u64) -> String {
70 #[allow(deprecated)]
71 let game_end_time = NaiveDateTime::from_timestamp_opt((game_end_timestamp / 1000) as i64, 0)
72 .expect("Invalid timestamp");
73 let now = Utc::now().naive_utc();
74 let duration = now.signed_duration_since(game_end_time);
75
76 if duration.num_seconds() < 60 {
77 format!("{} seconds ago", duration.num_seconds())
78 } else if duration.num_minutes() < 60 {
79 format!("{} minutes ago", duration.num_minutes())
80 } else if duration.num_hours() < 24 {
81 format!("{} hours ago", duration.num_hours())
82 } else if duration.num_days() < 30 {
83 format!("{} days ago", duration.num_days())
84 } else if duration.num_days() < 365 {
85 format!("{} months ago", duration.num_days() / 30)
86 } else {
87 format!("{} years ago", duration.num_days() / 365)
88 }
89}
90
91/// ⚙️ **Function**: Determines Solo/Duo and Flex ranks from rank information.
92///
93/// This function analyzes a list of rank information and determines the Solo/Duo and Flex ranks based on the provided data.
94/// It checks the `queueType` field in the rank data to distinguish between Solo/Duo and Flex ranks. If no rank data is available
95/// for a specific queue, it returns a default rank.
96///
97/// # Parameters:
98/// - `rank_info`: A vector containing rank information in the form of a list of `HashMap<String, serde_json::Value>`. Each `HashMap` represents a rank type with various rank data, including `queueType`.
99/// - `default_rank`: A default rank (`HashMap<String, serde_json::Value>`) to return if the corresponding rank information is missing.
100///
101/// # Returns:
102/// - `(HashMap<String, serde_json::Value>, HashMap<String, serde_json::Value>)`: A tuple containing two `HashMap` values, where the first element is the Solo/Duo rank and the second is the Flex rank.
103///
104/// # ⚠️ Notes:
105/// - The function expects the `queueType` field to differentiate between "RANKED_SOLO_5x5" and "RANKED_FLEX_SR".
106/// - If rank information is missing for either Solo/Duo or Flex, the function returns the `default_rank` for that rank type.
107/// - It assumes that the first element in the `rank_info` corresponds to Flex if `queueType` is "RANKED_FLEX_SR", otherwise it assumes the first element is Solo/Duo.
108///
109/// # Example:
110/// ```rust
111/// let rank_info = vec![
112/// hashmap! { "queueType".to_string() => serde_json::Value::String("RANKED_FLEX_SR".to_string()) },
113/// hashmap! { "queueType".to_string() => serde_json::Value::String("RANKED_SOLO_5x5".to_string()) }
114/// ];
115/// let default_rank = hashmap! { "tier".to_string() => serde_json::Value::String("UNRANKED".to_string()) };
116///
117/// let (solo_rank, flex_rank) = determine_solo_flex(&rank_info, &default_rank);
118///
119/// assert_eq!(solo_rank.get("queueType").unwrap(), "RANKED_SOLO_5x5");
120/// assert_eq!(flex_rank.get("queueType").unwrap(), "RANKED_FLEX_SR");
121/// ```
122///
123/// In this example, the function will correctly identify the Solo/Duo and Flex ranks based on the `queueType` values provided in `rank_info`.
124pub fn determine_solo_flex(
125 rank_info: &Vec<HashMap<String, serde_json::Value>>,
126 default_rank: &HashMap<String, serde_json::Value>,
127) -> (
128 HashMap<String, serde_json::Value>,
129 HashMap<String, serde_json::Value>,
130) {
131 if rank_info
132 .get(0)
133 .unwrap_or(&default_rank)
134 .get("queueType")
135 .unwrap()
136 .as_str()
137 == Some("RANKED_FLEX_SR")
138 {
139 let flex_rank = rank_info.get(0).unwrap_or(&default_rank).clone();
140 let solo_rank = rank_info.get(1).unwrap_or(&default_rank).clone();
141 (solo_rank, flex_rank)
142 } else {
143 let solo_rank = rank_info.get(0).unwrap_or(&default_rank).clone();
144 let flex_rank = rank_info.get(1).unwrap_or(&default_rank).clone();
145 (solo_rank, flex_rank)
146 }
147}
148
149/// ⚙️ **Function**: Converts a `Region` enum into its corresponding server string representation.
150///
151/// This function takes a reference to a `Region` enum and returns a string representing the
152/// appropriate server for that region. It maps each region to its official server shorthand,
153/// which is used in API requests to the Riot Games platform.
154///
155/// # Parameters:
156/// - region: A reference to a `Region` enum, representing the different League of Legends regions.
157///
158/// # Returns:
159/// - `String`: A string that corresponds to the server shorthand for the provided region.
160///
161/// # Supported Regions:
162/// - **NA**: Maps to "na1"
163/// - **EUW**: Maps to "euw1"
164/// - **EUNE**: Maps to "eun1"
165/// - **KR**: Maps to "kr"
166/// - **BR**: Maps to "br1"
167/// - **LAN**: Maps to "la1"
168/// - **LAS**: Maps to "la2"
169/// - **OCE**: Maps to "oc1"
170/// - **RU**: Maps to "ru"
171/// - **TR**: Maps to "tr1"
172/// - **JP**: Maps to "jp1"
173///
174/// # Example:
175/// This function can be used when you need to retrieve the corresponding server for a specific region.
176///
177/// ```rust
178/// let server = region_to_string(&Region::NA);
179/// assert_eq!(server, "na1");
180/// ```
181pub fn region_to_string(region: &Region) -> String {
182 (match region {
183 Region::NA => "na1",
184 Region::EUW => "euw1",
185 Region::EUNE => "eun1",
186 Region::KR => "kr",
187 Region::BR => "br1",
188 Region::LAN => "la1",
189 Region::LAS => "la2",
190 Region::OCE => "oc1",
191 Region::RU => "ru",
192 Region::TR => "tr1",
193 Region::JP => "jp1",
194 })
195 .to_string()
196}
197
198/// ⚙️ **Function**: Converts a duration in seconds into a tuple representing minutes and seconds.
199///
200/// This function takes a duration in seconds and converts it into a more human-readable format, returning
201/// the number of minutes and the remaining seconds as a tuple of strings. This is useful for displaying
202/// game durations or other time intervals in a clear way.
203///
204/// # Parameters:
205/// - `seconds`: A `u64` value representing the total duration in seconds.
206///
207/// # Returns:
208/// - `(String, String)`: A tuple where the first value is the number of minutes, and the second value is the number of seconds (formatted as two digits if necessary).
209///
210/// # Example:
211/// This function is useful when converting raw game duration data into a more readable format.
212///
213/// ```rust
214/// let (minutes, seconds) = seconds_to_time(645);
215/// assert_eq!(minutes, "10");
216/// assert_eq!(seconds, "45");
217/// ```
218/// In this example, 645 seconds are converted to 10 minutes and 45 seconds.
219///
220/// # Notes:
221/// - The seconds part is always formatted as two digits. For example, if the input is 610 seconds (10 minutes and 10 seconds), the result will be `"10", "10"`.
222pub fn seconds_to_time(seconds: u64) -> (String, String) {
223 let game_duration_minutes = seconds / 60;
224 let game_duration_seconds = seconds % 60;
225 let game_duration_seconds_str: String;
226 if game_duration_seconds < 10 {
227 game_duration_seconds_str = format!("0{}", game_duration_seconds);
228 } else {
229 game_duration_seconds_str = game_duration_seconds.to_string();
230 }
231 (game_duration_minutes.to_string(), game_duration_seconds_str)
232}
233/// ⚙️ **Function**: Retrieves a custom emoji string based on role and name from a MongoDB collection.
234///
235/// This asynchronous function searches a MongoDB collection for a custom emoji corresponding to a specific role and name.
236/// If found, it formats the emoji in a string compatible with Discord. If not found, it returns the provided name as a fallback.
237///
238/// # Parameters:
239/// - `collection`: A MongoDB `Collection<EmojiId>` containing the emoji mappings, where each document maps a role and name to an emoji ID.
240/// - `role`: A string slice representing the role of the emoji (e.g., "position", "champions").
241/// - `name`: A string slice representing the name of the emoji (e.g., "TOP", "JUNGLE", champion names).
242///
243/// # Returns:
244/// - `Result<String, Error>`: Returns a `Result` containing the formatted emoji string (if found) or the name as a fallback.
245/// In case of errors, it logs the error and returns the name.
246///
247/// # Example:
248/// This function can be used to retrieve custom emojis for roles or champions when creating embeds for Discord:
249///
250/// ```rust
251/// let emoji = get_emoji(collection_emojis, "position", "TOP").await?;
252/// println!("The emoji for TOP is: {}", emoji);
253/// ```
254///
255/// # Notes:
256/// - The function creates a MongoDB filter to search for the emoji based on the role and name fields.
257/// - If an emoji is found, it formats the emoji string in the form `<:name:id>`, which is recognized by Discord.
258/// - If no emoji is found or an error occurs, the function returns the `name` string as a fallback and logs any errors encountered during the search.
259pub async fn get_emoji(
260 collection: Collection<EmojiId>,
261 role: &str,
262 name: &str,
263) -> Result<String, Error> {
264 let filter = doc! { "role": role, "name": name };
265
266 match collection.find_one(filter).await {
267 Ok(Some(emoji_id)) => {
268 let emoji_str = format!("<:{}:{}>", name, emoji_id.id_emoji);
269 Ok(emoji_str)
270 }
271 Ok(None) => Ok(name.to_string()),
272 Err(e) => {
273 log::error!("Erreur lors de la recherche de l'emoji: {:?}", e);
274 Ok(name.to_string())
275 }
276 }
277}
278
279/// ⚙️ **Function**: Retrieves the game mode corresponding to a given queue ID.
280///
281/// This function looks up the game mode based on a provided `queue_id` using a predefined mapping (`QUEUE_ID_MAP`)
282/// of queue IDs to game modes. If the `queue_id` is not found in the map, it returns "Unknown".
283///
284/// # Parameters:
285/// - `queue_id`: An `i64` representing the queue ID for which the game mode is being queried.
286///
287/// # Returns:
288/// - `&'static str`: Returns a string slice representing the game mode name corresponding to the queue ID, or "Unknown" if the queue ID is not found.
289///
290/// # Example:
291/// This function can be used to retrieve the game mode based on the queue ID returned from match data:
292///
293/// ```rust
294/// let queue_id = 420; // Example queue ID for Ranked Solo/Duo
295/// let game_mode = get_game_mode(queue_id);
296/// println!("The game mode is: {}", game_mode);
297/// ```
298///
299/// # Notes:
300/// - The function iterates over the `QUEUE_ID_MAP`, a predefined list of tuples mapping queue IDs to game modes.
301/// - If the queue ID is found in the map, the corresponding game mode is returned immediately.
302/// - If the queue ID is not found, the function defaults to returning "Unknown".
303pub fn get_game_mode(queue_id: i64) -> &'static str {
304 for &(id, mode) in QUEUE_ID_MAP.iter() {
305 if id == queue_id {
306 return mode;
307 }
308 }
309 "Unknown"
310}
311
312pub fn get_champion_names(dd_json: &Value) -> Vec<String> {
313 // Obtenir le champ "data" qui contient les champions
314 let data = &dd_json["data"];
315
316 // Vérifier que "data" est un objet
317 if let Some(champion_map) = data.as_object() {
318 // Itérer sur les valeurs (données des champions)
319 champion_map
320 .values()
321 .filter_map(|champion| champion["name"].as_str().map(|s| s.to_string()))
322 .collect()
323 } else {
324 vec![]
325 }
326}
327
328pub fn get_champion_id(dd_json: &Value, name: &str) -> Option<String> {
329 let data = &dd_json["data"];
330 if let Some(champion_map) = data.as_object() {
331 for (_, champion_value) in champion_map {
332 // Obtenir le nom du champion
333 if let Some(champion_name) = champion_value["name"].as_str() {
334 if champion_name.eq_ignore_ascii_case(name) {
335 if let Some(champion_id) = champion_value["id"].as_str() {
336 return Some(champion_id.to_string());
337 }
338 }
339 }
340 }
341 }
342 None
343}
344
345async fn create_user(
346 user_id: String,
347 username: String,
348 mongo_client: &Client,
349 is_command_suggestion: bool,
350) -> Result<(), mongodb::error::Error> {
351 let collection = mongo_client
352 .database("stat-summoner")
353 .collection::<User>("users");
354 let user = User {
355 user_id,
356 username,
357 last_command_at: Utc::now().timestamp() as u64,
358 count_command: 1,
359 last_suggestion_at: if is_command_suggestion {
360 Utc::now().timestamp() as u64
361 } else {
362 0
363 },
364 is_blacklisted: false,
365 created_at: Utc::now().timestamp() as u64,
366 };
367 collection.insert_one(user).await?;
368 Ok(())
369}
370
371async fn update_user(
372 user_id: String,
373 mongo_client: &Client,
374 is_command_suggestion: bool,
375) -> Result<(), mongodb::error::Error> {
376 let collection = mongo_client
377 .database("stat-summoner")
378 .collection::<User>("users");
379 let filter = doc! { "user_id": user_id };
380 let update = if is_command_suggestion {
381 doc! { "$inc": { "count_command": Bson::Int32(1) }, "$set": { "last_command_at": Bson::Int64(Utc::now().timestamp()), "last_suggestion_at": Bson::Int64(Utc::now().timestamp()) } }
382 } else {
383 doc! { "$inc": { "count_command": Bson::Int32(1) }, "$set": { "last_command_at": Bson::Int64(Utc::now().timestamp()) } }
384 };
385 collection.update_one(filter, update).await?;
386 Ok(())
387}
388
389async fn is_user_in_db(
390 user_id: String,
391 mongo_client: &Client,
392) -> Result<bool, mongodb::error::Error> {
393 let collection = mongo_client
394 .database("stat-summoner")
395 .collection::<User>("users");
396 let filter = doc! { "user_id": user_id };
397 let user = collection.find_one(filter).await?;
398 Ok(user.is_some())
399}
400
401pub async fn manage_user(
402 user_id: String,
403 username: String,
404 mongo_client: &Client,
405 is_command_suggestion: bool,
406) -> Result<(), mongodb::error::Error> {
407 let is_user = is_user_in_db(user_id.clone(), mongo_client).await?;
408 if is_user {
409 update_user(user_id, mongo_client, is_command_suggestion).await?;
410 } else {
411 create_user(user_id, username, mongo_client, is_command_suggestion).await?;
412 }
413 Ok(())
414}