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

View File

@ -1,19 +1,23 @@
use poise::{
serenity_prelude::{CreateEmbed, CreateEmbedFooter},
CreateReply,
};
use crate::{consts::THEME_COLOR, Context, Error};
/// View bot commands
#[poise::command(slash_command)]
pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(|m| {
m.ephemeral(true).embed(|e| {
e.title("Help")
ctx.send(
CreateReply::new().ephemeral(true).embed(
CreateEmbed::new()
.title("Help")
.color(THEME_COLOR)
.footer(|f| {
f.text(concat!(
.footer(CreateEmbedFooter::new(concat!(
env!("CARGO_PKG_NAME"),
" ver ",
env!("CARGO_PKG_VERSION")
))
})
)))
.description(
"__Info Commands__
`/help` `/info`
@ -49,9 +53,9 @@ __Setting Commands__
__Advanced Commands__
`/soundboard` - Create a soundboard",
),
),
)
})
})
.await?;
Ok(())
@ -62,12 +66,16 @@ __Advanced Commands__
pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
let current_user = ctx.serenity_context().cache.current_user();
ctx.send(|m| m.ephemeral(true)
.embed(|e| e
ctx.send(
CreateReply::new().ephemeral(true).embed(
CreateEmbed::new()
.title("Info")
.color(THEME_COLOR)
.footer(|f| f
.text(concat!(env!("CARGO_PKG_NAME"), " ver ", env!("CARGO_PKG_VERSION"))))
.footer(CreateEmbedFooter::new(concat!(
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!**
@ -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
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(())
}

View File

@ -63,11 +63,12 @@ pub async fn upload_new_sound(
// need to check if user is Patreon or not
if count >= *MAX_SOUNDS {
let patreon_guild_member =
GuildId(*PATREON_GUILD).member(ctx, ctx.author().id).await;
let patreon_guild_member = GuildId::from(*PATREON_GUILD)
.member(ctx, ctx.author().id)
.await;
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 {
permit_upload = false;
}

View File

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

View File

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

View File

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

View File

@ -1,9 +1,6 @@
use poise::serenity_prelude::{
model::{
application::interaction::{Interaction, InteractionResponseType},
channel::Channel,
},
ActionRowComponent, Activity, Context, CreateActionRow, CreateComponents,
model::channel::Channel, ActionRowComponent, Activity, Context, CreateActionRow, FullEvent,
Interaction,
};
#[cfg(feature = "metrics")]
@ -19,16 +16,18 @@ use crate::{
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 {
poise::Event::Ready { .. } => {
FullEvent::Ready { .. } => {
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(guild_id), None) = (past_state.guild_id, new.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 {
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) => {
if let Some(guild_id) = component.guild_id {
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 dashmap::DashMap;
use poise::serenity_prelude::{
builder::CreateApplicationCommands,
model::{
use poise::serenity_prelude::model::{
gateway::GatewayIntents,
id::{GuildId, UserId},
},
};
use serde_json::Value;
use songbird::SerenityInit;
use sqlx::{MySql, Pool};
use tokio::sync::RwLock;
@ -40,29 +38,20 @@ type Context<'a> = poise::Context<'a, Data, Error>;
pub async fn register_application_commands(
ctx: &poise::serenity_prelude::Context,
framework: &poise::Framework<Data, Error>,
guild_id: Option<GuildId>,
) -> Result<(), poise::serenity_prelude::Error> {
let mut commands_builder = CreateApplicationCommands::default();
let mut commands_builder = vec![];
let commands = &framework.options().commands;
for command in commands {
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() {
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_guild_application_commands(guild_id.0, &commands_builder)
.await?;
} else {
ctx.http
.create_global_application_commands(&commands_builder)
.await?;
}
ctx.http.create_global_commands(&commands_builder).await?;
Ok(())
}
@ -153,9 +142,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.token(discord_token)
.setup(move |ctx, _bot, framework| {
Box::pin(async move {
register_application_commands(ctx, framework, None)
.await
.unwrap();
register_application_commands(ctx, framework).await.unwrap();
Ok(Data {
database,

View File

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

View File

@ -51,9 +51,8 @@ SELECT join_sound_id
FROM join_sounds
WHERE user = ?
AND guild = ?
ORDER BY guild IS NULL
",
user_id.as_u64(),
ORDER BY guild IS NULL",
user_id,
guild_id.map(|g| g.0)
)
.fetch_one(&self.database)
@ -66,9 +65,8 @@ SELECT join_sound_id
FROM join_sounds
WHERE user = ?
AND (guild IS NULL OR guild = ?)
ORDER BY guild IS NULL
",
user_id.as_u64(),
ORDER BY guild IS NULL",
user_id,
guild_id.map(|g| g.0)
)
.fetch_one(&self.database)

View File

@ -1,7 +1,7 @@
use std::{env, path::Path};
use poise::serenity_prelude::async_trait;
use songbird::input::restartable::Restartable;
use songbird::input::Input;
use sqlx::Executor;
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
@ -441,12 +441,10 @@ impl Sound {
pub async fn playable(
&self,
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?;
Ok(Restartable::ffmpeg(path_name, false)
.await
.expect("FFMPEG ERROR!"))
Ok(Input::from(path_name))
}
pub async fn count_user_sounds<U: Into<u64>>(

View File

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