diff --git a/bot.conf b/bot.conf index d82b2b7..e8c4308 100644 --- a/bot.conf +++ b/bot.conf @@ -1,3 +1,3 @@ [Meta] token = NDc4NTQ1NzY1MDc4MjY5OTU2.DlMQjQ.lqep6rd5w-uBOGst_cuEOQptt84 - +prefix = ! diff --git a/src/confighandler.rs b/src/confighandler.rs index 85f254c..f44a6b9 100644 --- a/src/confighandler.rs +++ b/src/confighandler.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use std::fmt::Display; use std::fs::File; use std::io::{BufRead, BufReader, Write}; diff --git a/src/main.rs b/src/main.rs index 28d5d46..a80be2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -#[macro_use] extern crate serenity; extern crate parking_lot; @@ -12,11 +11,10 @@ mod macros; // feature, it's not as ergonomic to work with as it could be. The client // provides a clean bridged integration with voice. use serenity::client::bridge::voice::ClientVoiceManager; -use serenity::client::{Client, Context, EventHandler, CACHE}; +use serenity::client::{Client, Context, EventHandler}; use serenity::framework::StandardFramework; use serenity::model::channel::Message; use serenity::model::gateway::Ready; -use serenity::model::misc::Mentionable; // Import the `Context` from the client and `parking_lot`'s `Mutex`. // // `parking_lot` offers much more efficient implementations of `std::sync`'s @@ -55,29 +53,54 @@ impl EventHandler for Handler { } } +struct Config { + token: String, + prefix: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + token: String::new(), + prefix: String::new(), + } + } +} + fn main() { // read config file - let config = check_result_return!(read_config("bot.conf")); + let config_file = check_result_return!(read_config("bot.conf")); - let token = match config.get("Meta") { - Some(info) => match info.get("token") { - Some(token_pair) => { - let mut token = String::new(); - display_error!(token_pair.set_value(&mut token)); - token + let mut config = Config::default(); + + match config_file.get("Meta") { + Some(info) => { + match info.get("token") { + Some(token_pair) => { + display_error!(token_pair.set_value(&mut config.token)); + } + None => { + println!("couldn't find token inside meta section"); + return; + } } - None => { - println!("couldn't find token inside meta section"); - return; + match info.get("prefix") { + Some(prefix_pair) => { + display_error!(prefix_pair.set_value(&mut config.prefix)); + } + None => { + println!("couldn't find prefix inside meta section"); + return; + } } - }, + } None => { println!("couldn't find Meta section in config file"); return; } }; - let mut client = Client::new(&token, Handler).expect("Err creating client"); + let mut client = Client::new(&config.token, Handler).expect("Err creating client"); // Obtain a lock to the data owned by the client, and insert the client's // voice manager into it. This allows the voice manager to be accessible by @@ -91,16 +114,24 @@ fn main() { client.with_framework( StandardFramework::new() - .configure(|c| c.prefix("~").on_mention(true)) - .cmd("deafen", deafen) - .cmd("join", join) - .cmd("leave", leave) - .cmd("mute", mute) + .configure(|c| c.prefix(&config.prefix).on_mention(true)) .cmd("play", Play::new(media_data.clone())) .cmd("pause", Pause::new(media_data.clone())) - .cmd("ping", ping) - .cmd("undeafen", undeafen) - .cmd("unmute", unmute), + .cmd( + "help", + Help::new( + &config.prefix, + vec![ + "play".to_string(), + "pause".to_string(), + "stop".to_string(), + "help".to_string(), + "list".to_string(), + ], + ), + ) + .cmd("stop", Stop::new(media_data.clone())) + .cmd("list", List::new(media_data.clone())), ); let _ = client @@ -108,175 +139,6 @@ fn main() { .map_err(|why| println!("Client ended: {:?}", why)); } -command!(deafen(ctx, msg) { - let guild_id = match CACHE.read().guild_channel(msg.channel_id) { - Some(channel) => channel.read().guild_id, - None => { - check_msg(msg.channel_id.say("Groups and DMs not supported")); - - return Ok(()); - }, - }; - - let mut manager_lock = ctx.data.lock().get::().cloned().unwrap(); - let mut manager = manager_lock.lock(); - - let handler = match manager.get_mut(guild_id) { - Some(handler) => handler, - None => { - check_msg(msg.reply("Not in a voice channel")); - - return Ok(()); - }, - }; - - if handler.self_deaf { - check_msg(msg.channel_id.say("Already deafened")); - } else { - handler.deafen(true); - - check_msg(msg.channel_id.say("Deafened")); - } -}); - -command!(join(ctx, msg) { - let guild = match msg.guild() { - Some(guild) => guild, - None => { - check_msg(msg.channel_id.say("Groups and DMs not supported")); - - return Ok(()); - } - }; - - let guild_id = guild.read().id; - - let channel_id = guild - .read() - .voice_states.get(&msg.author.id) - .and_then(|voice_state| voice_state.channel_id); - - - let connect_to = match channel_id { - Some(channel) => channel, - None => { - check_msg(msg.reply("Not in a voice channel")); - - return Ok(()); - } - }; - - let mut manager_lock = ctx.data.lock().get::().cloned().unwrap(); - let mut manager = manager_lock.lock(); - - if manager.join(guild_id, connect_to).is_some() { - check_msg(msg.channel_id.say(&format!("Joined {}", connect_to.mention()))); - } else { - check_msg(msg.channel_id.say("Error joining the channel")); - } -}); - -command!(leave(ctx, msg) { - let guild_id = match CACHE.read().guild_channel(msg.channel_id) { - Some(channel) => channel.read().guild_id, - None => { - check_msg(msg.channel_id.say("Groups and DMs not supported")); - - return Ok(()); - }, - }; - - let mut manager_lock = ctx.data.lock().get::().cloned().unwrap(); - let mut manager = manager_lock.lock(); - let has_handler = manager.get(guild_id).is_some(); - - if has_handler { - manager.remove(guild_id); - - check_msg(msg.channel_id.say("Left voice channel")); - } else { - check_msg(msg.reply("Not in a voice channel")); - } -}); - -command!(mute(ctx, msg) { - let guild_id = match CACHE.read().guild_channel(msg.channel_id) { - Some(channel) => channel.read().guild_id, - None => { - check_msg(msg.channel_id.say("Groups and DMs not supported")); - - return Ok(()); - }, - }; - - let mut manager_lock = ctx.data.lock().get::().cloned().unwrap(); - let mut manager = manager_lock.lock(); - - let handler = match manager.get_mut(guild_id) { - Some(handler) => handler, - None => { - check_msg(msg.reply("Not in a voice channel")); - - return Ok(()); - }, - }; - - if handler.self_mute { - check_msg(msg.channel_id.say("Already muted")); - } else { - handler.mute(true); - - check_msg(msg.channel_id.say("Now muted")); - } -}); - -command!(ping(_context, msg) { - check_msg(msg.channel_id.say("Pong!")); -}); - -command!(undeafen(ctx, msg) { - let guild_id = match CACHE.read().guild_channel(msg.channel_id) { - Some(channel) => channel.read().guild_id, - None => { - check_msg(msg.channel_id.say("Error finding channel info")); - - return Ok(()); - }, - }; - - let mut manager_lock = ctx.data.lock().get::().cloned().unwrap(); - let mut manager = manager_lock.lock(); - - if let Some(handler) = manager.get_mut(guild_id) { - handler.deafen(false); - - check_msg(msg.channel_id.say("Undeafened")); - } else { - check_msg(msg.channel_id.say("Not in a voice channel to undeafen in")); - } -}); - -command!(unmute(ctx, msg) { - let guild_id = match CACHE.read().guild_channel(msg.channel_id) { - Some(channel) => channel.read().guild_id, - None => { - check_msg(msg.channel_id.say("Error finding channel info")); - - return Ok(()); - }, - }; - let mut manager_lock = ctx.data.lock().get::().cloned().unwrap(); - let mut manager = manager_lock.lock(); - - if let Some(handler) = manager.get_mut(guild_id) { - handler.mute(false); - - check_msg(msg.channel_id.say("Unmuted")); - } else { - check_msg(msg.channel_id.say("Not in a voice channel to undeafen in")); - } -}); - /// Checks that a message successfully sent; if not, then logs why to stdout. fn check_msg(result: SerenityResult) { if let Err(why) = result { diff --git a/src/player.rs b/src/player.rs index 943ec9e..a9b2707 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,5 +1,7 @@ use parking_lot; +use youtube::convert_file_name; + use serenity; use serenity::client::CACHE; use serenity::model::id::{ChannelId, GuildId}; @@ -7,7 +9,7 @@ use serenity::voice::{AudioSource, Handler, LockedAudio}; use std::cell::RefCell; use std::ops::DerefMut; -use std::sync::Arc; +use std::sync::{Arc, Mutex, MutexGuard}; use std::thread; use std::time; @@ -27,7 +29,7 @@ pub struct Song { pub struct MediaData { playlist: RefCell>, - current_song: RefCell>, + current_song: Mutex>>, } fn guild_id(channel_id: ChannelId) -> Option { @@ -51,6 +53,10 @@ fn handler<'a>( } impl MediaData { + fn song_mut(&self) -> MutexGuard>> { + self.current_song.lock().unwrap() + } + fn start_thread( media: &Arc, channel_id: ChannelId, @@ -62,51 +68,66 @@ impl MediaData { let manager_clone = manager_lock.clone(); thread::spawn(move || loop { - let mut borrow = media_clone.current_song.borrow_mut(); + { + let song_lock = media_clone.song_mut(); + let mut borrow = song_lock.borrow_mut(); - if let Some(ref mut song) = borrow.deref_mut() { - let song_lock = song.clone(); - let audio = song_lock.lock(); + if let Some(ref mut song) = borrow.deref_mut() { + let song_lock = song.clone(); + let audio = song_lock.lock(); - if audio.finished { - if !Self::next_song( - song, - media_clone.playlist.borrow_mut().deref_mut(), - channel_id, - &manager_clone, - ) { - manager_clone - .lock() - .remove(check_option!(guild_id(channel_id))); + if audio.finished { + if !Self::next_song( + song, + media_clone.playlist.borrow_mut().deref_mut(), + channel_id, + &manager_clone, + ) { + manager_clone + .lock() + .remove(check_option!(guild_id(channel_id))); - return; + println!("left channel"); + + return; + } } + + continue; } - continue; - } + let mut playlist = media_clone.playlist.borrow_mut(); - let mut playlist = media_clone.playlist.borrow_mut(); + if !playlist.is_empty() { + let mut manager = manager_clone.lock(); - if !playlist.is_empty() { - let mut manager = manager_clone.lock(); + if let Some(handler) = + handler(check_option!(guild_id(channel_id)), &mut manager) + { + let first = playlist.remove(0); - if let Some(handler) = handler(check_option!(guild_id(channel_id)), &mut manager) { - let first = playlist.remove(0); + *borrow = Some(handler.play_returning(first.source)); - *borrow = Some(handler.play_returning(first.source)); - - super::check_msg(channel_id.say(format!( - "Playing song: {}", - super::youtube::convert_file_name(first.name) - ))); + super::check_msg(channel_id.say(format!( + "Playing song: {}", + super::youtube::convert_file_name(first.name) + ))); + } else { + return; + } } else { + manager_clone + .lock() + .remove(check_option!(guild_id(channel_id))); + + println!("left channel"); + return; } } - let two_sec = time::Duration::new(2, 0); - thread::sleep(two_sec); + let five_sec = time::Duration::from_secs(5); + thread::sleep(five_sec); }); } @@ -150,7 +171,7 @@ impl Default for MediaData { fn default() -> MediaData { MediaData { playlist: RefCell::new(Vec::new()), - current_song: RefCell::new(None), + current_song: Mutex::new(RefCell::new(None)), } } } @@ -166,6 +187,54 @@ impl Play { pub fn new(media_data: Arc) -> Play { Play { media: media_data } } + + fn check_for_continue(song_lock: MutexGuard>>) -> bool { + match song_lock.borrow_mut().deref_mut() { + Some(song) => { + let song_clone = song.clone(); + let mut audio_lock = song_clone.lock(); + + audio_lock.play(); + true + } + None => false, + } + } + + fn check_join_channel( + manager_lock: &Arc>, + msg: &serenity::model::channel::Message, + ) { + let guild = match msg.guild() { + Some(guild) => guild, + None => { + display_error!(msg.channel_id.say("Groups and DMs not supported")); + + return; + } + }; + + let guild_id = guild.read().id; + + let channel_id = guild + .read() + .voice_states + .get(&msg.author.id) + .and_then(|voice_state| voice_state.channel_id); + + let connect_to = match channel_id { + Some(channel) => channel, + None => { + display_error!(msg.reply("Not in a voice channel")); + + return; + } + }; + + let mut manager = manager_lock.lock(); + + manager.join(guild_id, connect_to); + } } impl serenity::framework::standard::Command for Play { @@ -177,26 +246,24 @@ impl serenity::framework::standard::Command for Play { mut args: serenity::framework::standard::Args, ) -> ::std::result::Result<(), serenity::framework::standard::CommandError> { let url = match args.single::() { - Ok(url) => url, + Ok(url) => { + if !url.starts_with("http") { + super::check_msg(msg.channel_id.say("Must provide a valid URL")); + + return Ok(()); + } + + url + } Err(_) => { - super::check_msg(msg.channel_id.say("Must provide a URL to a video or audio")); + if !Self::check_for_continue(self.media.song_mut()) { + super::check_msg(msg.channel_id.say("Must provide a URL to a video or audio")); + } return Ok(()); } }; - if !url.starts_with("http") { - super::check_msg(msg.channel_id.say("Must provide a valid URL")); - - return Ok(()); - } - - let mut manager_lock = ctx.data - .lock() - .get::() - .cloned() - .unwrap(); - let mut source = match super::youtube::youtube_dl(&url) { Ok(source) => source, Err(why) => { @@ -210,6 +277,14 @@ impl serenity::framework::standard::Command for Play { self.media.playlist.borrow_mut().append(&mut source); + let mut manager_lock = ctx.data + .lock() + .get::() + .cloned() + .unwrap(); + + Self::check_join_channel(&manager_lock, msg); + MediaData::start_thread(&self.media, msg.channel_id, &manager_lock); Ok(()) @@ -234,7 +309,9 @@ impl serenity::framework::standard::Command for Pause { _: &serenity::model::channel::Message, _: serenity::framework::standard::Args, ) -> ::std::result::Result<(), serenity::framework::standard::CommandError> { - if let Some(song) = self.media.current_song.borrow_mut().deref_mut() { + let song_lock = self.media.song_mut(); + + if let Some(song) = song_lock.borrow_mut().deref_mut() { let song_clone = song.clone(); let mut audio_lock = song_clone.lock(); @@ -244,3 +321,129 @@ impl serenity::framework::standard::Command for Pause { Ok(()) } } + +pub struct List { + media: Arc, +} + +impl List { + pub fn new(media_data: Arc) -> List { + List { media: media_data } + } +} + +impl serenity::framework::standard::Command for List { + #[allow(unreachable_code, unused_mut)] + fn execute( + &self, + _: &mut serenity::client::Context, + msg: &serenity::model::channel::Message, + _: serenity::framework::standard::Args, + ) -> ::std::result::Result<(), serenity::framework::standard::CommandError> { + let mut output = String::new(); + + let playlist = self.media.playlist.borrow(); + + output += &format!( + "{} {} queued\n", + playlist.len(), + if playlist.len() == 1 { "song" } else { "songs" } + ); + + for (i, song) in playlist.iter().enumerate() { + output += &format!("\t{}.\t{}\n", i + 1, convert_file_name(song.name.clone())); + } + + super::check_msg(msg.channel_id.say(output)); + + Ok(()) + } +} + +pub struct Help { + prefix: String, + commands: Vec, +} + +impl Help { + pub fn new(prefix: &String, commands: Vec) -> Help { + Help { + prefix: prefix.clone(), + commands: commands, + } + } +} + +impl serenity::framework::standard::Command for Help { + #[allow(unreachable_code, unused_mut)] + fn execute( + &self, + _: &mut serenity::client::Context, + msg: &serenity::model::channel::Message, + _: serenity::framework::standard::Args, + ) -> ::std::result::Result<(), serenity::framework::standard::CommandError> { + let mut output = String::new(); + + output += "Available commands:\n"; + + for command in &self.commands { + output += &format!("\t{}{}\n", self.prefix, command); + } + + super::check_msg(msg.channel_id.say(output)); + + Ok(()) + } +} + +pub struct Stop { + media: Arc, +} + +impl Stop { + pub fn new(media_data: Arc) -> Stop { + Stop { media: media_data } + } +} + +impl serenity::framework::standard::Command for Stop { + #[allow(unreachable_code, unused_mut)] + fn execute( + &self, + ctx: &mut serenity::client::Context, + msg: &serenity::model::channel::Message, + _: serenity::framework::standard::Args, + ) -> ::std::result::Result<(), serenity::framework::standard::CommandError> { + self.media.playlist.borrow_mut().clear(); + + { + let song = self.media.song_mut(); + *song.borrow_mut() = None; + } + + { + let mut manager_lock = ctx.data + .lock() + .get::() + .cloned() + .unwrap(); + + let mut manager = manager_lock.lock(); + let guild_id = match guild_id(msg.channel_id) { + Some(guild_id) => guild_id, + None => return Ok(()), + }; + + let handler = match handler(guild_id, &mut manager) { + Some(handler) => handler, + None => return Ok(()), + }; + + println!("stopped handler"); + + handler.stop(); + } + + Ok(()) + } +} diff --git a/src/youtube.rs b/src/youtube.rs index 127b85c..d8002c8 100644 --- a/src/youtube.rs +++ b/src/youtube.rs @@ -60,13 +60,6 @@ fn convert_output(out: &Output) -> Result, String> { } } -fn print_output(out: &Output) { - match from_utf8(out.stdout.as_slice()) { - Ok(string) => println!("value: {}", string), - Err(_) => println!("error converting output"), - }; -} - pub fn youtube_dl(uri: &str) -> Result, String> { let args = ["-f", "webm[abr>0]/bestaudio/best", uri];