Wip commit

This commit is contained in:
jude 2023-12-18 19:06:18 +00:00
parent cd5651c7f6
commit 07538f3277
13 changed files with 1272 additions and 800 deletions

1787
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,17 +7,17 @@ authors = ["jellywx <judesouthworth@pm.me>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
songbird = { version = "0.3", features = ["builtin-queue"] } songbird = { version = "0.4", features = ["builtin-queue"] }
poise = "0.5.5" poise = "0.6.1-rc1"
sqlx = { version = "0.5", default-features = false, features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "migrate"] } sqlx = { version = "0.5", default-features = false, features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "migrate"] }
tokio = { version = "1", features = ["fs", "process", "io-util"] } tokio = { version = "1", features = ["fs", "process", "io-util"] }
lazy_static = "1.4" lazy_static = "1.4"
reqwest = "0.11" reqwest = "0.11"
env_logger = "0.10" env_logger = "0.10"
regex = "1.4" regex = "1.10"
log = "0.4" log = "0.4"
serde_json = "1.0" serde_json = "1.0"
dashmap = "5.3" dashmap = "5.5"
serde = "1.0" serde = "1.0"
dotenv = "0.15.0" dotenv = "0.15.0"
prometheus = { version = "0.13.3", optional = true } prometheus = { version = "0.13.3", optional = true }
@ -26,9 +26,6 @@ axum = { version = "0.6.20", optional = true }
[features] [features]
metrics = ["dep:prometheus", "dep:axum"] metrics = ["dep:prometheus", "dep:axum"]
[patch."https://github.com/serenity-rs/serenity"]
serenity = { version = "0.11.6" }
[package.metadata.deb] [package.metadata.deb]
features = ["metrics"] features = ["metrics"]
depends = "$auto, ffmpeg" depends = "$auto, ffmpeg"

View File

@ -1,19 +1,23 @@
use poise::{
serenity_prelude::{CreateEmbed, CreateEmbedFooter},
CreateReply,
};
use crate::{consts::THEME_COLOR, Context, Error}; use crate::{consts::THEME_COLOR, Context, Error};
/// View bot commands /// View bot commands
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn help(ctx: Context<'_>) -> Result<(), Error> { pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(|m| { ctx.send(
m.ephemeral(true).embed(|e| { CreateReply::new().ephemeral(true).embed(
e.title("Help") CreateEmbed::new()
.title("Help")
.color(THEME_COLOR) .color(THEME_COLOR)
.footer(|f| { .footer(CreateEmbedFooter::new(concat!(
f.text(concat!( env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_NAME"), " ver ",
" ver ", env!("CARGO_PKG_VERSION")
env!("CARGO_PKG_VERSION") )))
))
})
.description( .description(
"__Info Commands__ "__Info Commands__
`/help` `/info` `/help` `/info`
@ -49,9 +53,9 @@ __Setting Commands__
__Advanced Commands__ __Advanced Commands__
`/soundboard` - Create a soundboard", `/soundboard` - Create a soundboard",
) ),
}) ),
}) )
.await?; .await?;
Ok(()) Ok(())
@ -62,13 +66,17 @@ __Advanced Commands__
pub async fn info(ctx: Context<'_>) -> Result<(), Error> { pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
let current_user = ctx.serenity_context().cache.current_user(); let current_user = ctx.serenity_context().cache.current_user();
ctx.send(|m| m.ephemeral(true) ctx.send(
.embed(|e| e CreateReply::new().ephemeral(true).embed(
.title("Info") CreateEmbed::new()
.color(THEME_COLOR) .title("Info")
.footer(|f| f .color(THEME_COLOR)
.text(concat!(env!("CARGO_PKG_NAME"), " ver ", env!("CARGO_PKG_VERSION")))) .footer(CreateEmbedFooter::new(concat!(
.description(format!("Invite me: https://discord.com/api/oauth2/authorize?client_id={}&permissions=3165184&scope=applications.commands%20bot env!("CARGO_PKG_NAME"),
" ver ",
env!("CARGO_PKG_VERSION")
)))
.description(format!("Invite me: https://discord.com/api/oauth2/authorize?client_id={}&permissions=3165184&scope=applications.commands%20bot
**Welcome to SoundFX!** **Welcome to SoundFX!**
Developer: <@203532103185465344> Developer: <@203532103185465344>
@ -76,7 +84,9 @@ Find me on https://discord.jellywx.com/ and on https://github.com/JellyWX :)
**An online dashboard is available!** Visit https://soundfx.jellywx.com/dashboard **An online dashboard is available!** Visit https://soundfx.jellywx.com/dashboard
There is a maximum sound limit per user. This can be removed by subscribing at **https://patreon.com/jellywx**", There is a maximum sound limit per user. This can be removed by subscribing at **https://patreon.com/jellywx**",
current_user.id.as_u64())))).await?; current_user.id.as_u64())))
)
.await?;
Ok(()) Ok(())
} }

View File

@ -63,11 +63,12 @@ pub async fn upload_new_sound(
// need to check if user is Patreon or not // need to check if user is Patreon or not
if count >= *MAX_SOUNDS { if count >= *MAX_SOUNDS {
let patreon_guild_member = let patreon_guild_member = GuildId::from(*PATREON_GUILD)
GuildId(*PATREON_GUILD).member(ctx, ctx.author().id).await; .member(ctx, ctx.author().id)
.await;
if let Ok(member) = patreon_guild_member { if let Ok(member) = patreon_guild_member {
permit_upload = member.roles.contains(&RoleId(*PATREON_ROLE)); permit_upload = member.roles.contains(&RoleId::from(*PATREON_ROLE));
} else { } else {
permit_upload = false; permit_upload = false;
} }

View File

@ -1,3 +1,5 @@
use poise::serenity_prelude::AutocompleteChoice;
use crate::{models::sound::SoundCtx, Context}; use crate::{models::sound::SoundCtx, Context};
pub mod favorite; pub mod favorite;
@ -8,34 +10,22 @@ pub mod search;
pub mod settings; pub mod settings;
pub mod stop; pub mod stop;
pub async fn autocomplete_sound( pub async fn autocomplete_sound(ctx: Context<'_>, partial: &str) -> Vec<AutocompleteChoice> {
ctx: Context<'_>,
partial: &str,
) -> Vec<poise::AutocompleteChoice<String>> {
ctx.data() ctx.data()
.autocomplete_user_sounds(&partial, ctx.author().id, ctx.guild_id().unwrap()) .autocomplete_user_sounds(&partial, ctx.author().id, ctx.guild_id().unwrap())
.await .await
.unwrap_or(vec![]) .unwrap_or(vec![])
.iter() .iter()
.map(|s| poise::AutocompleteChoice { .map(|s| AutocompleteChoice::new(s.name.clone(), s.id.to_string()))
name: s.name.clone(),
value: s.id.to_string(),
})
.collect() .collect()
} }
pub async fn autocomplete_favorite( pub async fn autocomplete_favorite(ctx: Context<'_>, partial: &str) -> Vec<AutocompleteChoice> {
ctx: Context<'_>,
partial: &str,
) -> Vec<poise::AutocompleteChoice<String>> {
ctx.data() ctx.data()
.autocomplete_favorite_sounds(&partial, ctx.author().id) .autocomplete_favorite_sounds(&partial, ctx.author().id)
.await .await
.unwrap_or(vec![]) .unwrap_or(vec![])
.iter() .iter()
.map(|s| poise::AutocompleteChoice { .map(|s| AutocompleteChoice::new(s.name.clone(), s.id.to_string()))
name: s.name.clone(),
value: s.id.to_string(),
})
.collect() .collect()
} }

View File

@ -1,9 +1,6 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use poise::serenity_prelude::{ use poise::serenity_prelude::{builder::CreateActionRow, ButtonStyle, GuildChannel, ReactionType};
builder::CreateActionRow, model::application::component::ButtonStyle, GuildChannel,
ReactionType,
};
#[cfg(feature = "metrics")] #[cfg(feature = "metrics")]
use crate::metrics::PLAY_COUNTER; use crate::metrics::PLAY_COUNTER;

View File

@ -1,10 +1,8 @@
use poise::{ use poise::{
serenity_prelude, serenity_prelude,
serenity_prelude::{ serenity_prelude::{
application::component::ButtonStyle, constants::MESSAGE_CODE_LIMIT, ButtonStyle, ComponentInteraction, CreateActionRow,
constants::MESSAGE_CODE_LIMIT, CreateEmbed, EditInteractionResponse, GuildId, UserId,
interaction::{message_component::MessageComponentInteraction, InteractionResponseType},
CreateActionRow, CreateEmbed, GuildId, UserId,
}, },
CreateReply, CreateReply,
}; };
@ -16,7 +14,7 @@ use crate::{
Context, Data, Error, Context, Data, Error,
}; };
fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> { fn format_search_results(search_results: Vec<Sound>) -> CreateReply {
let mut builder = CreateReply::default(); let mut builder = CreateReply::default();
let mut current_character_count = 0; let mut current_character_count = 0;
@ -213,7 +211,7 @@ impl SoundPager {
pub async fn handle_interaction( pub async fn handle_interaction(
ctx: &serenity_prelude::Context, ctx: &serenity_prelude::Context,
data: &Data, data: &Data,
interaction: &MessageComponentInteraction, interaction: &ComponentInteraction,
) -> Result<(), Error> { ) -> Result<(), Error> {
let user_id = interaction.user.id; let user_id = interaction.user.id;
let guild_id = interaction.guild_id.unwrap(); let guild_id = interaction.guild_id.unwrap();
@ -226,18 +224,11 @@ impl SoundPager {
ListContext::Guild => data.count_guild_sounds(guild_id).await?, ListContext::Guild => data.count_guild_sounds(guild_id).await?,
}; };
interaction let response = EditInteractionResponse::default()
.create_interaction_response(&ctx, |r| { .add_embed(pager.embed(&sounds, count))
r.kind(InteractionResponseType::UpdateMessage) .components(|c| c.add_action_row(pager.create_action_row(count / 25)));
.interaction_response_data(|d| {
d.ephemeral(true)
.add_embed(pager.embed(&sounds, count))
.components(|c| c.add_action_row(pager.create_action_row(count / 25)))
})
})
.await?;
Ok(()) interaction.Ok(())
} }
async fn reply(&self, ctx: Context<'_>) -> Result<(), Error> { async fn reply(&self, ctx: Context<'_>) -> Result<(), Error> {

View File

@ -1,9 +1,6 @@
use poise::serenity_prelude::{ use poise::serenity_prelude::{
model::{ model::channel::Channel, ActionRowComponent, Activity, Context, CreateActionRow, FullEvent,
application::interaction::{Interaction, InteractionResponseType}, Interaction,
channel::Channel,
},
ActionRowComponent, Activity, Context, CreateActionRow, CreateComponents,
}; };
#[cfg(feature = "metrics")] #[cfg(feature = "metrics")]
@ -19,16 +16,18 @@ use crate::{
Data, Error, Data, Error,
}; };
pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> { pub async fn listener(ctx: &Context, event: &FullEvent, data: &Data) -> Result<(), Error> {
match event { match event {
poise::Event::Ready { .. } => { FullEvent::Ready { .. } => {
ctx.set_activity(Activity::watching("for /play")).await; ctx.set_activity(Activity::watching("for /play")).await;
} }
poise::Event::VoiceStateUpdate { old, new, .. } => { FullEvent::VoiceStateUpdate { old, new, .. } => {
if let Some(past_state) = old { if let Some(past_state) = old {
if let (Some(guild_id), None) = (past_state.guild_id, new.channel_id) { if let (Some(guild_id), None) = (past_state.guild_id, new.channel_id) {
if let Some(channel_id) = past_state.channel_id { if let Some(channel_id) = past_state.channel_id {
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) { if let Some(Channel::Guild(channel)) =
channel_id.to_channel_cached(&ctx.cache)
{
if channel.members(&ctx).await.map(|m| m.len()).unwrap_or(0) <= 1 { if channel.members(&ctx).await.map(|m| m.len()).unwrap_or(0) <= 1 {
let songbird = songbird::get(ctx).await.unwrap(); let songbird = songbird::get(ctx).await.unwrap();
@ -93,7 +92,7 @@ pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> R
} }
} }
} }
poise::Event::InteractionCreate { interaction } => match interaction { FullEvent::InteractionCreate { interaction } => match interaction {
Interaction::MessageComponent(component) => { Interaction::MessageComponent(component) => {
if let Some(guild_id) = component.guild_id { if let Some(guild_id) = component.guild_id {
if let Ok(()) = SoundPager::handle_interaction(ctx, &data, component).await { if let Ok(()) = SoundPager::handle_interaction(ctx, &data, component).await {

View File

@ -13,13 +13,11 @@ mod utils;
use std::{env, path::Path, sync::Arc}; use std::{env, path::Path, sync::Arc};
use dashmap::DashMap; use dashmap::DashMap;
use poise::serenity_prelude::{ use poise::serenity_prelude::model::{
builder::CreateApplicationCommands, gateway::GatewayIntents,
model::{ id::{GuildId, UserId},
gateway::GatewayIntents,
id::{GuildId, UserId},
},
}; };
use serde_json::Value;
use songbird::SerenityInit; use songbird::SerenityInit;
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -40,29 +38,20 @@ type Context<'a> = poise::Context<'a, Data, Error>;
pub async fn register_application_commands( pub async fn register_application_commands(
ctx: &poise::serenity_prelude::Context, ctx: &poise::serenity_prelude::Context,
framework: &poise::Framework<Data, Error>, framework: &poise::Framework<Data, Error>,
guild_id: Option<GuildId>,
) -> Result<(), poise::serenity_prelude::Error> { ) -> Result<(), poise::serenity_prelude::Error> {
let mut commands_builder = CreateApplicationCommands::default(); let mut commands_builder = vec![];
let commands = &framework.options().commands; let commands = &framework.options().commands;
for command in commands { for command in commands {
if let Some(slash_command) = command.create_as_slash_command() { if let Some(slash_command) = command.create_as_slash_command() {
commands_builder.add_application_command(slash_command); commands_builder.push(slash_command.into());
} }
if let Some(context_menu_command) = command.create_as_context_menu_command() { if let Some(context_menu_command) = command.create_as_context_menu_command() {
commands_builder.add_application_command(context_menu_command); commands_builder.push(context_menu_command.into());
} }
} }
let commands_builder = poise::serenity_prelude::json::Value::Array(commands_builder.0); let commands_builder = Value::Array(commands_builder);
if let Some(guild_id) = guild_id { ctx.http.create_global_commands(&commands_builder).await?;
ctx.http
.create_guild_application_commands(guild_id.0, &commands_builder)
.await?;
} else {
ctx.http
.create_global_application_commands(&commands_builder)
.await?;
}
Ok(()) Ok(())
} }
@ -153,9 +142,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.token(discord_token) .token(discord_token)
.setup(move |ctx, _bot, framework| { .setup(move |ctx, _bot, framework| {
Box::pin(async move { Box::pin(async move {
register_application_commands(ctx, framework, None) register_application_commands(ctx, framework).await.unwrap();
.await
.unwrap();
Ok(Data { Ok(Data {
database, database,

View File

@ -78,12 +78,10 @@ impl GuildData {
let guild_data = sqlx::query_as_unchecked!( let guild_data = sqlx::query_as_unchecked!(
GuildData, GuildData,
" "SELECT id, prefix, volume, allow_greets, allowed_role
SELECT id, prefix, volume, allow_greets, allowed_role FROM servers
FROM servers WHERE id = ?",
WHERE id = ? guild_id
",
guild_id.as_u64()
) )
.fetch_one(db_pool) .fetch_one(db_pool)
.await; .await;
@ -104,17 +102,15 @@ SELECT id, prefix, volume, allow_greets, allowed_role
let guild_id = guild_id.into(); let guild_id = guild_id.into();
sqlx::query!( sqlx::query!(
" "INSERT INTO servers (id)
INSERT INTO servers (id) VALUES (?)",
VALUES (?) guild_id
",
guild_id.as_u64()
) )
.execute(db_pool) .execute(db_pool)
.await?; .await?;
Ok(GuildData { Ok(GuildData {
id: guild_id.as_u64().to_owned(), id: guild_id.get(),
prefix: String::from("?"), prefix: String::from("?"),
volume: 100, volume: 100,
allow_greets: AllowGreet::Enabled, allow_greets: AllowGreet::Enabled,

View File

@ -47,13 +47,12 @@ impl JoinSoundCtx for Data {
sqlx::query_as!( sqlx::query_as!(
JoinSound, JoinSound,
" "
SELECT join_sound_id SELECT join_sound_id
FROM join_sounds FROM join_sounds
WHERE user = ? WHERE user = ?
AND guild = ? AND guild = ?
ORDER BY guild IS NULL ORDER BY guild IS NULL",
", user_id,
user_id.as_u64(),
guild_id.map(|g| g.0) guild_id.map(|g| g.0)
) )
.fetch_one(&self.database) .fetch_one(&self.database)
@ -62,13 +61,12 @@ SELECT join_sound_id
sqlx::query_as!( sqlx::query_as!(
JoinSound, JoinSound,
" "
SELECT join_sound_id SELECT join_sound_id
FROM join_sounds FROM join_sounds
WHERE user = ? WHERE user = ?
AND (guild IS NULL OR guild = ?) AND (guild IS NULL OR guild = ?)
ORDER BY guild IS NULL ORDER BY guild IS NULL",
", user_id,
user_id.as_u64(),
guild_id.map(|g| g.0) guild_id.map(|g| g.0)
) )
.fetch_one(&self.database) .fetch_one(&self.database)

View File

@ -1,7 +1,7 @@
use std::{env, path::Path}; use std::{env, path::Path};
use poise::serenity_prelude::async_trait; use poise::serenity_prelude::async_trait;
use songbird::input::restartable::Restartable; use songbird::input::Input;
use sqlx::Executor; use sqlx::Executor;
use tokio::{fs::File, io::AsyncWriteExt, process::Command}; use tokio::{fs::File, io::AsyncWriteExt, process::Command};
@ -441,12 +441,10 @@ impl Sound {
pub async fn playable( pub async fn playable(
&self, &self,
db_pool: impl Executor<'_, Database = Database>, db_pool: impl Executor<'_, Database = Database>,
) -> Result<Restartable, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Input, Box<dyn std::error::Error + Send + Sync>> {
let path_name = self.store_sound_source(db_pool).await?; let path_name = self.store_sound_source(db_pool).await?;
Ok(Restartable::ffmpeg(path_name, false) Ok(Input::from(path_name))
.await
.expect("FFMPEG ERROR!"))
} }
pub async fn count_user_sounds<U: Into<u64>>( pub async fn count_user_sounds<U: Into<u64>>(

View File

@ -1,11 +1,14 @@
use std::sync::Arc; use std::sync::Arc;
use poise::serenity_prelude::model::{ use poise::serenity_prelude::{
channel::Channel, model::{
guild::Guild, channel::Channel,
id::{ChannelId, UserId}, guild::Guild,
id::{ChannelId, UserId},
},
GuildRef,
}; };
use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call}; use songbird::{error::JoinResult, tracks::TrackHandle, Call};
use sqlx::Executor; use sqlx::Executor;
use tokio::sync::{Mutex, MutexGuard}; use tokio::sync::{Mutex, MutexGuard};
@ -24,19 +27,18 @@ pub async fn play_audio(
db_pool: impl Executor<'_, Database = Database>, db_pool: impl Executor<'_, Database = Database>,
r#loop: bool, r#loop: bool,
) -> Result<TrackHandle, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<TrackHandle, Box<dyn std::error::Error + Send + Sync>> {
let (track, track_handler) = create_player(sound.playable(db_pool).await?.into()); let track = sound.playable(db_pool).await?;
let handle = call_handler.play_source(track);
let _ = track_handler.set_volume(volume as f32 / 100.0); handle.set_volume(volume as f32 / 100.0)?;
if r#loop { if r#loop {
let _ = track_handler.enable_loop(); handle.enable_loop()?;
} else { } else {
let _ = track_handler.disable_loop(); handle.disable_loop()?;
} }
call_handler.play(track); Ok(handle)
Ok(track_handler)
} }
pub async fn queue_audio( pub async fn queue_audio(
@ -46,11 +48,10 @@ pub async fn queue_audio(
db_pool: impl Executor<'_, Database = Database> + Copy, db_pool: impl Executor<'_, Database = Database> + Copy,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
for sound in sounds { for sound in sounds {
let (a, b) = create_player(sound.playable(db_pool).await?.into()); let track = sound.playable(db_pool).await?;
let handle = call_handler.enqueue_source(track);
let _ = b.set_volume(volume as f32 / 100.0); handle.set_volume(volume as f32 / 100.0)?;
call_handler.enqueue(a);
} }
Ok(()) Ok(())
@ -58,31 +59,28 @@ pub async fn queue_audio(
pub async fn join_channel( pub async fn join_channel(
ctx: &poise::serenity_prelude::Context, ctx: &poise::serenity_prelude::Context,
guild: Guild, guild: impl AsRef<Guild>,
channel_id: ChannelId, channel_id: ChannelId,
) -> (Arc<Mutex<Call>>, JoinResult<()>) { ) -> Arc<Mutex<Call>> {
let songbird = songbird::get(ctx).await.unwrap(); let songbird = songbird::get(ctx).await.unwrap();
let current_user = ctx.cache.current_user_id(); let current_user = ctx.cache.current_user_id();
let current_voice_state = guild let current_voice_state = guild
.as_ref()
.voice_states .voice_states
.get(&current_user) .get(&current_user)
.and_then(|voice_state| voice_state.channel_id); .and_then(|voice_state| voice_state.channel_id);
let (call, res) = if current_voice_state == Some(channel_id) { let call = if current_voice_state == Some(channel_id) {
let call_opt = songbird.get(guild.id); let call_opt = songbird.get(guild.as_ref().id);
if let Some(call) = call_opt { if let Some(call) = call_opt {
(call, Ok(())) Ok(call)
} else { } else {
let (call, res) = songbird.join(guild.id, channel_id).await; songbird.join(guild.as_ref().id, channel_id).await
(call, res)
} }
} else { } else {
let (call, res) = songbird.join(guild.id, channel_id).await; songbird.join(guild.as_ref().id, channel_id).await
(call, res)
}; };
{ {
@ -96,7 +94,7 @@ pub async fn join_channel(
.await; .await;
} }
(call, res) call
} }
pub async fn play_from_query( pub async fn play_from_query(
@ -129,8 +127,7 @@ pub async fn play_from_query(
match sound_res { match sound_res {
Some(sound) => { Some(sound) => {
{ {
let (call_handler, _) = let call_handler = join_channel(ctx, guild.clone(), user_channel).await;
join_channel(ctx, guild.clone(), user_channel).await;
let guild_data = data.guild_data(guild_id).await.unwrap(); let guild_data = data.guild_data(guild_id).await.unwrap();