Reimplement queue and add guild settings

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/2/head
trivernis 3 years ago
parent 1fad2950b5
commit b6dd726c17
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -17,6 +17,27 @@
</Attribute>
</value>
</entry>
<entry key="/src/commands/music/mod.rs">
<value>
<Attribute>
<option name="separator" value=":" />
</Attribute>
</value>
</entry>
<entry key="/src/commands/music/play.rs">
<value>
<Attribute>
<option name="separator" value=":" />
</Attribute>
</value>
</entry>
<entry key="/src/commands/music/play_next.rs">
<value>
<Attribute>
<option name="separator" value=":" />
</Attribute>
</value>
</entry>
<entry key="/src/commands/music/utils.rs">
<value>
<Attribute>
@ -24,6 +45,13 @@
</Attribute>
</value>
</entry>
<entry key="/src/database/guild.rs">
<value>
<Attribute>
<option name="separator" value="," />
</Attribute>
</value>
</entry>
<entry key="/src/database/scripts/update_tables.sql">
<value>
<Attribute>
@ -38,6 +66,13 @@
</Attribute>
</value>
</entry>
<entry key="/src/providers/ytdl/mod.rs">
<value>
<Attribute>
<option name="separator" value=":" />
</Attribute>
</value>
</entry>
</map>
</option>
</component>

@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="60" name="Rust" />
</Languages>
</inspection_tool>
</profile>
</component>

1
Cargo.lock generated

@ -1666,7 +1666,6 @@ version = "0.1.0"
dependencies = [
"dotenv",
"minecraft-data-rs",
"parking_lot",
"rand 0.8.3",
"rusqlite",
"serde",

@ -15,8 +15,7 @@ rusqlite = "0.24"
serde_derive = "1.0.125"
serde = "1.0.125"
thiserror = "1.0.24"
parking_lot = "0.11.1"
minecraft-data-rs = "0.2.0"
songbird = {version = "0.1.5", features=["builtin-queue"]}
songbird = "0.1.5"
serde_json = "1.0.64"
rand = "0.8.3"

@ -1,10 +1,10 @@
use serenity::async_trait;
use serenity::client::{Context, EventHandler};
use serenity::framework::standard::macros::hook;
use serenity::Client;
use serenity::framework::standard::{CommandResult, DispatchError};
use serenity::framework::standard::macros::hook;
use serenity::framework::StandardFramework;
use serenity::model::channel::Message;
use serenity::Client;
use songbird::SerenityInit;
use crate::commands::*;
@ -41,12 +41,13 @@ pub fn get_framework() -> StandardFramework {
.unwrap_or("~!".to_string())
.as_str(),
)
.allow_dm(true)
.ignore_bots(true)
.allow_dm(true)
.ignore_bots(true)
})
.group(&MINECRAFT_GROUP)
.group(&MISC_GROUP)
.group(&MUSIC_GROUP)
.group(&SETTINGS_GROUP)
.after(after_hook)
.before(before_hook)
.on_dispatch_error(dispatch_error)
@ -67,6 +68,7 @@ async fn before_hook(ctx: &Context, msg: &Message, _: &str) -> bool {
let _ = msg.channel_id.broadcast_typing(ctx).await;
true
}
#[hook]
async fn dispatch_error(ctx: &Context, msg: &Message, error: DispatchError) {
match error {

@ -9,6 +9,7 @@ use crate::utils::store::Store;
#[usage("enchantment <enchantment-name>")]
#[example("item unbreaking")]
#[min_args(1)]
#[aliases("ench")]
pub(crate) async fn enchantment(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let data = ctx.data.read().await;
let store = data.get::<Store>().expect("Failed to get store");
@ -31,7 +32,8 @@ pub(crate) async fn enchantment(ctx: &Context, msg: &Message, args: Args) -> Com
e = e
.title(enchantment.display_name)
.field("Name", enchantment.name, false)
.field("Category", enchantment.category, false);
.field("Category", enchantment.category, false)
.thumbnail("https://minecraftitemids.com/item/128/enchanted_book.png");
if !enchantment.exclude.is_empty() {
e = e.field("Incompatible With", enchantment.exclude.join(", "), false);
}

@ -9,6 +9,7 @@ use crate::utils::store::Store;
#[usage("item <item-name>")]
#[example("item bread")]
#[min_args(1)]
#[aliases("i")]
pub(crate) async fn item(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let data = ctx.data.read().await;
let store = data.get::<Store>().expect("Failed to get store");

@ -1,9 +1,9 @@
use std::collections::HashSet;
use serenity::client::Context;
use serenity::framework::standard::macros::help;
use serenity::framework::standard::{help_commands, Args};
use serenity::framework::standard::{Args, help_commands};
use serenity::framework::standard::{CommandGroup, CommandResult, HelpOptions};
use serenity::framework::standard::macros::help;
use serenity::model::channel::Message;
use serenity::model::id::UserId;

@ -3,7 +3,7 @@ use serenity::framework::standard::macros::group;
use ping::PING_COMMAND;
pub(crate) mod help;
pub(crate) mod ping;
mod ping;
#[group]
#[commands(ping)]

@ -1,6 +1,6 @@
use serenity::client::Context;
use serenity::framework::standard::CommandResult;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
#[command]

@ -2,7 +2,9 @@ pub use minecraft::MINECRAFT_GROUP;
pub use misc::help::HELP;
pub use misc::MISC_GROUP;
pub use music::MUSIC_GROUP;
pub use settings::SETTINGS_GROUP;
pub(crate) mod minecraft;
pub(crate) mod misc;
pub(crate) mod music;
pub(crate) mod settings;

@ -0,0 +1,28 @@
use serenity::client::Context;
use serenity::framework::standard::CommandResult;
use serenity::framework::standard::macros::command;
use serenity::model::channel::Message;
use crate::commands::music::get_queue_for_guild;
#[command]
#[only_in(guilds)]
#[description("Clears the queue")]
#[usage("clear")]
#[aliases("cl")]
#[allowed_roles("DJ")]
async fn clear(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let queue = get_queue_for_guild(ctx, &guild.id).await?;
{
let mut queue_lock = queue.lock().await;
queue_lock.clear();
}
msg.channel_id
.say(ctx, "The queue has been cleared")
.await?;
Ok(())
}

@ -1,8 +1,10 @@
use serenity::client::Context;
use serenity::framework::standard::CommandResult;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{CommandError, CommandResult};
use serenity::model::channel::Message;
use crate::commands::music::get_queue_for_guild;
#[command]
#[only_in(guilds)]
#[description("Displays the currently playing song")]
@ -11,17 +13,10 @@ use serenity::model::channel::Message;
async fn current(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let handler_lock = manager
.get(guild.id)
.ok_or(CommandError::from("Not in a voice channel"))?;
let handler = handler_lock.lock().await;
let queue = get_queue_for_guild(ctx, &guild.id).await?;
let queue_lock = queue.lock().await;
if let Some(current) = handler.queue().current() {
if let Some(current) = queue_lock.current() {
let metadata = current.metadata().clone();
msg.channel_id
.send_message(ctx, |m| {

@ -3,7 +3,7 @@ use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
use crate::commands::music::utils::{get_channel_for_author, join_channel};
use crate::commands::music::{get_channel_for_author, join_channel};
#[command]
#[only_in(guilds)]

@ -3,22 +3,32 @@ use serenity::framework::standard::CommandResult;
use serenity::framework::standard::macros::command;
use serenity::model::channel::Message;
use crate::commands::music::{get_queue_for_guild, get_voice_manager};
#[command]
#[only_in(guilds)]
#[description("Leaves a voice channel")]
#[usage("leave")]
#[aliases("stop")]
#[allowed_roles("DJ")]
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild_id = guild.id;
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let manager = get_voice_manager(ctx).await;
let queue = get_queue_for_guild(ctx, &guild.id).await?;
let queue_lock = queue.lock().await;
let handler = manager.get(guild.id);
if let Some(handler) = handler {
let mut handler_lock = handler.lock().await;
handler_lock.remove_all_global_events();
}
if let Some(current) = queue_lock.current() {
current.stop()?;
}
if manager.get(guild_id).is_some() {
manager.remove(guild_id).await?;
if manager.get(guild.id).is_some() {
manager.remove(guild.id).await?;
msg.channel_id.say(ctx, "Left the voice channel").await?;
} else {
msg.channel_id.say(ctx, "Not in a voice channel").await?;

@ -1,23 +1,181 @@
use std::sync::Arc;
use serenity::async_trait;
use serenity::client::Context;
use serenity::framework::standard::macros::group;
use serenity::http::Http;
use serenity::model::channel::Message;
use serenity::model::guild::Guild;
use serenity::model::id::{ChannelId, GuildId, UserId};
use songbird::{
Call, Event, EventContext, EventHandler as VoiceEventHandler, Songbird, TrackEvent,
};
use tokio::sync::Mutex;
use clear::CLEAR_COMMAND;
use current::CURRENT_COMMAND;
use join::JOIN_COMMAND;
use leave::LEAVE_COMMAND;
use play::PLAY_COMMAND;
use play_next::PLAY_NEXT_COMMAND;
use queue::QUEUE_COMMAND;
use shuffle::SHUFFLE_COMMAND;
use skip::SKIP_COMMAND;
use crate::providers::music::{get_video_information, get_videos_for_playlist};
use crate::providers::music::queue::{MusicQueue, Song};
use crate::utils::error::{BotError, BotResult};
use crate::utils::store::Store;
mod clear;
mod current;
mod join;
mod leave;
mod play;
mod play_next;
mod queue;
mod shuffle;
mod skip;
mod utils;
#[group]
#[commands(join, leave, play, queue, skip, shuffle, current)]
#[commands(join, leave, play, queue, skip, shuffle, current, play_next, clear)]
#[prefix("m")]
pub struct Music;
/// Joins a voice channel
async fn join_channel(ctx: &Context, channel_id: ChannelId, guild_id: GuildId) -> Arc<Mutex<Call>> {
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let (handler, _) = manager.join(guild_id, channel_id).await;
let mut data = ctx.data.write().await;
let store = data.get_mut::<Store>().unwrap();
let queue = Arc::new(Mutex::new(MusicQueue::new()));
store.music_queues.insert(guild_id, queue.clone());
{
let mut handler_lock = handler.lock().await;
handler_lock.add_global_event(
Event::Track(TrackEvent::End),
SongEndNotifier {
channel_id,
http: ctx.http.clone(),
queue: Arc::clone(&queue),
handler: handler.clone(),
},
);
}
handler
}
/// Returns the voice channel the author is in
fn get_channel_for_author(author_id: &UserId, guild: &Guild) -> BotResult<ChannelId> {
guild
.voice_states
.get(author_id)
.and_then(|voice_state| voice_state.channel_id)
.ok_or(BotError::from("Not in a voice channel."))
}
/// Returns the voice manager from the context
async fn get_voice_manager(ctx: &Context) -> Arc<Songbird> {
songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone()
}
/// Returns a reference to a guilds music queue
async fn get_queue_for_guild(
ctx: &Context,
guild_id: &GuildId,
) -> BotResult<Arc<Mutex<MusicQueue>>> {
let data = ctx.data.read().await;
let store = data.get::<Store>().unwrap();
let queue = store
.music_queues
.get(guild_id)
.ok_or(BotError::from("No queue for server"))?
.clone();
Ok(queue)
}
struct SongEndNotifier {
channel_id: ChannelId,
http: Arc<Http>,
queue: Arc<Mutex<MusicQueue>>,
handler: Arc<Mutex<Call>>,
}
#[async_trait]
impl VoiceEventHandler for SongEndNotifier {
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
play_next_in_queue(&self.http, &self.channel_id, &self.queue, &self.handler).await;
None
}
}
/// Plays the next song in the queue
async fn play_next_in_queue(
http: &Arc<Http>,
channel_id: &ChannelId,
queue: &Arc<Mutex<MusicQueue>>,
handler: &Arc<Mutex<Call>>,
) {
let mut queue_lock = queue.lock().await;
if let Some(next) = queue_lock.next() {
let source = match songbird::ytdl(&next.url).await {
Ok(s) => s,
Err(e) => {
let _ = channel_id
.say(&http, format!("Failed to enqueue {}: {:?}", next.title, e))
.await;
return;
}
};
let mut handler_lock = handler.lock().await;
let track = handler_lock.play_only_source(source);
queue_lock.set_current(track);
} else {
queue_lock.clear_current();
}
}
/// Returns the list of songs for a given url
async fn get_songs_for_url(ctx: &&Context, msg: &Message, url: &str) -> BotResult<Vec<Song>> {
let mut songs: Vec<Song> = get_videos_for_playlist(url)?
.into_iter()
.map(Song::from)
.collect();
if songs.len() == 0 {
let song: Song = get_video_information(url)?.into();
msg.channel_id
.send_message(&ctx.http, |m| {
m.embed(|mut e| {
e = e.description(format!("Added [{}]({}) to the queue", song.title, song.url));
if let Some(thumb) = &song.thumbnail {
e = e.thumbnail(thumb);
}
e
})
})
.await?;
songs.push(song);
} else {
msg.channel_id
.send_message(&ctx.http, |m| {
m.embed(|e| e.description(format!("Added {} songs to the queue", songs.len())))
})
.await?;
}
Ok(songs)
}

@ -1,10 +1,14 @@
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandError, CommandResult};
use serenity::framework::standard::macros::command;
use serenity::model::channel::Message;
use crate::commands::music::utils::{get_channel_for_author, join_channel};
use crate::providers::ytdl::get_videos_for_url;
use crate::commands::music::{
get_channel_for_author, get_queue_for_guild, get_songs_for_url, get_voice_manager,
join_channel, play_next_in_queue,
};
use crate::database::get_database_from_context;
use crate::database::guild::SETTING_AUTOSHUFFLE;
#[command]
#[only_in(guilds)]
@ -22,11 +26,7 @@ async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let manager = get_voice_manager(ctx).await;
let mut handler = manager.get(guild.id);
if handler.is_none() {
@ -36,56 +36,29 @@ async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
}
let handler_lock = handler.ok_or(CommandError::from("Not in a voice channel"))?;
let mut handler = handler_lock.lock().await;
let mut videos: Vec<String> = get_videos_for_url(url)?
.into_iter()
.map(|v| format!("https://www.youtube.com/watch?v={}", v.url))
.collect();
if videos.len() == 0 {
videos.push(url.to_string());
}
let mut metadata = None;
for video in &videos {
let source = match songbird::ytdl(video).await {
Ok(s) => s,
Err(e) => {
msg.channel_id
.say(ctx, format!("Failed to enqueue {}: {:?}", video, e))
.await?;
continue;
}
};
let songs = get_songs_for_url(&ctx, msg, url).await?;
metadata = Some(source.metadata.clone());
let queue = get_queue_for_guild(ctx, &guild.id).await?;
handler.enqueue_source(source);
}
if videos.len() == 1 {
let metadata = metadata.unwrap();
msg.channel_id
.send_message(&ctx.http, |m| {
m.embed(|mut e| {
e = e.description(format!(
"Added [{}]({}) to the queue",
metadata.title.unwrap(),
url
));
if let Some(thumb) = metadata.thumbnail {
e = e.thumbnail(thumb);
}
let play_first = {
let mut queue_lock = queue.lock().await;
for song in songs {
queue_lock.add(song);
}
let database = get_database_from_context(ctx).await;
let database_lock = database.lock().await;
let autoshuffle = database_lock
.get_guild_setting(&guild.id, SETTING_AUTOSHUFFLE)
.unwrap_or(false);
if autoshuffle {
queue_lock.shuffle();
}
queue_lock.current().is_none()
};
e
})
})
.await?;
} else {
msg.channel_id
.send_message(&ctx.http, |m| {
m.embed(|e| e.description(format!("Added {} songs to the queue", videos.len())))
})
.await?;
if play_first {
play_next_in_queue(&ctx.http, &msg.channel_id, &queue, &handler_lock).await;
}
Ok(())

@ -0,0 +1,57 @@
use serenity::client::Context;
use serenity::framework::standard::{Args, CommandError, CommandResult};
use serenity::framework::standard::macros::command;
use serenity::model::channel::Message;
use crate::commands::music::{
get_channel_for_author, get_queue_for_guild, get_songs_for_url, get_voice_manager,
join_channel, play_next_in_queue,
};
#[command]
#[only_in(guilds)]
#[description("Puts a song as the next to play in the queue")]
#[usage("play_next <song-url>")]
#[min_args(1)]
#[max_args(2)]
#[aliases("pn")]
#[allowed_roles("DJ")]
async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let url = args.message();
if !url.starts_with("http") {
return Err(CommandError::from("The provided url is not valid"));
}
let guild = msg.guild(&ctx.cache).await.unwrap();
let manager = get_voice_manager(ctx).await;
let mut handler = manager.get(guild.id);
if handler.is_none() {
msg.guild(&ctx.cache).await.unwrap();
let channel_id = get_channel_for_author(&msg.author.id, &guild)?;
handler = Some(join_channel(ctx, channel_id, guild.id).await);
}
let handler = handler.ok_or(CommandError::from("Not in a voice channel"))?;
let mut songs = get_songs_for_url(&ctx, msg, url).await?;
let queue = get_queue_for_guild(ctx, &guild.id).await?;
let play_first = {
let mut queue_lock = queue.lock().await;
songs.reverse();
for song in songs {
queue_lock.add_next(song);
}
queue_lock.current().is_none()
};
if play_first {
play_next_in_queue(&ctx.http, &msg.channel_id, &queue, &handler).await;
}
Ok(())
}

@ -1,10 +1,12 @@
use std::cmp::min;
use serenity::client::Context;
use serenity::framework::standard::{CommandError, CommandResult};
use serenity::framework::standard::CommandResult;
use serenity::framework::standard::macros::command;
use serenity::model::channel::Message;
use crate::commands::music::get_queue_for_guild;
#[command]
#[only_in(guilds)]
#[description("Shows the song queue")]
@ -13,20 +15,12 @@ use serenity::model::channel::Message;
async fn queue(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let handler_lock = manager
.get(guild.id)
.ok_or(CommandError::from("Not in a voice channel"))?;
let handler = handler_lock.lock().await;
let songs: Vec<(usize, String)> = handler
.queue()
.current_queue()
let queue = get_queue_for_guild(ctx, &guild.id).await?;
let queue_lock = queue.lock().await;
let songs: Vec<(usize, String)> = queue_lock
.entries()
.into_iter()
.map(|t| t.metadata().title.clone().unwrap())
.map(|s| s.title.clone())
.enumerate()
.collect();

@ -1,42 +1,28 @@
use std::collections::VecDeque;
use rand::Rng;
use serenity::client::Context;
use serenity::framework::standard::{CommandError, CommandResult};
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
use crate::commands::music::get_queue_for_guild;
#[command]
#[only_in(guilds)]
#[description("Shuffles the queue")]
#[usage("shuffle")]
#[aliases("sh")]
#[allowed_roles("DJ")]
async fn shuffle(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let queue = get_queue_for_guild(ctx, &guild.id).await?;
{
let mut queue_lock = queue.lock().await;
queue_lock.shuffle();
}
let handler_lock = manager
.get(guild.id)
.ok_or(CommandError::from("Not in a voice channel"))?;
let handler = handler_lock.lock().await;
handler.queue().modify_queue(shuffle_vec_deque);
msg.channel_id
.say(ctx, "The queue has been shuffled")
.await?;
Ok(())
}
/// Fisher-Yates shuffle for VecDeque
fn shuffle_vec_deque<T>(deque: &mut VecDeque<T>) {
let mut rng = rand::thread_rng();
let mut i = deque.len();
while i >= 2 {
i -= 1;
deque.swap(i, rng.gen_range(0..i + 1))
}
}

@ -1,30 +1,26 @@
use serenity::client::Context;
use serenity::framework::standard::{CommandError, CommandResult};
use serenity::framework::standard::CommandResult;
use serenity::framework::standard::macros::command;
use serenity::model::channel::Message;
use crate::commands::music::get_queue_for_guild;
#[command]
#[only_in(guilds)]
#[description("Skips to the next song")]
#[usage("skip")]
#[aliases("next")]
#[allowed_roles("DJ")]
async fn skip(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let handler_lock = manager
.get(guild.id)
.ok_or(CommandError::from("Not in a voice channel"))?;
let handler = handler_lock.lock().await;
let queue = get_queue_for_guild(ctx, &guild.id).await?;
let queue_lock = queue.lock().await;
if let Some(current) = handler.queue().current() {
if let Some(current) = queue_lock.current() {
current.stop()?;
}
handler.queue().skip()?;
msg.channel_id.say(ctx, "Skipped to the next song").await?;
Ok(())

@ -1,33 +0,0 @@
use std::sync::Arc;
use serenity::client::Context;
use serenity::model::guild::Guild;
use serenity::model::id::{ChannelId, GuildId, UserId};
use songbird::Call;
use tokio::sync::Mutex;
use crate::utils::error::{BotError, BotResult};
/// Joins a voice channel
pub(crate) async fn join_channel(
ctx: &Context,
channel_id: ChannelId,
guild_id: GuildId,
) -> Arc<Mutex<Call>> {
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let (handler, _) = manager.join(guild_id, channel_id).await;
handler
}
/// Returns the voice channel the author is in
pub(crate) fn get_channel_for_author(author_id: &UserId, guild: &Guild) -> BotResult<ChannelId> {
guild
.voice_states
.get(author_id)
.and_then(|voice_state| voice_state.channel_id)
.ok_or(BotError::from("Not in a voice channel."))
}

@ -0,0 +1,60 @@
use serenity::client::Context;
use serenity::framework::standard::{Args, CommandResult};
use serenity::framework::standard::macros::command;
use serenity::model::channel::Message;
use crate::database::get_database_from_context;
use crate::database::guild::GUILD_SETTINGS;
#[command]
#[only_in(guilds)]
#[description("Get a guild setting")]
#[usage("get (<setting>)")]
#[example("get music.autoshuffle")]
#[min_args(0)]
#[max_args(1)]
#[required_permissions("MANAGE_GUILD")]
async fn get(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let database = get_database_from_context(ctx).await;
let guild = msg.guild(&ctx.cache).await.unwrap();
if let Some(key) = args.single::<String>().ok() {
let database_lock = database.lock().await;
let setting = database_lock.get_guild_setting::<String>(&guild.id, &key);
match setting {
Ok(value) => {
msg.channel_id
.say(ctx, format!("`{}` is set to to `{}`", key, value))
.await?;
}
Err(e) => {
eprintln!("Failed to get setting: {:?}", e);
msg.channel_id
.say(ctx, format!("`{}` is not set", key))
.await?;
}
}
} else {
for key in GUILD_SETTINGS {
let mut kv_pairs = Vec::new();
{
let database_lock = database.lock().await;
match database_lock.get_guild_setting::<String>(&guild.id, &key) {
Ok(value) => kv_pairs.push(format!("`{}` = `{}`", key, value)),
Err(e) => {
eprintln!("Failed to get setting: {:?}", e);
kv_pairs.push(format!("`{}` not set", key))
}
}
}
msg.channel_id
.send_message(ctx, |m| {
m.embed(|e| e.title("Guild Settings").description(kv_pairs.join("\n")))
})
.await?;
}
}
Ok(())
}

@ -0,0 +1,12 @@
use serenity::framework::standard::macros::group;
use get::GET_COMMAND;
use set::SET_COMMAND;
mod get;
mod set;
#[group]
#[commands(set, get)]
#[prefix("settings")]
pub struct Settings;

@ -0,0 +1,28 @@
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
use crate::database::get_database_from_context;
#[command]
#[only_in(guilds)]
#[description("Set a guild setting")]
#[usage("set <setting> <value>")]
#[example("set music.autoshuffle true")]
#[min_args(2)]
#[max_args(2)]
#[required_permissions("MANAGE_GUILD")]
async fn set(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let key = args.single::<String>().unwrap();
let value = args.single::<String>().unwrap();
let database = get_database_from_context(ctx).await;
let database_lock = database.lock().await;
let guild = msg.guild(&ctx.cache).await.unwrap();
database_lock.set_guild_setting(&guild.id, &key, value.clone())?;
msg.channel_id
.say(ctx, format!("Set `{}` to `{}`", key, value))
.await?;
Ok(())
}

@ -7,7 +7,10 @@ pub struct Guild {
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct GuildSettings {
guild_id: i32,
key: String,
value: String,
pub guild_id: String,
pub setting_key: String,
pub setting_value: String,
}
pub static SETTING_AUTOSHUFFLE: &str = "music.autoshuffle";
pub static GUILD_SETTINGS: &[&str] = &[SETTING_AUTOSHUFFLE];

@ -1,7 +1,15 @@
use rusqlite::{Connection, NO_PARAMS};
use std::str::FromStr;
use std::sync::Arc;
use rusqlite::{Connection, NO_PARAMS, params};
use serenity::client::Context;
use serenity::model::id::GuildId;
use tokio::sync::Mutex;
use crate::database::guild::GuildSettings;
use crate::database::scripts::{CREATE_SCRIPT, UPDATE_SCRIPT};
use crate::utils::error::BotResult;
use crate::utils::error::{BotError, BotResult};
use crate::utils::store::Store;
pub mod guild;
pub mod scripts;
@ -23,6 +31,44 @@ impl Database {
Ok(())
}
/// Returns a guild setting
pub fn get_guild_setting<T>(&self, guild_id: &GuildId, key: &str) -> BotResult<T>
where
T: Clone + FromStr,
{
self.connection
.query_row(
"SELECT guild_id, setting_key, setting_value FROM guild_settings WHERE guild_id = ?1 AND setting_key = ?2",
params![guild_id.to_string(), key],
|r| Ok(serde_rusqlite::from_row::<GuildSettings>(r).unwrap()),
)
.map_err(BotError::from)
.and_then(|s| {
s.setting_value
.parse::<T>()
.map_err(|_| BotError::from("Failed to parse Setting"))
})
}
/// Sets a guild setting and overrides it if it already exists
pub fn set_guild_setting<T>(&self, guild_id: &GuildId, key: &str, value: T) -> BotResult<()>
where
T: ToString + FromStr + Clone,
{
if self.get_guild_setting::<T>(guild_id, key).is_ok() {
self.connection.execute(
"DELETE FROM guild_settings WHERE guild_id = ?1 AND setting_key = ?2",
params![guild_id.to_string(), key],
)?;
}
self.connection.execute(
"INSERT INTO guild_settings (guild_id, setting_key, setting_value) VALUES (?1, ?2, ?3)",
params![guild_id.to_string(), key, value.to_string()],
)?;
Ok(())
}
}
pub fn get_database() -> BotResult<Database> {
@ -33,3 +79,11 @@ pub fn get_database() -> BotResult<Database> {
Ok(database)
}
/// Returns a reference to a guilds music queue
pub(crate) async fn get_database_from_context(ctx: &Context) -> Arc<Mutex<Database>> {
let data = ctx.data.read().await;
let store = data.get::<Store>().unwrap();
Arc::clone(&store.database)
}

@ -1,26 +1,6 @@
CREATE TABLE IF NOT EXISTS guilds
(
guild_id INTEGER PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS guild_settings
(
guild_id
INTEGER
NOT
NULL,
setting_key
TEXT
NOT
NULL,
setting_value
TEXT,
FOREIGN
KEY
(
guild_id
) REFERENCES guilds
(
guild_id
)
);
guild_id TEXT NOT NULL,
setting_key TEXT NOT NULL,
setting_value TEXT NOT NULL
);

@ -1,2 +1,2 @@
SELECT NULL
FROM guilds;
PRAGMA foreign_keys = false;
DROP TABLE IF EXISTS guilds;

@ -1 +1 @@
pub(crate) mod ytdl;
pub(crate) mod music;

@ -0,0 +1,38 @@
use std::io::Read;
use std::process::{Command, Stdio};
use crate::providers::music::responses::{PlaylistEntry, VideoInformation};
use crate::utils::error::BotResult;
pub(crate) mod queue;
pub(crate) mod responses;
/// Returns a list of youtube videos for a given url
pub(crate) fn get_videos_for_playlist(url: &str) -> BotResult<Vec<PlaylistEntry>> {
let ytdl = Command::new("youtube-dl")
.args(&["--no-warnings", "--flat-playlist", "--dump-json", "-i", url])
.stdout(Stdio::piped())
.spawn()?;
let mut output = String::new();
ytdl.stdout.unwrap().read_to_string(&mut output)?;
let videos = output
.lines()
.filter_map(|l| serde_json::from_str::<PlaylistEntry>(l).ok())
.collect();
Ok(videos)
}
/// Returns information for a single video by using youtube-dl
pub(crate) fn get_video_information(url: &str) -> BotResult<VideoInformation> {
let ytdl = Command::new("youtube-dl")
.args(&["--no-warnings", "--dump-json", "-i", url])
.stdout(Stdio::piped())
.spawn()?;
let information = serde_json::from_reader(ytdl.stdout.unwrap())?;
Ok(information)
}

@ -0,0 +1,96 @@
use std::collections::VecDeque;
use songbird::tracks::TrackHandle;
use crate::providers::music::responses::{PlaylistEntry, VideoInformation};
use crate::utils::shuffle_vec_deque;
#[derive(Clone, Debug)]
pub struct MusicQueue {
inner: VecDeque<Song>,
current: Option<TrackHandle>,
}
impl MusicQueue {
pub fn new() -> Self {
Self {
inner: VecDeque::new(),
current: None,
}
}
/// Adds a song to the queue
pub fn add(&mut self, song: Song) {
self.inner.push_back(song);
}
/// Adds a song to be played next in the queue
pub fn add_next(&mut self, song: Song) {
self.inner.push_front(song);
}
/// Shuffles the queue
pub fn shuffle(&mut self) {
shuffle_vec_deque(&mut self.inner)
}
/// Returns a reference to the inner deque
pub fn entries(&self) -> &VecDeque<Song> {
&self.inner
}
/// Returns the next song from the queue
pub fn next(&mut self) -> Option<Song> {
self.inner.pop_front()
}
/// Sets the currently playing song
pub fn set_current(&mut self, handle: TrackHandle) {
self.current = Some(handle)
}
/// Clears the currently playing song
pub fn clear_current(&mut self) {
self.current = None;
}
/// Returns the reference to the currently playing song
pub fn current(&self) -> &Option<TrackHandle> {
&self.current
}
/// Clears the queue
pub fn clear(&mut self) {
self.inner.clear();
}
}
#[derive(Clone, Debug)]
pub struct Song {
pub url: String,
pub title: String,
pub author: String,
pub thumbnail: Option<String>,
}
impl From<VideoInformation> for Song {
fn from(info: VideoInformation) -> Self {
Self {
url: info.webpage_url,
title: info.title,
author: info.uploader,
thumbnail: info.thumbnail,
}
}
}
impl From<PlaylistEntry> for Song {
fn from(entry: PlaylistEntry) -> Self {
Self {
url: format!("https://www.youtube.com/watch?v={}", entry.url),
title: entry.title,
author: entry.uploader,
thumbnail: None,
}
}
}

@ -0,0 +1,19 @@
use serde_derive::Deserialize;
#[derive(Deserialize, Clone, Debug)]
pub(crate) struct PlaylistEntry {
ie_key: String,
id: String,
pub url: String,
pub title: String,
pub uploader: String,
}
#[derive(Deserialize, Clone, Debug)]
pub(crate) struct VideoInformation {
id: String,
pub title: String,
pub thumbnail: Option<String>,
pub webpage_url: String,
pub uploader: String,
}

@ -1,32 +0,0 @@
use std::io::Read;
use std::process::{Command, Stdio};
use crate::providers::ytdl::playlist_entry::PlaylistEntry;
use crate::utils::error::BotResult;
mod playlist_entry;
/// Returns a list of youtube videos for a given url
pub(crate) fn get_videos_for_url(url: &str) -> BotResult<Vec<PlaylistEntry>> {
let ytdl = Command::new("youtube-dl")
.args(&[
"-f",
"--no-warnings",
"--flat-playlist",
"--dump-json",
"-i",
url,
])
.stdout(Stdio::piped())
.spawn()?;
let mut output = String::new();
ytdl.stdout.unwrap().read_to_string(&mut output)?;
let videos = output
.lines()
.map(|l| serde_json::from_str::<PlaylistEntry>(l).unwrap())
.collect();
Ok(videos)
}

@ -1,9 +0,0 @@
use serde_derive::Deserialize;
#[derive(Deserialize, Clone, Debug)]
pub(crate) struct PlaylistEntry {
ie_key: String,
id: String,
pub url: String,
pub title: String,
}

@ -19,6 +19,9 @@ pub enum BotError {
#[error("IO Error: {0}")]
IOError(#[from] std::io::Error),
#[error("JSON Error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("{0}")]
Msg(String),
}

@ -1,2 +1,16 @@
use std::collections::VecDeque;
use rand::Rng;
pub mod error;
pub mod store;
/// Fisher-Yates shuffle for VecDeque
pub fn shuffle_vec_deque<T>(deque: &mut VecDeque<T>) {
let mut rng = rand::thread_rng();
let mut i = deque.len();
while i >= 2 {
i -= 1;
deque.swap(i, rng.gen_range(0..i + 1))
}
}

@ -1,15 +1,19 @@
use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::Mutex;
use serenity::model::id::GuildId;
use serenity::prelude::TypeMapKey;
use tokio::sync::Mutex;
use crate::database::Database;
use crate::providers::music::queue::MusicQueue;
pub struct Store;
pub struct StoreData {
pub database: Arc<Mutex<Database>>,
pub minecraft_data_api: minecraft_data_rs::api::Api,
pub music_queues: HashMap<GuildId, Arc<Mutex<MusicQueue>>>,
}
impl StoreData {
@ -19,6 +23,7 @@ impl StoreData {
minecraft_data_api: minecraft_data_rs::api::Api::new(
minecraft_data_rs::api::versions::latest_stable().unwrap(),
),
music_queues: HashMap::new(),
}
}
}

Loading…
Cancel
Save