From 4bddb8371cd268296096e63adfcb701c871fa52d Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Tue, 24 Sep 2024 21:20:01 -0600 Subject: [PATCH] Initial event/session management work --- .gitignore | 2 + Cargo.lock | 108 ++++++++++++++++++++++++++----- Cargo.toml | 4 +- src/bot.rs | 146 +++++++++++++++++++++++++++++++++++++++++- src/config.rs | 1 + src/context.rs | 5 +- src/main.rs | 1 + src/model/mod.rs | 3 + src/model/playlist.rs | 14 ++++ src/model/scene.rs | 25 ++++++++ src/model/session.rs | 54 ++++++++++++++++ 11 files changed, 342 insertions(+), 21 deletions(-) create mode 100644 src/model/mod.rs create mode 100644 src/model/playlist.rs create mode 100644 src/model/scene.rs create mode 100644 src/model/session.rs diff --git a/.gitignore b/.gitignore index d81f12e..2762036 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target /.idea +config.toml +session_config.toml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index b07f3be..22d5fdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,6 +342,27 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "chrono-tz" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + [[package]] name = "clap" version = "2.34.0" @@ -814,7 +835,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -1021,9 +1042,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -1274,6 +1295,15 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "pathdiff" version = "0.2.1" @@ -1338,7 +1368,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap 2.5.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", ] [[package]] @@ -1701,9 +1769,10 @@ dependencies = [ [[package]] name = "rpb" -version = "0.1.0" +version = "0.2.0" dependencies = [ "chrono", + "chrono-tz", "config", "env_logger", "log", @@ -1713,6 +1782,7 @@ dependencies = [ "serde", "structopt", "tokio", + "toml", "tonic", ] @@ -1905,9 +1975,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -1979,6 +2049,12 @@ dependencies = [ "digest", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "skeptic" version = "0.13.7" @@ -2319,9 +2395,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -2331,20 +2407,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", @@ -2935,9 +3011,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 3e924bf..1ddfc9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rpb" description = "Role Playing Bot!" -version = "0.1.0" +version = "0.2.0" edition = "2021" [dependencies] @@ -15,6 +15,8 @@ raas_types = {version = "0.0.9", registry = "jojo-dev"} tonic = "0.11.0" roll-rs = "0.3.0" chrono = "0.4.38" +chrono-tz = "0.10.0" +toml = "0.8.19" [dependencies.tokio] version = "1.38.0" diff --git a/src/bot.rs b/src/bot.rs index 29aa282..8e849c3 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,8 +1,13 @@ use crate::config::BotConfig; use crate::context::Context; +use crate::model::session::{Session, SessionConfig}; use chrono::Utc; use log::{debug, info}; -use poise::serenity_prelude::{CreateAttachment, MessageBuilder}; +use poise::futures_util::{Stream, StreamExt}; +use poise::serenity_prelude::{ + futures, AutocompleteChoice, CreateAttachment, EditScheduledEvent, MessageBuilder, + ScheduledEventId, ScheduledEventStatus, +}; use poise::CreateReply; use raas_types::raas::bot::roll::{roll_response, Roll, RollCmd}; use raas_types::raas::resp::response::Resp; @@ -91,6 +96,128 @@ fn real_roll_help() -> String { "Roll a real D20!".to_string() } +#[poise::command(slash_command)] +async fn create_session( + ctx: BotContext<'_>, + #[description = "Create session from event"] event_name: String, +) -> Result<(), Error> { + let guild = ctx.guild_id().unwrap(); + + let events = guild.scheduled_events(&ctx.http(), false).await?; + + let event = events + .iter() + .find(|e| e.name.eq_ignore_ascii_case(&event_name)) + .unwrap(); + + let path = ctx.data().config.session_config_path.clone(); + let mut session_config = ctx.data().session_config.lock().await; + + session_config.create_session(event.id).await; + + session_config.save_config(path).await.unwrap(); + + ctx.reply(format!("Created session from event '{}'", event.name)) + .await + .unwrap(); + + Ok(()) +} + +#[poise::command(slash_command)] +async fn start_session( + ctx: BotContext<'_>, + #[description = "Session Name"] + #[autocomplete = "autocomplete_session"] + session: ScheduledEventId, +) -> Result<(), Error> { + let sessions = &mut ctx.data().session_config.lock().await; + + let session = sessions + .sessions + .iter() + .find(|s| s.event == session) + .unwrap(); + + let event = ctx + .guild_id() + .unwrap() + .edit_scheduled_event( + ctx.http(), + session.event, + EditScheduledEvent::new().status(ScheduledEventStatus::Active), + ) + .await?; + + ctx.reply(format!("Started session '{}'", event.name)) + .await?; + + sessions.active_session = Some(event.id); + + sessions.save_config(ctx.data().config.session_config_path.clone()).await?; + + Ok(()) +} + +#[poise::command(slash_command)] +async fn stop_session(ctx: BotContext<'_>) -> Result<(), Error> { + let sessions = &mut ctx.data().session_config.lock().await; + + if let Some(session) = sessions.active_session { + match ctx + .guild_id() + .unwrap() + .edit_scheduled_event( + &ctx.http(), + session, + EditScheduledEvent::new().status(ScheduledEventStatus::Completed), + ) + .await + { + Ok(e) => { + ctx.reply(format!("Ending session '{}'", e.name)).await?; + } + Err(err) => { + ctx.reply(format!("Failed to end session: '{}'", err)) + .await?; + } + } + } else { + ctx.reply("No sessions in progress!").await?; + } + + sessions.active_session = None; + + Ok(()) +} + +async fn autocomplete_session<'a>( + ctx: BotContext<'a>, + partial: &'a str, +) -> impl Stream + 'a { + let sessions: Vec = { ctx.data().session_config.lock().await.sessions.clone() }; + + futures::stream::iter(sessions).filter_map(move |s| async move { + let event = ctx + .guild_id() + .unwrap() + .scheduled_event(ctx.http(), s.event, false) + .await; + + if let Ok(event) = event { + if event.status == ScheduledEventStatus::Scheduled + && event.name.matches(partial).count() > 0 + { + Some(AutocompleteChoice::new(event.name, event.id.to_string())) + } else { + None + } + } else { + None + } + }) +} + pub async fn start_bot(config: BotConfig) { info!("Starting bot!"); @@ -100,13 +227,26 @@ pub async fn start_bot(config: BotConfig) { let framework = poise::Framework::builder() .options(poise::FrameworkOptions { - commands: vec![roll(), real_roll()], + commands: vec![ + roll(), + real_roll(), + create_session(), + start_session(), + stop_session(), + ], ..Default::default() }) .setup(|ctx, _ready, framework| { Box::pin(async move { poise::builtins::register_globally(ctx, &framework.options().commands).await?; - Ok(Context { config }) + Ok(Context { + session_config: tokio::sync::Mutex::new( + SessionConfig::init(config.session_config_path.clone()) + .await + .unwrap(), + ), + config, + }) }) }) .build(); diff --git a/src/config.rs b/src/config.rs index 8de1af3..d68a5f2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,7 @@ pub struct Args { pub struct BotConfig { pub bot_token: String, pub raas_url: String, + pub session_config_path: PathBuf, } impl BotConfig { diff --git a/src/context.rs b/src/context.rs index ee9ceec..2dc1e33 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,6 +1,9 @@ use crate::config::BotConfig; +use crate::model::session::SessionConfig; +use tokio::sync::Mutex; -#[derive(Clone)] +#[derive(Debug)] pub struct Context { pub config: BotConfig, + pub session_config: Mutex, } diff --git a/src/main.rs b/src/main.rs index c3b7be3..a0910f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use structopt::StructOpt; mod bot; mod config; mod context; +mod model; #[tokio::main] async fn main() { diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..0e32805 --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,3 @@ +pub mod playlist; +pub mod scene; +pub mod session; diff --git a/src/model/playlist.rs b/src/model/playlist.rs new file mode 100644 index 0000000..09e5702 --- /dev/null +++ b/src/model/playlist.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Mode { + InOrder, + Shuffle, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Playlist { + pub name: String, + pub playlist: String, + pub mode: Mode, +} diff --git a/src/model/scene.rs b/src/model/scene.rs new file mode 100644 index 0000000..968cda0 --- /dev/null +++ b/src/model/scene.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "adv_type")] +pub enum SceneAdvancement { + Manual, + Timed { time_s: f32 }, + SequenceComplete, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum NextScene { + NextInSequence, + Previous, + JumpTo { scene_name: String }, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Scene { + pub name: String, + pub playlist: String, + pub adv: SceneAdvancement, + pub next_scene: NextScene, +} diff --git a/src/model/session.rs b/src/model/session.rs new file mode 100644 index 0000000..be06d23 --- /dev/null +++ b/src/model/session.rs @@ -0,0 +1,54 @@ +use crate::model::playlist::Playlist; +use crate::model::scene::Scene; +use log::info; +use poise::serenity_prelude::ScheduledEventId; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Session { + pub event: ScheduledEventId, + pub scenes: Vec, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct SessionConfig { + pub active_session: Option, + pub playlists: Vec, + pub global_scenes: Vec, + pub sessions: Vec, +} + +impl SessionConfig { + pub async fn init(path: PathBuf) -> Result { + if path.exists() { + info!("Initializing with config at {:?}...", path); + Self::load_config(path).await + } else { + info!("Initializing with a blank config..."); + let config = Self::default(); + + config.save_config(path).await?; + Ok(config) + } + } + + pub async fn load_config(path: PathBuf) -> Result { + let file = tokio::fs::read(path).await?; + + let toml_string = String::from_utf8(file).unwrap(); + + Ok(toml::from_str(&toml_string).unwrap()) + } + + pub async fn save_config(&self, path: PathBuf) -> Result<(), std::io::Error> { + tokio::fs::write(path, toml::to_string(self).unwrap()).await + } + + pub async fn create_session(&mut self, event: ScheduledEventId) { + self.sessions.push(Session { + event, + scenes: vec![], + }) + } +}