diff --git a/Cargo.lock b/Cargo.lock index 32b67af..1cb6261 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,14 +51,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" [[package]] name = "ark-guild-bot" version = "0.1.0" dependencies = [ + "anyhow", "chrono", "dateparser", "dotenv", @@ -69,7 +70,7 @@ dependencies = [ "once_cell", "parking_lot 0.12.0", "parse-display", - "poise 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "poise", "sea-orm", "serde", "tokio", @@ -627,7 +628,7 @@ version = "0.1.0" dependencies = [ "enum-iterator", "parse-display", - "poise 0.1.0 (git+https://github.com/kangalioo/poise)", + "poise", "sea-orm", "strum", ] @@ -1433,24 +1434,7 @@ dependencies = [ "futures-util", "log", "once_cell", - "poise_macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex", - "serenity", - "tokio", -] - -[[package]] -name = "poise" -version = "0.1.0" -source = "git+https://github.com/kangalioo/poise#34d5863d292bb220d0b24fdd5a12eb79081c268a" -dependencies = [ - "async-trait", - "derivative", - "futures-core", - "futures-util", - "log", - "once_cell", - "poise_macros 0.1.0 (git+https://github.com/kangalioo/poise)", + "poise_macros", "regex", "serenity", "tokio", @@ -1468,17 +1452,6 @@ dependencies = [ "syn", ] -[[package]] -name = "poise_macros" -version = "0.1.0" -source = "git+https://github.com/kangalioo/poise#34d5863d292bb220d0b24fdd5a12eb79081c268a" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "polling" version = "2.2.0" @@ -2451,9 +2424,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +checksum = "0f48b6d60512a392e34dbf7fd456249fd2de3c83669ab642e021903f4015185b" dependencies = [ "bytes", "libc", diff --git a/Cargo.toml b/Cargo.toml index e75e010..200fd1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ members = [ ] [dependencies] -tokio = {version = "1.17.0", features = ["full"]} +tokio = {version = "1.18.0", features = ["full"]} dotenv = "0.15.0" sea-orm = {version = "0.7.1", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"], default-features = false} poise = "0.1.0" @@ -30,4 +30,5 @@ serde = "1.0.136" parking_lot = "0.12.0" dateparser = "0.1.6" hashbrown = "0.12.0" -uuid = { version = "0.8.2", features = ["v4"]} \ No newline at end of file +uuid = { version = "0.8.2", features = ["v4"]} +anyhow = "1.0.57" \ No newline at end of file diff --git a/entity/Cargo.toml b/entity/Cargo.toml index fe8871e..06bd59a 100644 --- a/entity/Cargo.toml +++ b/entity/Cargo.toml @@ -10,7 +10,7 @@ path = "src/lib.rs" [dependencies] sea-orm = {version = "0.7.1", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"], default-features = false} -poise = {git = "https://github.com/kangalioo/poise"} # For slash command choice parameter derive macro +poise = "0.1.0" # For slash command choice parameter derive macro parse-display = "0.5.5" enum-iterator = "0.7.0" strum = {version = "0.24.0", features = ["derive"]} \ No newline at end of file diff --git a/src/commands/lobby/close_lobby.rs b/src/commands/lobby/close_lobby.rs new file mode 100644 index 0000000..65d5448 --- /dev/null +++ b/src/commands/lobby/close_lobby.rs @@ -0,0 +1,62 @@ +use sea_orm::DbErr; +use uuid::Uuid; + +use crate::{ + check::is_guild_init, commands::lobby::helper::LobbyEvent, database::get_lobby, Context, Error, +}; + +#[poise::command(slash_command, guild_only, check = "is_guild_init")] +pub async fn close_lobby( + ctx: Context<'_>, + #[description = "ID of the lobby"] lobby_id: String, +) -> Result<(), Error> { + let guild_id = ctx.guild_id().unwrap().0; + + let lobby_id = Uuid::parse_str(&lobby_id); + if let Err(_) = lobby_id { + ctx.say("Invalid lobby ID is given").await?; + return Ok(()); + } + + let lobby = get_lobby(lobby_id.unwrap(), ctx.data().db).await; + + match lobby { + Ok(ref lobby) => { + if lobby.guild_id.parse::().unwrap() != guild_id { + ctx.say("Lobby doesn't belong to this guild.").await?; + return Ok(()); + } else if !lobby.active { + ctx.say("Lobby is not active").await?; + return Ok(()); + } + } + Err(DbErr::RecordNotFound(_)) => { + ctx.say("There is no lobby with the given ID").await?; + return Ok(()); + } + Err(_) => { + ctx.say("A database error has occured. Try again.").await?; + return Ok(()); + } + } + + let lobby = lobby.unwrap(); + + let active_lobbies = ctx.data().active_lobbies.write(); + + let channel = match active_lobbies.get(&lobby.lobby_id.to_hyphenated().to_string()) { + Some(channel) => channel, + None => { + drop(active_lobbies); + ctx.say("Lobby is not tracked.").await?; + return Ok(()); + } + }; + + if let Err(_) = channel.send(LobbyEvent::CloseLobby(ctx.data().active_lobbies.clone())) { + ctx.say("An error occured while setting the time.").await?; + return Ok(()); + } + + Ok(()) +} diff --git a/src/commands/lobby/command.rs b/src/commands/lobby/command.rs index 6ad208a..62a4c92 100644 --- a/src/commands/lobby/command.rs +++ b/src/commands/lobby/command.rs @@ -1,4 +1,5 @@ -use crate::{commands::lobby::context::LobbyContext, info::*, check::is_guild_init}; +use crate::{check::is_guild_init, commands::lobby::context::LobbyContext, info::*}; +use chrono::Utc; use helper::*; use parking_lot::RwLock; use poise::{ @@ -143,10 +144,11 @@ pub async fn create_lobby( state: State::ContentSelection, content: None, content_info: None, - lobby_time, + lobby_time: (lobby_time, None), players: vec![], active_players: vec![], player_list: vec![], + http_client: ctx.discord().http.clone(), })); while let Some(mci) = CollectComponentInteraction::new(ctx.discord()) @@ -376,23 +378,55 @@ pub async fn create_lobby( // I spent too much time thinking about this and i am not proud of it. insert_lobby(&lobby_context_locked.read(), db).await?; - let (sender, mut reciever) = unbounded_channel::(); - ctx.data() - .active_lobbies - .write() - .insert(lobby_context_locked.read().id_as_string.clone(), sender); + let (sender, mut reciever) = unbounded_channel::(); + ctx.data().active_lobbies.write().insert( + lobby_context_locked.read().id_as_string.clone(), + sender.clone(), + ); println!( "Inserted lobby id: {}", lobby_context_locked.read().id_as_string ); + // It is the first time creating a timer task so second field is always None + if let Some(time) = lobby_context_locked.read().lobby_time.0 { + lobby_context_locked.write().lobby_time.1 = Some(tokio::spawn({ + let channel = sender; + let lobby_context_locked = lobby_context_locked.clone(); + let db = db; + let active_lobbies = ctx.data().active_lobbies.clone(); + async move{ + let time_left = time - Utc::now(); + // We check the time range so unwrapping is okay + let time_left = std::time::Duration::from_millis( + time_left.num_milliseconds().try_into().unwrap(), + ); + + // Message the users when 10 mins left + tokio::time::sleep(time_left - std::time::Duration::from_secs(600)).await; + + let _ = channel.send(LobbyEvent::LobbyIsDue); + + tokio::time::sleep(std::time::Duration::from_secs(600)).await; + + // Make lobby inactive + let lobby = get_lobby(lobby_context_locked.read().id, db).await; + if let Ok(ref lobby) = lobby { + let _ = disable_lobby(lobby, db).await; + } + + active_lobbies.write().remove(&lobby_context_locked.read().id_as_string); + } + })) + } + // End the command context here and spawn a background task tokio::spawn({ let db = ctx.data().db.clone(); async move { - while let Some(event_c) = reciever.recv().await { - match process_lobby_event(event_c, lobby_context_locked.clone(), &db).await { + while let Some(event) = reciever.recv().await { + match process_lobby_event(event, lobby_context_locked.clone(), &db).await { Ok(_) => {} Err(err) => { println!("Error processing event: {err}") diff --git a/src/commands/lobby/context.rs b/src/commands/lobby/context.rs index eff52e9..8d9af7c 100644 --- a/src/commands/lobby/context.rs +++ b/src/commands/lobby/context.rs @@ -1,7 +1,10 @@ +use std::sync::Arc; + use chrono::{DateTime, Utc}; -use poise::serenity_prelude::{self as serenity, CreateSelectMenu}; +use poise::serenity_prelude::{self as serenity, CreateSelectMenu, Http}; use poise::serenity_prelude::{CreateActionRow, CreateEmbed}; use sea_orm::DatabaseConnection; +use tokio::task::JoinHandle; use super::command::State; use super::helper::*; @@ -26,10 +29,16 @@ pub struct LobbyContext { pub state: State, pub content: Option, pub content_info: Option<&'static ContentInfo>, - pub lobby_time: Option>, + pub lobby_time: ( Option> , Option>), pub players: Vec, pub active_players: Vec, pub player_list: Vec, + // This field is added when I was writing lobby time change command. + // I don't know why I did not thought about doing this earlier + // (probably because I thought it was not necessary) + // but since this is added there is no need to send http client through channels + // and such. I will fix those things later. + pub http_client: Arc, } impl LobbyContext { @@ -74,7 +83,7 @@ impl LobbyContext { self.content_info().ilvl_req ), format!("Scheduled time: {}", { - match self.lobby_time { + match self.lobby_time.0 { Some(time) => format!(" ()", time.timestamp()), None => "Not Set".to_owned() + "", } @@ -234,4 +243,10 @@ impl LobbyContext { } false } + + pub fn drop_timebomb(&mut self) { + if let Some(task) = &self.lobby_time.1 { + task.abort() + } + } } diff --git a/src/commands/lobby/helper.rs b/src/commands/lobby/helper.rs index 4183bdd..3f0554d 100644 --- a/src/commands/lobby/helper.rs +++ b/src/commands/lobby/helper.rs @@ -1,12 +1,22 @@ -use std::sync::Arc; - +use anyhow::anyhow; +use anyhow::Result; +use chrono::DateTime; +use chrono::Utc; use entity::sea_orm_active_enums::Content; +use once_cell::sync::Lazy; use parking_lot::RwLock; use parse_display::Display; -use poise::serenity_prelude as serenity; use poise::serenity_prelude::CreateSelectMenuOption; +use poise::serenity_prelude::GuildId; +use poise::serenity_prelude::User; +use poise::serenity_prelude::UserId; +use poise::serenity_prelude::{self as serenity, MessageComponentInteraction}; use sea_orm::{DatabaseConnection, DbErr}; +use std::sync::Arc; +use crate::LobbyMap; +use crate::database::disable_lobby; +use crate::database::get_lobby; use crate::{ database::{ get_all_character_by_ilvl, get_single_character, insert_lobby_player, remove_lobby_player, @@ -63,41 +73,99 @@ impl LobbyContent { } } } +/// List of lobby events tracked by event listener +// This list acts like a filter. Any event that is not in this +// list gets filtered. +pub static LOBBY_EVENTS: Lazy> = + Lazy::new(|| vec!["lobby-join", "player-join", "lobby-leave"]); + + +// This event is constructed and sent to the lobby manager task. pub enum LobbyEvent { - LobbyJoin, - PlayerJoin, - LobbyLeave, + LobbyJoin(EventComponent), + PlayerJoin(EventComponent), + LobbyLeave(EventComponent), + ChangeTime( + DateTime, + LobbyMap, + ), + LobbyIsDue, + CloseLobby(LobbyMap), +} + +impl LobbyEvent { + pub fn new() -> EventBuilder { + EventBuilder::default() + } } -#[derive(Debug, Display)] -#[display("...")] -pub struct EventParseError {} +/// A builder type for LobbyEvent +// I know this is unnecessary but I felt like this is cool. +#[derive(Default)] +pub struct EventBuilder { + interaction: Option, + http_client: Option>, +} -impl std::error::Error for EventParseError {} +impl EventBuilder { + pub fn component_interaction(mut self, interaction: MessageComponentInteraction) -> Self { + self.interaction = Some(interaction); + self + } -impl TryFrom<&str> for LobbyEvent { - type Error = EventParseError; + pub fn http_client(mut self, client: Arc) -> Self { + self.http_client = Some(client); + self + } + + fn check_for_event_component(&self, event: &str) -> Result<()> { + if let None = self.interaction { + return Err(anyhow!("Interaction must be set for event {event}")); + } + if let None = self.http_client { + return Err(anyhow!("Http client must be set for event {event}")); + } + Ok(()) + } - fn try_from(value: &str) -> Result { - match value { - "lobby-join" => Ok(Self::LobbyJoin), - "player-join" => Ok(Self::PlayerJoin), - "lobby-leave" => Ok(Self::LobbyLeave), - _ => Err(EventParseError {}), + pub fn build(self, event: &str) -> Result { + match event { + "lobby-join" => { + self.check_for_event_component(event)?; + Ok(LobbyEvent::LobbyJoin(EventComponent { + message_component_interaction: self.interaction.unwrap(), + http_client: self.http_client.unwrap(), + })) + } + "player-join" => { + self.check_for_event_component(event)?; + Ok(LobbyEvent::PlayerJoin(EventComponent { + message_component_interaction: self.interaction.unwrap(), + http_client: self.http_client.unwrap(), + })) + } + "lobby-leave" => { + self.check_for_event_component(event)?; + Ok(LobbyEvent::LobbyLeave(EventComponent { + message_component_interaction: self.interaction.unwrap(), + http_client: self.http_client.unwrap(), + })) + } + _ => Err(anyhow!("Got event: {event}. Which is not tracked")), } } } pub async fn process_lobby_event( - event_c: EventComponent, + event: LobbyEvent, lobby_context_locked: Arc>, db: &DatabaseConnection, ) -> Result<(), Error> { - match event_c.event { - LobbyEvent::LobbyJoin => { + match event { + LobbyEvent::LobbyJoin(component) => { let lobby_context = lobby_context_locked.read(); - let mci = event_c.message_component_interaction; - let http_client = event_c.http_client; + let mci = component.message_component_interaction; + let http_client = component.http_client; // Check if lobby is full if lobby_context.content_info().content_size == lobby_context.active_players.len() { @@ -199,9 +267,9 @@ pub async fn process_lobby_event( }; Ok(()) } - LobbyEvent::PlayerJoin => { - let mci = event_c.message_component_interaction; - let http_client = event_c.http_client; + LobbyEvent::PlayerJoin(component) => { + let mci = component.message_component_interaction; + let http_client = component.http_client; // let (channel, message_id) = { let lobby_context = lobby_context_locked.read(); @@ -255,9 +323,9 @@ pub async fn process_lobby_event( Ok(()) } - LobbyEvent::LobbyLeave => { - let mci = event_c.message_component_interaction; - let http_client = event_c.http_client; + LobbyEvent::LobbyLeave(component) => { + let mci = component.message_component_interaction; + let http_client = component.http_client; let (channel, message_id) = { let lobby_context = lobby_context_locked.read(); @@ -327,5 +395,135 @@ pub async fn process_lobby_event( Ok(()) } + LobbyEvent::ChangeTime(time, active_lobbies) => { + { + let mut lobby_context = lobby_context_locked.write(); + lobby_context.lobby_time.0 = Some(time); + if let Some(handle) = lobby_context.lobby_time.1.as_ref() { + handle.abort(); + } + + lobby_context.lobby_time.1 = Some(tokio::spawn({ + let lobby_context_locked = lobby_context_locked.clone(); + let db = db.clone(); + let active_lobbies = active_lobbies.clone(); + async move { + let time_left = time - Utc::now(); + // We check the time range so unwrapping is okay + let time_left = std::time::Duration::from_millis( + time_left.num_milliseconds().try_into().unwrap(), + ); + + // Message the users when 10 mins left + tokio::time::sleep(time_left - std::time::Duration::from_secs(600)).await; + + let _ = active_lobbies + .read() + .get(&lobby_context_locked.read().id_as_string) + .unwrap() + .send(LobbyEvent::LobbyIsDue); + + tokio::time::sleep(std::time::Duration::from_secs(600)).await; + + // Make lobby inactive + let lobby = get_lobby(lobby_context_locked.read().id, &db).await; + if let Ok(ref lobby) = lobby { + let _ = disable_lobby(lobby, &db).await; + } + + active_lobbies + .write() + .remove(&lobby_context_locked.read().id_as_string); + } + })) + } + + let http_client = lobby_context_locked.read().http_client.clone(); + let (channel, message_id, lobby_embed, lobby_buttons) = { + let lobby_context = lobby_context_locked.read(); + ( + serenity::ChannelId(lobby_context.channel_id), + lobby_context.message_id, + lobby_context.create_embed(), + lobby_context.create_user_buttons(), + ) + }; + + // Edit the original message + channel + .edit_message(&http_client, message_id, |m| { + m.embed(|e| { + *e = lobby_embed; + e + }) + .components(|c| c.set_action_row(lobby_buttons)) + }) + .await + .expect("Couldn't edit the message"); + + let users = get_users_from_ids(&lobby_context_locked.read()).await; + + let lobby_context = lobby_context_locked.read(); + let guild = GuildId(lobby_context.guild_id) + .to_partial_guild(lobby_context.http_client.clone()) + .await?; + + for user in users { + user.dm(lobby_context.http_client.clone(), |message| { + message.embed(|e| { + e.title("Your lobby is rescheduled.") + .description(format!( + "Your {} lobby in server {} has been rescheduled to (). Don't forget about it!", + lobby_context.content_info().name, + guild.name, + time.timestamp() + )) + }) + }) + .await?; + } + Ok(()) + } + LobbyEvent::LobbyIsDue => { + let lobby_context = lobby_context_locked.read(); + let guild = GuildId(lobby_context.guild_id) + .to_partial_guild(lobby_context.http_client.clone()) + .await?; + + // Build users from active player ids + let users: Vec = get_users_from_ids(&lobby_context).await; + + for user in users { + user.dm(lobby_context.http_client.clone(), |message| { + message.embed(|e| { + e.title("Your lobby starts in 10 minutes.") + .description(format!( + "Your {} lobby in server {} starts soon. Have fun!", + lobby_context.content_info().name, + guild.name + )) + }) + }) + .await?; + } + Ok(()) + } + LobbyEvent::CloseLobby(active_lobbies) => { + let mut lobby_context = lobby_context_locked.write(); + lobby_context.drop_timebomb(); + active_lobbies.write().remove(&lobby_context.id_as_string); + Ok(()) + }, + } +} + +async fn get_users_from_ids(lobby_context: &LobbyContext) -> Vec { + let mut users = vec![]; + for model in &lobby_context.active_players { + let user = UserId(model.id.parse::().unwrap()) + .to_user(lobby_context.http_client.clone()) + .await; + users.push(user); } + users.into_iter().flatten().collect() } diff --git a/src/commands/lobby/mod.rs b/src/commands/lobby/mod.rs index c3b0159..a3fde7f 100644 --- a/src/commands/lobby/mod.rs +++ b/src/commands/lobby/mod.rs @@ -1,6 +1,7 @@ pub mod command; - +pub mod time; pub mod context; pub mod helper; +pub mod close_lobby; use super::*; diff --git a/src/commands/lobby/time.rs b/src/commands/lobby/time.rs new file mode 100644 index 0000000..df2aaa3 --- /dev/null +++ b/src/commands/lobby/time.rs @@ -0,0 +1,114 @@ +use sea_orm::DbErr; +use uuid::Uuid; + +use crate::{ + check::is_guild_init, + commands::lobby::helper::LobbyEvent, + database::{get_lobby, get_server}, + Context, Error, +}; + +#[poise::command(slash_command, guild_only, check = "is_guild_init")] +pub async fn change_lobby_time( + ctx: Context<'_>, + #[description = "ID of the lobby"] lobby_id: String, + #[description = "Time you want to set for the lobby."] time: String, +) -> Result<(), Error> { + let guild_id = ctx.guild_id().unwrap().0; + + let lobby_id = Uuid::parse_str(&lobby_id); + if let Err(_) = lobby_id { + ctx.say("Invalid lobby ID is given").await?; + return Ok(()); + } + + let offset = chrono::offset::FixedOffset::east( + get_server(guild_id, ctx.data().db).await?.timezone as i32 * 3600, + ); + let time = dateparser::parse_with_timezone(&time, &offset).ok(); + + match time { + None => { + ctx.send(|m| { + m.embed(|e| { + e.title("Invalid time") + .description("Couldn't set lobby time. Either you did not specify a lobby time or the time format is false") + .field("Example usage", "`/create_lobby `\n`/create_lobby 6:00pm`\n`/create_lobby May 02, 2021 15:51 UTC+2`", false) + .field("\0", "If no time zone is specified the guild time zone is used.", false) + .field("\0", format!("Your guild time zone is UTC {offset}"), false) + }) + }).await?; + return Ok(()); + } + Some(lobby_time) => { + if lobby_time <= (chrono::Utc::now() + chrono::Duration::minutes(15)) { + ctx.send(|m| { + m.embed(|e| { + e.title("Lobby time cannot be within 15 minutes") + .description(format!("Couldn't set lobby time. Got time: {}",lobby_time)) + .field("Example usage", "`/create_lobby `\n`/create_lobby 6:00pm`\n`/create_lobby May 02, 2021 15:51 UTC+2`", false) + .field("\0", "If no time zone is specified the guild time zone is used.", false) + .field("\0", format!("Your guild time zone is UTC{offset}"), false) + }) + }).await?; + return Ok(()); + } else if lobby_time >= (chrono::Utc::now() + chrono::Duration::weeks(2)) { + ctx.send(|m| { + m.embed(|e| { + e.title("Lobby time must be within 2 weeks.") + .description(format!("Couldn't set lobby time. Got time: {}",lobby_time)) + .field("Example usage", "`/create_lobby `\n`/create_lobby 6:00pm`\n`/create_lobby May 02, 2021 15:51 UTC+2`", false) + .field("\0", "If no time zone is specified the guild time zone is used.", false) + .field("\0", format!("Your guild time zone is UTC{offset}"), false) + }) + }).await?; + return Ok(()); + } + } + } + + let lobby = get_lobby(lobby_id.unwrap(), ctx.data().db).await; + + match lobby { + Ok(ref lobby) => { + if lobby.guild_id.parse::().unwrap() != guild_id { + ctx.say("Lobby doesn't belong to this guild.").await?; + return Ok(()); + } else if !lobby.active { + ctx.say("Lobby is not active").await?; + return Ok(()); + } + } + Err(DbErr::RecordNotFound(_)) => { + ctx.say("There is no lobby with the given ID").await?; + return Ok(()); + } + Err(_) => { + ctx.say("A database error has occured. Try again.").await?; + return Ok(()); + } + } + + let lobby = lobby.unwrap(); + let time = time.unwrap(); + let event = LobbyEvent::ChangeTime(time, ctx.data().active_lobbies.clone()); + let active_lobbies = ctx.data().active_lobbies.read(); + + let channel = match active_lobbies.get(&lobby.lobby_id.to_hyphenated().to_string()) { + Some(channel) => channel, + None => { + drop(active_lobbies); + ctx.say("Lobby is not tracked.").await?; + return Ok(()); + } + }; + + if let Err(_) = channel.send(event) { + ctx.say("An error occured while setting the time.").await?; + return Ok(()); + } + + ctx.say(format!("Changed the lobby time to: ()", time.timestamp())).await?; + + Ok(()) +} diff --git a/src/commands/register.rs b/src/commands/register.rs index 3cbe4a7..e1b2c75 100644 --- a/src/commands/register.rs +++ b/src/commands/register.rs @@ -5,7 +5,6 @@ use super::*; #[poise::command( prefix_command, slash_command, - hide_in_help, required_permissions = "ADMINISTRATOR" )] pub async fn register_guild( diff --git a/src/database.rs b/src/database.rs index 035cff1..dbbe7cb 100644 --- a/src/database.rs +++ b/src/database.rs @@ -305,7 +305,7 @@ pub async fn insert_lobby( lobby_master: Set(lobby_context.lobby_master.to_string()), content: Set(Content::from_str(content_name_retained.as_str()).unwrap()), created: Set(chrono::Utc::now()), - scheduled: Set(lobby_context.lobby_time), + scheduled: Set(lobby_context.lobby_time.0), active: Set(true), }; diff --git a/src/lib.rs b/src/lib.rs index 88a0fac..354c4e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,9 @@ +pub mod check; pub mod commands; pub mod database; pub mod info; pub mod listener; -pub mod check; -use commands::lobby::helper::{EventParseError, LobbyEvent}; +use commands::lobby::helper::LobbyEvent; pub use entity::sea_orm_active_enums::*; use hashbrown::HashMap; use parking_lot::RwLock; @@ -16,32 +16,25 @@ use tokio::sync::mpsc::UnboundedSender; pub struct Data { pub db: &'static DatabaseConnection, // Hashmap to store lobby ids with their task's channel handle - pub active_lobbies: RwLock>>, + pub active_lobbies: LobbyMap, } +pub type LobbyMap = Arc>>>; pub type Error = Box; pub type Context<'a> = poise::Context<'a, Data, Error>; pub struct EventComponent { message_component_interaction: MessageComponentInteraction, http_client: Arc, - event: LobbyEvent, } impl EventComponent { pub fn new( message_component_interaction: MessageComponentInteraction, http_client: Arc, - event: &str, - ) -> Result { - let event = event.try_into(); - - if let Err(err) = event { - return Err(err); - } - Ok(Self { + ) -> Self { + Self { message_component_interaction, http_client, - event: event.unwrap(), - }) + } } } diff --git a/src/listener.rs b/src/listener.rs index 939f1db..8d1efa4 100644 --- a/src/listener.rs +++ b/src/listener.rs @@ -1,6 +1,5 @@ use crate::{ - commands::{Data, Error}, - EventComponent, + commands::{Data, Error, lobby::helper::{LOBBY_EVENTS, LobbyEvent}}, }; use poise::serenity_prelude as serenity; @@ -18,12 +17,13 @@ pub async fn listener( // UUIDv4 length is 36 characters let (lobby_id_str, event_str) = mci.data.custom_id.split_at(36); - let event_c = EventComponent::new(mci.clone(), ctx.http.clone(), event_str); - - if event_c.is_err() { + if !LOBBY_EVENTS.contains(&event_str) { println!("Event is not tracked"); return Ok(()); } + + let event = LobbyEvent::new().component_interaction(mci.clone()).http_client(ctx.http.clone()).build(event_str)?; + println!("Lobby id: ({lobby_id_str})"); let res = user_data @@ -31,7 +31,7 @@ pub async fn listener( .read() .get(lobby_id_str) .expect("No active lobby found with given id") - .send(event_c.unwrap()); + .send(event); if let Err(err) = res { println!("Error sending event component to task {err}"); diff --git a/src/main.rs b/src/main.rs index 63bcf76..d991d50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,41 @@ use ark_guild_bot::{ commands::{ characters::*, - lobby::{command::*, context::LobbyContext, helper::process_lobby_event}, + lobby::{ + command::*, + context::LobbyContext, + helper::{process_lobby_event, LobbyEvent}, + time::change_lobby_time, + }, register::*, Data, }, database::{disable_lobby, get_active_characters_joined, get_active_lobbies}, info::ContentInfo, listener::listener, - Error, EventComponent, + Error, }; use chrono::Utc; use dotenv::dotenv; use hashbrown::HashMap; use once_cell::sync::OnceCell; use parking_lot::RwLock; -use poise::serenity_prelude::{self as serenity, GatewayIntents}; +use poise::serenity_prelude::{self as serenity, GatewayIntents, Http}; use sea_orm::{Database, DatabaseConnection, DbErr}; use std::sync::Arc; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; pub static DB: OnceCell = OnceCell::new(); +/// At this point, whole code became a mess. It is very hard to understand +/// the monstrosities that lie behind the unexplainably long functions. +/// But since it works and this project only has a "limited" scope, +/// I don't want to rewrite the whole thing. Instead, I embraced the demonic +/// design. It is now my perfect training ground where I get to suffer every time +/// I sit behind my keyboard and figure out the compiler errors that I explore for +/// the first time. This code is filled with "100 design decisions you should not make". +/// +/// I am not proud of this. #[tokio::main] async fn main() -> Result<(), Error> { dotenv().ok(); @@ -31,12 +45,12 @@ async fn main() -> Result<(), Error> { .unwrap(); poise::Framework::build() .token(std::env::var("DISCORD_TOKEN").expect("DISCORD_TOKEN must be set")) - .user_data_setup(move |_ctx, _ready, _framework| { + .user_data_setup(move |ctx, _ready, _framework| { Box::pin(async move { Ok(Data { db: DB.get().unwrap(), - // active_lobbies: todo!("Init this with database query"), - active_lobbies: init_active_lobbies(DB.get().unwrap()).await?, + active_lobbies: init_active_lobbies(DB.get().unwrap(), ctx.http.clone()) + .await?, }) }) }) @@ -49,6 +63,7 @@ async fn main() -> Result<(), Error> { delete_character(), edit_character_ilvl(), create_lobby(), + change_lobby_time(), ], listener: |ctx, event, framework, user_data| { Box::pin(listener(ctx, event, framework, user_data)) @@ -71,7 +86,8 @@ async fn main() -> Result<(), Error> { async fn init_active_lobbies( db: &'static DatabaseConnection, -) -> Result>>, DbErr> { + http_client: Arc, +) -> Result>>>, DbErr> { let mut lobby_map = HashMap::new(); let active_lobbies = get_active_lobbies(db).await?; for lobby in active_lobbies { @@ -100,10 +116,11 @@ async fn init_active_lobbies( state: State::Generated, content: Some(content_info.content_type.as_str().into()), content_info: Some(content_info), - lobby_time: lobby.scheduled, + lobby_time: (lobby.scheduled, None), players: vec![], active_players: vec![], player_list: vec![], + http_client: http_client.clone(), })); { @@ -132,5 +149,5 @@ async fn init_active_lobbies( }); } - Ok(RwLock::from(lobby_map)) + Ok(Arc::new(RwLock::from(lobby_map))) }