diff --git a/flake.nix b/flake.nix index a921715..0159ca6 100644 --- a/flake.nix +++ b/flake.nix @@ -35,7 +35,7 @@ packages = with pkgs; flake-utils.lib.flattenTree rec { default = rustPlatform.buildRustPackage rec { name = "memejoin-rs"; - version = "0.1.1-alpha"; + version = "0.1.2-alpha"; src = self; nativeBuildInputs = [ local-rust cmake gcc libopus ]; @@ -46,7 +46,7 @@ docker = dockerTools.buildImage { name = "memejoin-rs"; - tag = "0.1.1.alpha"; + tag = "0.1.2-alpha"; copyToRoot = buildEnv { name = "image-root"; paths = [ default ffmpeg libopus youtube-dl ]; diff --git a/src/main.rs b/src/main.rs index a693142..c59db1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,23 +2,33 @@ #![feature(proc_macro_hygiene)] #![feature(async_closure)] +use songbird::tracks::TrackQueue; use std::collections::HashMap; use std::env; use std::sync::Arc; +use tokio::sync::mpsc; use serde::Deserialize; use serenity::async_trait; -use serenity::model::prelude::{Channel, GuildId, Ready}; +use serenity::model::prelude::{Channel, ChannelId, GuildId, Member, Ready}; use serenity::model::voice::VoiceState; use serenity::prelude::GatewayIntents; use serenity::prelude::*; use songbird::SerenityInit; use tracing::*; -struct Handler; +enum HandlerMessage { + Ready(Context), + PlaySound(Context, Member, ChannelId), + TrackEnded(GuildId), +} + +struct Handler { + tx: std::sync::Mutex>, +} struct TrackEventHandler { - ctx: Context, + tx: mpsc::Sender, guild_id: GuildId, } @@ -28,36 +38,35 @@ impl songbird::EventHandler for TrackEventHandler { &'a self, ctx: &'b songbird::EventContext<'c>, ) -> Option { - if let songbird::EventContext::Track(tracks) = ctx { - for (track_state, _track_handle) in tracks.iter() { - // If any track is still playing, don't leave the channel yet - // there are more sounds to be played (I think) - if track_state.playing == songbird::tracks::PlayMode::Play { - return None; - } + if let songbird::EventContext::Track(_) = ctx { + if let Err(err) = self + .tx + .send(HandlerMessage::TrackEnded(self.guild_id)) + .await + { + error!("Failed to send track end message to handler: {err}"); } } - let manager = songbird::get(&self.ctx).await.expect("should get manager"); - if let Err(err) = manager.leave(self.guild_id).await { - error!("Failed to leave voice channel: {err:?}"); - } - None } } #[derive(Debug, Clone, Deserialize)] struct Settings { + guilds: HashMap, +} +impl TypeMapKey for Settings { + type Value = Arc; +} + +#[derive(Debug, Clone, Deserialize)] +struct GuildSettings { #[serde(alias = "userEnteredSoundDelay")] _sound_delay: u64, channels: HashMap, } -impl TypeMapKey for Settings { - type Value = Arc; -} - #[derive(Debug, Clone, Deserialize)] struct ChannelSettings { #[serde(alias = "enterUsers")] @@ -85,7 +94,17 @@ enum SoundType { #[async_trait] impl EventHandler for Handler { - async fn ready(&self, _ctx: Context, ready: Ready) { + async fn ready(&self, ctx: Context, ready: Ready) { + let tx = self + .tx + .lock() + .expect("failed to get message sender lock") + .clone(); + + tx.send(HandlerMessage::Ready(ctx)) + .await + .unwrap_or_else(|err| panic!("failed to send ready message to handler: {err}")); + info!("{} is ready", ready.user.name); } @@ -107,68 +126,17 @@ impl EventHandler for Handler { .unwrap_or("no_guild_name".to_string()) ); - let settings = { - let data_read = ctx.data.read().await; + let tx = self + .tx + .lock() + .expect("couldn't get lock for Handler messenger") + .clone(); - data_read - .get::() - .expect("settings should exist") - .clone() - }; - - let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx.cache) else { - error!("Failed to get cached channel from member!"); - return; - }; - - let Some(user) = settings.channels.get(channel.name()).and_then(|c| c.users.get(&member.user.name)) else { - info!("No sound associated for {} in channel {}", member.user.name, channel.name()); - return; - }; - - let Some(manager) = songbird::get(&ctx).await else { - error!("Failed to get songbird manager from context"); - return; - }; - - match manager.join(member.guild_id, channel_id).await { - (handler_lock, Ok(())) => { - let mut handler = handler_lock.lock().await; - - let source = match user.ty { - SoundType::Youtube => match songbird::ytdl(&user.sound).await { - Ok(source) => source, - Err(err) => { - error!("Error starting youtube source: {err:?}"); - return; - } - }, - SoundType::File => { - match songbird::ffmpeg(format!("sounds/{}", &user.sound)).await { - Ok(source) => source, - Err(err) => { - error!("Error starting file source: {err:?}"); - return; - } - } - } - }; - - let track_handler = handler.enqueue_source(source); - if let Err(err) = track_handler.add_event( - songbird::Event::Track(songbird::TrackEvent::End), - TrackEventHandler { - ctx, - guild_id: member.guild_id, - }, - ) { - error!("Failed to add event handler to track handle: {err:?}"); - }; - } - - (_, Err(err)) => { - error!("Failed to join voice channel {}: {err:?}", channel.name()); - } + if let Err(err) = tx + .send(HandlerMessage::PlaySound(ctx, member, channel_id)) + .await + { + error!("Failed to send play sound message to handler: {err}"); } } } @@ -189,22 +157,22 @@ async fn main() { info!("{settings:?}"); + let songbird = songbird::Songbird::serenity(); + + let (tx, mut rx) = mpsc::channel(10); + let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_VOICE_STATES; let mut client = Client::builder(&token, intents) - .event_handler(Handler) - .register_songbird() + .event_handler(Handler { + tx: std::sync::Mutex::new(tx.clone()), + }) + .register_songbird_with(songbird.clone()) .await .expect("Error creating client"); - { - let mut data = client.data.write().await; - - data.insert::(Arc::new(settings)); - } - info!("Starting bot with token '{token}'"); tokio::spawn(async move { if let Err(err) = client.start().await { @@ -212,6 +180,102 @@ async fn main() { } }); + tokio::spawn(async move { + while let Some(msg) = rx.recv().await { + match msg { + HandlerMessage::Ready(ctx) => { + info!("Got Ready message"); + + let songbird = songbird::get(&ctx).await.expect("no songbird instance"); + + for guild_id in settings.guilds.keys() { + let handler_lock = songbird.get_or_insert(GuildId(*guild_id)); + + let mut handler = handler_lock.lock().await; + + handler.add_global_event( + songbird::Event::Track(songbird::TrackEvent::End), + TrackEventHandler { + tx: tx.clone(), + guild_id: GuildId(*guild_id), + }, + ); + } + } + HandlerMessage::TrackEnded(guild_id) => { + info!("Got TrackEnded message"); + + if let Some(manager) = songbird.get(guild_id) { + let mut handler = manager.lock().await; + let queue = handler.queue(); + + if queue.is_empty() { + info!("Track Queue is empty, leaving voice channel"); + if let Err(err) = handler.leave().await { + error!("Failed to leave channel: {err:?}"); + } + } + } + } + + HandlerMessage::PlaySound(ctx, member, channel_id) => { + info!("Got PlaySound message"); + + let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx.cache) else { + error!("Failed to get cached channel from member!"); + continue; + }; + + let Some(user) = settings.guilds.get(channel.guild_id.as_u64()) + .and_then(|guild| guild.channels.get(channel.name())) + .and_then(|c| c.users.get(&member.user.name)) + else { + info!("No sound associated for {} in channel {}", member.user.name, channel.name()); + continue; + }; + + let source = match user.ty { + SoundType::Youtube => match songbird::ytdl(&user.sound).await { + Ok(source) => source, + Err(err) => { + error!( + "Error starting youtube source from {}: {err:?}", + user.sound + ); + continue; + } + }, + SoundType::File => { + match songbird::ffmpeg(format!("sounds/{}", &user.sound)).await { + Ok(source) => source, + Err(err) => { + error!( + "Error starting file source from {}: {err:?}", + user.sound + ); + continue; + } + } + } + }; + + match songbird.join(member.guild_id, channel_id).await { + (handler_lock, Ok(())) => { + let mut handler = handler_lock.lock().await; + + let _track_handler = handler.enqueue_source(source); + // TODO: set volume + } + + (_, Err(err)) => { + error!("Failed to join voice channel {}: {err:?}", channel.name()); + } + } + } + } + } + }); + let _ = tokio::signal::ctrl_c().await; info!("Received Ctrl-C, shuttdown down."); }