stat_summoner/module/whoisfollowed/utils.rs
1use crate::models::data::SummonerFollowedData;
2use crate::models::error::Error;
3use chrono::{Duration, Utc};
4use futures::StreamExt;
5use mongodb::bson::doc;
6use mongodb::Collection;
7use poise::serenity_prelude::{CreateEmbed, CreateEmbedFooter};
8use poise::CreateReply;
9use serde_json::json;
10use serde_json::Value;
11
12/// ⚙️ **Function**: Fetches the list of summoners followed in a specific Discord guild.
13///
14/// This asynchronous function retrieves data about summoners followed within a particular Discord guild.
15/// It queries the provided MongoDB collection for records matching the specified `guild_id` and
16/// returns a list of summoners, along with the remaining follow duration for each.
17/// If the follow has ended, it will return "Follow ended" for that summoner.
18///
19/// # Parameters:
20/// - `collection`: The MongoDB collection containing follow data, where each document represents a summoner being followed.
21/// - `guild_id`: A `String` representing the unique identifier of the Discord guild. This is used to filter the summoners
22/// being followed in that specific guild.
23///
24/// # Returns:
25/// - `Result<Value, Error>`: On success, it returns a `serde_json::Value` object containing a list of tracked summoners,
26/// each with their `name` and `time_remaining` (formatted as a human-readable string or "Follow ended" if the follow has expired).
27/// In case of an error, it returns an `Error` object.
28///
29/// # ⚠️ Notes:
30/// - The function calculates the remaining follow duration by comparing the current timestamp with the `time_end_follow`
31/// value from each summoner's record.
32/// - If a summoner's follow has expired, the time remaining is returned as "Follow ended".
33/// - The duration is formatted as a readable string for convenience.
34///
35/// # Example:
36/// ```rust
37/// let collection: Collection<SummonerFollowedData> = db.collection("follower_summoner");
38/// let guild_id = "1234567890".to_string();
39/// let result = get_data_followed_summoner(collection, guild_id).await?;
40///
41/// // The result would look like:
42/// /// ```json
43/// /// {
44/// /// "tracked_summoners": [
45/// /// {
46/// /// "name": "Summoner1",
47/// /// "time_remaining": "2 hours 15 minutes"
48/// /// },
49/// /// {
50/// /// "name": "Summoner2",
51/// /// "time_remaining": "Follow ended"
52/// /// }
53/// /// ]
54/// /// }
55/// ```
56pub async fn get_data_followed_summoner(
57 collection: Collection<SummonerFollowedData>,
58 guild_id: String,
59) -> Result<Value, Error> {
60 let current_timestamp = Utc::now().timestamp();
61 let mut cursor = collection.find(doc! { "guild_id": guild_id }).await?;
62 let mut summoners = Vec::new();
63 while let Some(followed_data) = cursor.next().await {
64 if let Ok(data) = followed_data {
65 let name = &data.name;
66 let time_end_follow = data.time_end_follow.parse::<i64>().unwrap();
67
68 let remaining_duration = time_end_follow - current_timestamp;
69 let time_remaining_str = if remaining_duration > 0 {
70 let duration = Duration::seconds(remaining_duration);
71 format_duration(duration)
72 } else {
73 "Follow ended".to_string()
74 };
75 let summoner = json!({
76 "name": name,
77 "time_remaining": time_remaining_str
78 });
79 summoners.push(summoner);
80 }
81 }
82 Ok(json!({ "tracked_summoners": summoners }))
83}
84
85/// ⚙️ **Function**: Formats a `Duration` into a human-readable string.
86///
87/// This function takes a `Duration` and returns a string representing the remaining time in a human-readable format.
88/// The function distinguishes between days, hours, and minutes, with specific rules for singular and plural terms.
89/// If the remaining time is less than a minute, it returns "less than a minute".
90///
91/// # Parameters:
92/// - `duration`: A `Duration` object representing the time span to format. The function will extract the number of
93/// days, hours, and minutes from this duration to create a user-friendly time description.
94///
95/// # Returns:
96/// - `String`: A human-readable string indicating how much time is left, formatted as:
97/// - "in 1 day", "in 1 day and X hours", "in X hours", "in X minutes", or "less than a minute".
98/// The string changes based on the length of the duration.
99///
100/// # ⚠️ Notes:
101/// - If the duration is greater than a day, the function formats the result as "in X days and Y hours",
102/// or "in X days" if there are no remaining hours.
103/// - If the duration is less than a day but more than an hour, the result is formatted as "in X hours".
104/// - For durations less than an hour but more than a minute, it returns "in X minutes".
105/// - If the duration is less than a minute, the function returns "less than a minute".
106///
107/// # Example:
108/// ```rust
109/// let duration = Duration::hours(5);
110/// let formatted = format_duration(duration);
111/// assert_eq!(formatted, "in 5 hours");
112///
113/// let short_duration = Duration::minutes(1);
114/// let formatted_short = format_duration(short_duration);
115/// assert_eq!(formatted_short, "in 1 minute");
116/// ```
117///
118/// The function will return the appropriate formatted string based on the duration passed in.
119
120fn format_duration(duration: Duration) -> String {
121 let days = duration.num_days();
122 let hours = duration.num_hours() % 24;
123 let minutes = duration.num_minutes() % 60;
124
125 if days > 0 {
126 if hours > 0 {
127 if hours == 1 {
128 return format!("in 1 day and 1 hour");
129 } else {
130 return format!("in 1 day and {} hours", hours);
131 }
132 } else {
133 return format!("in 1 day");
134 }
135 } else if hours > 0 {
136 if hours == 1 {
137 return format!("in 1 hour");
138 } else {
139 return format!("in {} hours", hours);
140 }
141 } else if minutes > 0 {
142 if minutes == 1 {
143 return format!("in 1 minute");
144 } else {
145 return format!("in {} minutes", minutes);
146 }
147 } else {
148 return "less than a minute".to_string();
149 }
150}
151
152/// ⚙️ **Function**: Creates an embed displaying the list of followed summoners.
153///
154/// This function constructs a Discord embed message that lists all summoners being followed in a guild.
155/// It includes the remaining time for each summoner's follow or a message if no summoners are currently being tracked.
156/// The embed has a default purple color and includes a footer stating that the message will be deleted after 60 seconds.
157///
158/// # Parameters:
159/// - `data`: A `serde_json::Value` object containing the list of tracked summoners.
160/// The `data` is expected to have a `tracked_summoners` field, which is an array of objects with each summoner's name and follow duration.
161///
162/// # Returns:
163/// - `CreateReply`: A Discord reply object containing the constructed embed. This can be sent to a Discord channel.
164/// The embed includes fields with each summoner's name and the remaining follow time, or a message stating that no summoners are currently being followed.
165///
166/// # ⚠️ Notes:
167/// - If no summoners are found in the `tracked_summoners` array, the embed will display "No summoners are currently being followed".
168/// - The embed's color is set to purple (`0xA020F0`), and a footer is included indicating that the message will be deleted after 60 seconds.
169/// - Each summoner's follow information is displayed in the format: `Follow ends in: X time`.
170///
171/// # Example:
172/// ```rust
173/// let data = json!({
174/// "tracked_summoners": [
175/// {
176/// "name": "Summoner1",
177/// "time_remaining": "2 hours 15 minutes"
178/// },
179/// {
180/// "name": "Summoner2",
181/// "time_remaining": "Follow ended"
182/// }
183/// ]
184/// });
185/// let embed_reply = create_embed_followed_summoner(data);
186/// ctx.send(embed_reply).await?;
187/// ```
188///
189/// This example would produce an embed listing two summoners, with their remaining follow durations.
190pub fn create_embed_followed_summoner(data: Value) -> CreateReply {
191 let binding = vec![];
192 let tracked_summoners = data["tracked_summoners"].as_array().unwrap_or(&binding);
193 let mut embed = CreateEmbed::new()
194 .title("Tracked Summoners")
195 .color(0xA020F0)
196 .footer(CreateEmbedFooter::new(
197 "This message will be deleted in 60 seconds.",
198 ))
199 .thumbnail("https://i.postimg.cc/9fKf2tYp/Logo.png");
200
201 if tracked_summoners.is_empty() {
202 embed = embed.field(
203 "",
204 "No summoners are currently being followed".to_string(),
205 false,
206 );
207 return CreateReply {
208 embeds: vec![embed],
209 ..Default::default()
210 };
211 }
212 for summoner in tracked_summoners {
213 let name = summoner["name"].as_str().unwrap_or("Unknown");
214 let time_remaining = summoner["time_remaining"].as_str().unwrap_or("Unknown");
215
216 embed = embed.field(name, format!("Follow ends in: {}", time_remaining), false);
217 }
218
219 CreateReply {
220 embeds: vec![embed],
221 ..Default::default()
222 }
223}