stat_summoner/embed.rs
1use crate::models::error::Error;
2use crate::models::modal::LolStatsModal;
3use crate::{
4 models::data::{Data, EmojiId},
5 utils::get_emoji,
6};
7use mongodb::Collection;
8use poise::ReplyHandle;
9use poise::{
10 serenity_prelude::{self as serenity},
11 CreateReply,
12};
13use serde_json::Value;
14use serenity::builder::{CreateEmbed, CreateEmbedFooter};
15use tokio::time::{sleep, Duration};
16
17/// ⚙️ **Function**: Creates a rich embed message displaying League of Legends player stats and match details.
18///
19/// This function constructs a `CreateEmbed` message containing information about the player's Solo/Duo and Flex ranks,
20/// top champions, and detailed information about recent matches. The generated embed is used for displaying formatted
21/// stats in Discord messages.
22///
23/// # Parameters:
24/// - `modal_data`: Contains the player's in-game name and tag, used to personalize the embed title.
25/// - `solo_rank`: A JSON-like value containing the player's Solo/Duo rank information, including tier, division, LP, wins, losses, and winrate.
26/// - `flex_rank`: A JSON-like value containing the player's Flex rank information, similar to `solo_rank`.
27/// - `champions_info`: A formatted string representing the player's top champions, their levels, and mastery points.
28/// - `match_details`: A vector of JSON-like values representing detailed match information, including K/D/A, farm, game duration, and result.
29///
30/// # Returns:
31/// - `CreateEmbed`: The formatted embed message ready to be sent in a Discord channel.
32///
33/// # ⚠️ Notes:
34/// - If no match details are available, the embed will indicate that no recent normal or ranked matches were found.
35/// - The embed displays rank information differently depending on whether the player has earned League Points (LP) in their rank.
36///
37/// # Example:
38/// ```rust
39/// let embed = create_embed(modal_data, solo_rank, flex_rank, champions_info, match_details);
40/// ctx.send(|m| m.set_embed(embed)).await?;
41/// ```
42///
43/// The resulting embed will contain information such as:
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_embed(
56 modal_data: &LolStatsModal,
57 solo_rank: Value,
58 flex_rank: Value,
59 champions_info: String,
60 match_details: Vec<Value>,
61 collection_emoji: Collection<EmojiId>,
62) -> Result<CreateEmbed, Error> {
63 // Récupérer les émojis pour le rang solo et flex
64 let solo_rank_tier = solo_rank["tier"].as_str().unwrap_or("Unknown");
65 let solo_emoji = get_emoji(collection_emoji.clone(), "rank", solo_rank_tier)
66 .await
67 .unwrap_or(solo_rank_tier.to_string());
68
69 let flex_rank_tier = flex_rank["tier"].as_str().unwrap_or("Unknown");
70 let flex_emoji = get_emoji(collection_emoji.clone(), "rank", flex_rank_tier)
71 .await
72 .unwrap_or(flex_rank_tier.to_string());
73
74 // Construction de la chaîne du rang Solo/Duo
75 let solo_rank_str = if solo_rank["lp"].as_i64().unwrap_or(0) > 0 {
76 if !solo_rank["division"].as_str().unwrap_or("").is_empty() {
77 format!(
78 "**{} {}** - {} LP",
79 solo_emoji,
80 solo_rank["division"].as_str().unwrap(),
81 solo_rank["lp"].as_i64().unwrap()
82 )
83 } else {
84 format!(
85 "**{}** - {} LP",
86 solo_emoji,
87 solo_rank["lp"].as_i64().unwrap()
88 )
89 }
90 } else {
91 format!("**{}**", solo_emoji)
92 };
93
94 // Construction de la chaîne du rang Flex
95 let flex_rank_str = if flex_rank["lp"].as_i64().unwrap_or(0) > 0 {
96 if !flex_rank["division"].as_str().unwrap_or("").is_empty() {
97 format!(
98 "**{} {}** ({} LP)",
99 flex_emoji,
100 flex_rank["division"].as_str().unwrap(),
101 flex_rank["lp"].as_i64().unwrap()
102 )
103 } else {
104 format!(
105 "**{}** ({} LP)",
106 flex_emoji,
107 flex_rank["lp"].as_i64().unwrap()
108 )
109 }
110 } else {
111 format!("**{}**", flex_emoji)
112 };
113
114 // Construction de l'embed
115 let embed = CreateEmbed::default()
116 .title(format!("📊 Stats for **{}#{}**", modal_data.game_name, modal_data.tag_line))
117 .color(0x00ff00)
118 .field("**Solo/Duo Rank**", solo_rank_str, false)
119 .field("🏆 **Wins**", format!("**{}**", solo_rank["wins"].as_i64().unwrap_or(-1)), true)
120 .field("❌ **Losses**", format!("**{}**", solo_rank["losses"].as_i64().unwrap_or(-1)), true)
121 .field(
122 "📊 **Winrate**",
123 format!("**{:.2}%**", solo_rank["winrate"].as_f64().unwrap_or(-1.0)),
124 true
125 )
126 .field("**Flex Rank**", flex_rank_str, false)
127 .field("🏆 **Wins**", format!("**{}**", flex_rank["wins"].as_i64().unwrap_or(-1)), true)
128 .field("❌ **Losses**", format!("**{}**", flex_rank["losses"].as_i64().unwrap_or(-1)), true)
129 .field(
130 "📊 **Winrate**",
131 format!("**{:.2}%**", flex_rank["winrate"].as_f64().unwrap_or(-1.0)),
132 true
133 )
134 .field("💥 **Top Champions**", champions_info, false)
135 .field(
136 "📜 **Match Details**",
137 if match_details.is_empty() {
138 "No match found on Normal and ranked game".to_string()
139 } else {
140 match_details
141 .iter()
142 .map(|match_detail| {
143 format!(
144 "{} - **{}**, {} ({}):\nK/D/A: **{}** | **{} CS** | Duration: **{}**\n⏳ Played: **{}**\n\n",
145 match_detail.get("Result").unwrap().as_str().unwrap(),
146 match_detail.get("champion_name").unwrap().as_str().unwrap(),
147 match_detail.get("time_elapsed").unwrap().as_str().unwrap(),
148 match_detail.get("game_type").unwrap().as_str().unwrap(),
149 match_detail.get("K/D/A").unwrap().as_str().unwrap(),
150 match_detail.get("Farm").unwrap().as_u64().unwrap(),
151 match_detail.get("Duration").unwrap().as_str().unwrap(),
152 match_detail.get("time_elapsed").unwrap().as_str().unwrap()
153 )
154 })
155 .collect::<String>()
156 },
157 false
158 )
159 .footer(CreateEmbedFooter::new("This message will be deleted in 60 seconds."))
160 .thumbnail("https://i.postimg.cc/9fKf2tYp/Logo.png");
161
162 Ok(embed)
163}
164
165/// ⚙️ **Function**: Creates an embed displaying an error message for Discord interactions.
166///
167/// This function constructs a Discord embed message that displays a given error message in a formatted way.
168/// The embed is styled with a red color to indicate an error and includes a default title of "Error".
169/// The embed is returned as part of a `CreateReply`, which can be sent to a Discord channel.
170///
171/// # Parameters:
172/// - `error_message`: A string slice containing the error message to be displayed in the embed's description.
173/// This message is intended to provide feedback to the user, typically in case of API errors, invalid inputs,
174/// or other issues encountered during the bot's execution.
175///
176/// # Returns:
177/// - `CreateReply`: A response object that includes the error embed. This is ready to be sent to a Discord channel.
178///
179/// # ⚠️ Notes:
180/// - The embed's color is set to red (`0xff0000`) to visually signify an error.
181/// - The title of the embed is always set to "Error", and the provided `error_message` is used in the description.
182/// - The function is primarily used to provide user-friendly error messages in response to invalid inputs
183/// or issues in API calls.
184///
185/// # Example:
186/// ```rust
187/// let error_reply = create_embed_error("Failed to fetch data from the Riot API.");
188/// ctx.send(error_reply).await?;
189/// ```
190///
191/// The resulting embed message will look like this:
192/// ```text
193/// ❌ **Error**
194/// Failed to fetch data from the Riot API.
195/// ```
196pub fn create_embed_error(error_message: &str) -> CreateReply {
197 let embed: CreateEmbed = CreateEmbed::default()
198 .title("Error")
199 .description(error_message)
200 .color(0xff0000)
201 .footer(CreateEmbedFooter::new(
202 "This message will be deleted in 60 seconds.",
203 ))
204 .thumbnail("https://i.postimg.cc/9fKf2tYp/Logo.png");
205 CreateReply {
206 embeds: vec![embed],
207 ..Default::default()
208 }
209}
210
211/// ⚙️ **Function**: Creates a success embed reply for Discord messages.
212///
213/// This function generates a Discord embed with the title "Success", a description provided by the `success_message` parameter,
214/// a green color to indicate success, and a footer notifying that the message will be deleted in 60 seconds. It returns a
215/// `CreateReply` containing the embed, suitable for sending as a response to a Discord interaction or message.
216///
217/// # Parameters:
218/// - `success_message`: A string slice that holds the success message to be displayed in the embed's description.
219///
220/// # Returns:
221/// - `CreateReply`: A Discord reply containing the constructed success embed.
222///
223/// # ⚠️ Notes:
224/// - The embed's footer is in French: "This message will be deleted in 60 seconds." ("This message will be deleted in 60 seconds.").
225/// - The title "Success" appears to have a typo and might be intended as "Success".
226/// - The embed uses a green color (`0x00ff00`) to visually indicate a successful operation.
227///
228/// # Example:
229/// ```rust
230/// let reply = create_embed_success("Operation completed successfully!");
231/// // Use `reply` to send the embed in a Discord channel
232/// ```
233pub fn create_embed_success(success_message: &str) -> CreateReply {
234 let embed: CreateEmbed = CreateEmbed::default()
235 .title("Success")
236 .description(success_message)
237 .color(0x00ff00)
238 .footer(CreateEmbedFooter::new(
239 "This message will be deleted in 60 seconds.",
240 ))
241 .thumbnail("https://i.postimg.cc/9fKf2tYp/Logo.png");
242 CreateReply {
243 embeds: vec![embed],
244 ..Default::default()
245 }
246}
247
248/// ⚙️ **Function**: Schedules the deletion of a Discord message after a delay.
249///
250/// This function delays the deletion of a Discord message by 60 seconds. After the delay, the function attempts
251/// to delete the message from the channel. It ensures that messages sent by the bot (e.g., error messages or
252/// status updates) are automatically removed after a certain time to keep the chat clean.
253///
254/// # Parameters:
255/// - `sent_message`: A `ReplyHandle` representing the message to be deleted. This handle provides access to the
256/// message object, allowing the function to delete it once the delay has passed.
257/// - `ctx`: The application context, which provides access to Discord methods (such as message deletion)
258/// and other necessary data like API keys.
259///
260/// # Returns:
261/// - `Result<(), Error>`: If successful, returns `Ok(())`. If an error occurs while fetching the message
262/// or deleting it, returns an `Error`.
263///
264/// # ⚠️ Notes:
265/// - The function uses `tokio::time::sleep` to pause execution for 60 seconds before attempting to delete the message.
266/// - If the message cannot be fetched (e.g., due to permissions or being deleted manually), the deletion attempt will silently fail.
267/// - This function is typically used in scenarios where temporary messages (like errors or status updates)
268/// need to be cleaned up automatically after a short period.
269///
270/// # Example:
271/// ```rust
272/// schedule_message_deletion(sent_message, ctx).await?;
273/// ```
274///
275/// After 60 seconds, the message will be deleted from the Discord channel.
276pub async fn schedule_message_deletion(
277 sent_message: ReplyHandle<'_>,
278 ctx: poise::ApplicationContext<'_, Data, Error>,
279) -> Result<(), Error> {
280 sleep(Duration::from_secs(60)).await;
281 if let Ok(sent_msg) = sent_message.message().await {
282 sent_msg.delete(&ctx.serenity_context().http).await?;
283 }
284 Ok(())
285}