feat: bot now plays songs, started to work on queue system

This commit is contained in:
moonleay 2024-03-08 01:18:30 +01:00
parent edc22a91f2
commit a16d8a6b60
Signed by: moonleay
GPG key ID: 82667543CCD715FB
12 changed files with 289 additions and 130 deletions

8
Cargo.lock generated
View file

@ -1338,6 +1338,12 @@ dependencies = [
"unicase",
]
[[package]]
name = "queues"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1475abae4f8ad4998590fe3acfe20104f0a5d48fc420c817cd2c09c3f56151f0"
[[package]]
name = "quote"
version = "1.0.35"
@ -1561,6 +1567,8 @@ dependencies = [
"chrono",
"confy",
"futures",
"once_cell",
"queues",
"reqwest",
"serde",
"serde_json",

View file

@ -21,3 +21,5 @@ tracing = "0.1.40"
tracing-subscriber = "0.3.18"
tracing-futures = "0.2.5"
futures = "0.3.1"
queues = "1"
once_cell = "1"

View file

@ -7,9 +7,9 @@ pub async fn run(_ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
let username = command.user.name.as_str();
Embed::create(
username,
"",
"Botendo v7\ndeveloped by [moonleay](https://moonleay.net)\n\nCheck out the repository: https://git.moonleay.net/DiscordBots/Rustendo",
"Botendo v7",
"developed by [moonleay](https://moonleay.net)\n\nCheck out the repository: https://git.moonleay.net/DiscordBots/Rustendo",
)
}

View file

@ -1,15 +1,15 @@
use serenity::all::{CommandDataOptionValue, CommandInteraction, Context};
use serenity::builder::{CreateCommand, CreateCommandOption, CreateEmbed};
use serenity::model::application::CommandOptionType;
use crate::music::music_manager;
use crate::util::embed::Embed;
use crate::util::user_util::{self, get_vc_id};
pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
let username = command.user.name.as_str();
let options = &command.data.options;
let query = command.data.options.first().and_then(|option| {
let query = options.first().and_then(|option| {
if let CommandDataOptionValue::String(query) = &option.value {
Some(query)
} else {
@ -28,40 +28,7 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
}
};
println!("Guild ID: {:?}", guild_id);
let connect_to = get_vc_id(ctx, &guild_id, &command.user.id).await.expect("Cannot get channel id");
let manager = &songbird::get(ctx)
.await
.expect("Cannot get Songbird.")
.clone();
let self_channel = user_util::get_self_vc_id(ctx, &guild_id).await;
if !user_util::is_self_connected_to_vc(ctx, &guild_id).await {
// self is connected to vc, check if user is in same vc
if self_channel.is_none() {
// Connect to VC
manager
.join(*guild_id, connect_to)
.await
.expect("Cannot connect>...");
}
}
let self_channel = self_channel.expect("Cannot get self channel");
// Check if user is in the same VC as the bot
if self_channel != connect_to {
return Embed::create(
username,
"You are not in my VC.",
"Connect to my VC to control the music.",
);
}
Embed::create(username, "Searching...", format!("Looking for {:?}", query))
music_manager::attempt_to_queue_song(&ctx, &guild_id, &command.user.id, &command.user.name, query.unwrap()).await
}
pub fn register() -> CreateCommand {

View file

@ -1,8 +1,7 @@
use serenity::all::{CommandInteraction, Context};
use serenity::builder::{CreateCommand, CreateEmbed};
use crate::music::music_manager;
use crate::util::embed::Embed;
use crate::util::user_util;
pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
let username = command.user.name.as_str();
@ -14,38 +13,7 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
}
};
if !user_util::is_self_connected_to_vc_cached(ctx, guild_id) {
// Bot is not connectd to vc; no need to dc
return Embed::create(
username,
"Bot is not connected",
"And therefore I cannot stop playing.",
);
}
let manager = songbird::get(ctx)
.await
.expect("Cannot get Songbird")
.clone();
let has_handler = manager.get(*guild_id).is_some();
if has_handler {
if let Err(e) = manager.remove(*guild_id).await {
return Embed::create(username, "There was an error", format!("Failed: {:?}", e));
}
return Embed::create(
username,
"I stopped and left\nJust like your girlfriend.",
"",
);
}
Embed::create(
username,
"Bot is not connected",
"And therefore I cannot stop playing.\nSomething happend, which shouldn't have.",
)
music_manager::attempt_to_stop(&ctx, &guild_id, &command.user.id, &command.user.name).await
}
pub fn register() -> CreateCommand {

View file

View file

@ -1,15 +1,15 @@
mod commands;
mod handler;
mod util;
mod music;
use serenity::all::{CommandInteraction, OnlineStatus};
use serenity::all::{CommandInteraction, CreateInteractionResponseFollowup, OnlineStatus, VoiceState};
use serenity::async_trait;
use serenity::builder::{CreateEmbed, CreateInteractionResponse, CreateInteractionResponseMessage};
use serenity::builder::{CreateEmbed};
use serenity::gateway::ActivityData;
use serenity::model::application::{Command, Interaction};
use serenity::model::gateway::Ready;
use serenity::prelude::*;
use util::{config, embed::Embed};
use util::{config, embed::Embed, user_util};
// This trait adds the `register_songbird` and `register_songbird_with` methods
// to the client builder below, making it easy to install this voice client.
@ -30,6 +30,8 @@ struct Handler;
impl EventHandler for Handler {
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
if let Interaction::Command(command) = interaction {
let _ = &command.defer(&ctx.http()).await.expect("Cannot defer");
let content = Some(match command.data.name.as_str() {
"info" => commands::info::run(&ctx, &command).await,
"play" => commands::play::run(&ctx, &command).await,
@ -38,10 +40,9 @@ impl EventHandler for Handler {
});
if let Some(embed) = content {
let data = CreateInteractionResponseMessage::new().embed(embed);
let builder = CreateInteractionResponse::Message(data);
if let Err(why) = command.create_response(&ctx.http, builder).await {
println!("Cannot respond to slash command: {why}");
let followup = CreateInteractionResponseFollowup::new().embed(embed);
if let Err(why) = command.create_followup(&ctx.http, followup).await {
println!("Cannot followup to slash command: {why}")
}
}
}
@ -53,8 +54,24 @@ impl EventHandler for Handler {
let _command = Command::create_global_command(&ctx.http, commands::info::register()).await;
let _command = Command::create_global_command(&ctx.http, commands::stop::register()).await;
let _command = Command::create_global_command(&ctx.http, commands::play::register()).await;
println!("Commands are registered and Rustendo is ready for Freddy.");
}
println!("Created all public / commands");
async fn voice_state_update(&self, ctx: Context, old: Option<VoiceState>, new: VoiceState) {
if !new.channel_id.is_none() {
return; // User did not leave, ignore
}
if let Some(old) = old {
if !user_util::is_self_connected_to_vc(&ctx, &old.guild_id.unwrap()).await {
return; // Bot is not connected to a VC, ignore
}
if user_util::get_amount_of_members_in_vc(&ctx, &old.guild_id.unwrap(), &old.channel_id.unwrap()).await < 2 {
let manager = songbird::get(&ctx).await.expect("Cannot get Songbird");
if let Err(e) = manager.remove(old.guild_id.unwrap()).await {
println!("Failed to remove handler: {:?}", e);
}
}
} // else: new user joined, ignore
}
}

2
src/music/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod music_manager;
pub mod music_events;

42
src/music/music_events.rs Normal file
View file

@ -0,0 +1,42 @@
use std::sync::Arc;
use queues::IsQueue;
use serenity::all::{ChannelId, GuildId, Http};
use serenity::async_trait;
use songbird::{Event, EventContext, EventHandler};
use crate::music::music_manager;
pub struct TrackEndNotifier {
pub guild_id: GuildId,
pub channel_id: ChannelId,
pub http: Arc<Http>,
pub cmdctx: Arc<serenity::client::Context>,
}
#[async_trait]
impl EventHandler for TrackEndNotifier {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
unsafe { // TODO: Does this need to be unsafe?
if let EventContext::Track(track_list) = ctx {
println!("The track ended!");
let queue = music_manager::get_queue(&self.guild_id);
if queue.size() == 0 {
let stopped = match music_manager::stop(&self.cmdctx, &self.guild_id).await {
Ok(stopped) => stopped,
Err(e) => {
println!("Cannot stop: {:?}", e);
return None;
}
};
if stopped {
println!("Stopped playing successfully.");
} else {
println!("Failed to stop playing.");
}
return None;
}
}
}
None
}
}

173
src/music/music_manager.rs Normal file
View file

@ -0,0 +1,173 @@
use std::collections::{HashMap};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use once_cell::sync::Lazy;
use queues::Queue;
use serenity::all::{Context, CreateEmbed, GuildId, UserId};
use songbird::{Event, TrackEvent};
use songbird::error::JoinError;
use songbird::input::{Compose, YoutubeDl};
use crate::HttpKey;
use crate::music::music_events;
use crate::util::embed::Embed;
use crate::util::user_util;
use crate::util::user_util::get_vc_id;
/// pub static mut MUSIC_QUEUE: HashMap<GuildId, Queue<String>> = HashMap::new();
pub static mut MUSIC_QUEUE: Lazy<Mutex<HashMap<GuildId, Queue<String>>>> = Lazy::new(|| {
Mutex::new(HashMap::new())
}); // TODO: This does not work and this is not the way to do it. This is a placeholder for now.
pub unsafe fn get_queue(guild_id: &GuildId) -> &Queue<String> {
MUSIC_QUEUE.lock().unwrap().entry(*guild_id).or_insert_with(Queue::new)
}
pub async fn attempt_to_queue_song(ctx: &Context, guild_id: &GuildId, user_id: &UserId, username: &str, query: &str) -> CreateEmbed {
if !user_util::is_user_connected_to_vc(ctx, guild_id, user_id).await {
return Embed::create(username, "You are not connected to a VC", "Connect to my VC to control the music.");
}
let connect_to = match get_vc_id(ctx, &guild_id, &user_id).await {
Some(channel_id) => channel_id,
None => {
return Embed::create(username, "Error", "Cannot get channel id");
}
};
let manager = &songbird::get(ctx)
.await
.expect("Cannot get Songbird.")
.clone();
let self_channel = user_util::get_self_vc_id(ctx, &guild_id).await;
if !user_util::is_self_connected_to_vc(ctx, &guild_id).await {
// self is connected to vc, check if user is in same vc
if self_channel.is_none() { // TODO This could maybe be removed?
// Connect to VC
manager
.join(*guild_id, connect_to)
.await
.expect("Cannot connect>...");
}
} else {
let self_channel = self_channel.expect("Cannot get self channel");
// Check if user is in the same VC as the bot
if self_channel != connect_to {
return Embed::create(
username,
"You are not in my VC.",
"Connect to my VC to control the music.",
);
}
}
// Get query
let do_search = !query.starts_with("http");
let http_client = {
let data = ctx.data.read().await;
data.get::<HttpKey>()
.cloned()
.expect("Guaranteed to exist in the typemap.")
};
// Create source
let mut src = if do_search {
YoutubeDl::new_search(http_client, query.to_string())
} else {
YoutubeDl::new(http_client, query.to_string())
};
let handler_lock = match manager.get(*guild_id) {
Some(handler) => handler,
None => {
return Embed::create(username, "Error", "Cannot get handler");
},
};
// Start playing
let mut handler = handler_lock.lock().await;
let track_handle = handler.play_input(src.clone().into()); // TODO: Add event handlers
handler.add_global_event(
Event::Track(TrackEvent::End),
music_events::TrackEndNotifier {
guild_id: *guild_id,
channel_id: connect_to,
http: Arc::clone(&ctx.http),
cmdctx: Arc::new(ctx.clone()),
},
);
// Get metadata
let metadata = src.aux_metadata().await.expect("Cannot get metadata");
let title = metadata.title.unwrap_or("Unknown title".to_string());
let author = metadata.artist.unwrap_or("Unknown artist".to_string());
let duration = metadata.duration.unwrap_or(Duration::from_millis(0));
let thumbnail = metadata.thumbnail.unwrap_or("https://http.cat/images/404.jpg".to_string());
let link = metadata.source_url.unwrap_or("https://piped.moonleay.net/404".to_string());
Embed::create(username, "Added to queue", format!("{} by {} ({}min {}sec) was added to the queue.\n [[Link]({})]",
title, author, duration.as_secs() / 60, duration.as_secs() % 60, link))
.thumbnail(thumbnail)
}
pub async fn attempt_to_stop(ctx: &Context, guild_id: &GuildId, user_id: &UserId, username: &str) -> CreateEmbed {
if !user_util::is_self_connected_to_vc(ctx, guild_id).await {
// Bot is not connectd to vc; no need to dc
return Embed::create(
username,
"Bot is not connected",
"And therefore there is no need to do anything.",
);
}
let self_channel = user_util::get_self_vc_id(ctx, &guild_id).await.expect("Cannot get self channel");
let connect_to = get_vc_id(ctx, &guild_id, &user_id).await.expect("Cannot get channel id");
// Check if user is in the same VC as the bot
if self_channel != connect_to {
return Embed::create(
username,
"You are not in my VC.",
"Connect to my VC to control the music.",
);
}
let stopped = match stop(ctx, guild_id).await {
Ok(stopped) => stopped,
Err(e) => {
println!("Error while stopping: {:?}", e);
return Embed::create(username, "There was an error", "Tell moonleay to check the logs.".to_string());
}
};
if !stopped {
return Embed::create(username, "Can't stop, what ain't running.", "I am not connected. I cant stop doing something, when I'm not doing it".to_string());
} else {
return Embed::create(
username,
"I stopped and left",
"Just like your girlfriend.",
);
}
}
// Make the bot leave the voice channel. Returns Ok(true) if bot was connected, returns Ok(false) if bot was not connected. Returns Err if something went wrong.
pub async fn stop(ctx: &Context, guild_id: &GuildId) -> Result<bool, JoinError> {
let manager = songbird::get(ctx)
.await
.expect("Cannot get Songbird")
.clone();
let has_handler = manager.get(*guild_id).is_some();
if has_handler {
if let Err(e) = manager.remove(*guild_id).await {
return Err(e); // Failed to remove handler
}
return Ok(true) // Handler removed
}
Ok(false) // No handler, so it's already stopped
}

View file

@ -22,7 +22,7 @@ impl Embed {
.title(title)
.description(message)
.footer(CreateEmbedFooter::new(format!(
"> {} | {}",
"> {} - {}",
current_time, username
)))
}

View file

@ -1,21 +1,7 @@
use serenity::all::{CacheHttp, ChannelId, Context, Guild, GuildId, GuildRef, PartialGuild, UserId};
use serenity::Error;
/// Get a guild by id
pub fn get_guild_cached(ctx: &Context, guild_id: &GuildId) -> Option<Guild> {
let guild = match ctx.cache.guild(guild_id) {
Some(guild) => guild,
None => {
println!("Cannot get guild with id {:?}!", guild_id);
return None
}
};
println!("Got guild: {:?}", guild.name);
Some(guild.clone())
}
use std::collections::HashMap;
use serenity::all::{ChannelId, Context, Guild, GuildId, PartialGuild, UserId, VoiceState};
/// Request a guild by id, get it from cache
pub fn request_guild(ctx: &Context, guild_id: &GuildId) -> Guild {
let guild = match guild_id.to_guild_cached(&ctx.cache) {
Some(guild) => guild.clone(),
@ -39,26 +25,7 @@ pub async fn request_partial_guild(ctx: &Context, guild_id: &GuildId) -> Partial
guild
}
/// Get the current channel id of the bot
pub fn get_vc_id_cached(ctx: &Context, guild_id: &GuildId, user_id: &UserId) -> Option<ChannelId> {
let guild = match get_guild_cached(&ctx, guild_id){
Some(guild) => guild,
None => {
println!("Cannot get guild while getting channel id!");
return None
}
};
let channel_id = guild
.voice_states
.get(user_id)
.and_then(|voice_state| voice_state.channel_id)?;
println!("Got vc with id: {:?}", channel_id);
Some(channel_id)
}
/// Get the voice channel id of a user
pub async fn get_vc_id(ctx: &Context, guild_id: &GuildId, user_id: &UserId) -> Option<ChannelId> {
let guild = request_guild(&ctx, guild_id);
guild
@ -69,27 +36,40 @@ pub async fn get_vc_id(ctx: &Context, guild_id: &GuildId, user_id: &UserId) -> O
}
/// Check if the bot is connected to a voice channel
pub fn is_self_connected_to_vc_cached(ctx: &Context, guild_id: &GuildId) -> bool {
let channel_id = get_self_vc_id_cached(ctx, guild_id);
!channel_id.is_none()
}
pub async fn is_self_connected_to_vc(ctx: &Context, guild_id: &GuildId) -> bool {
let channel_id = get_self_vc_id(ctx, guild_id);
!channel_id.await.is_none()
}
/// Get the current channel id of the bot
pub fn get_self_vc_id_cached(ctx: &Context, guild_id: &GuildId) -> Option<ChannelId> {
let channel_id = get_vc_id_cached(ctx, guild_id, &ctx.cache.current_user().id)?;
/// Check if a user is connected to a voice channel
pub async fn is_user_connected_to_vc(ctx: &Context, guild_id: &GuildId, user_id: &UserId) -> bool {
let channel_id = get_vc_id(ctx, guild_id, user_id).await;
Some(channel_id)
!channel_id.is_none()
}
/// Get the voice channel id of the bot
pub async fn get_self_vc_id(ctx: &Context, guild_id: &GuildId) -> Option<ChannelId> {
let user_id = ctx.cache.current_user().id;
get_vc_id(ctx, guild_id, &user_id).await
}
/// Get all voice states of a guild
pub async fn get_voice_states(ctx: &Context, guild_id: &GuildId) -> HashMap<UserId, VoiceState>{
let guild = request_guild(ctx, guild_id);
let voice_states = guild.voice_states.clone();
voice_states
}
/// Get the amount of members in a voice channel
pub async fn get_amount_of_members_in_vc(ctx: &Context, guild_id: &GuildId, channel_id: &ChannelId,) -> usize {
let voice_states = get_voice_states(ctx, guild_id).await;
let amount = voice_states
.iter()
.filter(|(_, voice_state)| voice_state.channel_id == Some(*channel_id))
.count();
amount
}