Wip commit

This commit is contained in:
jude 2023-12-18 19:06:18 +00:00
parent cd5651c7f6
commit e369b42131
14 changed files with 1432 additions and 948 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::default().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,12 +66,16 @@ __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::default().ephemeral(true).embed(
CreateEmbed::new()
.title("Info") .title("Info")
.color(THEME_COLOR) .color(THEME_COLOR)
.footer(|f| f .footer(CreateEmbedFooter::new(concat!(
.text(concat!(env!("CARGO_PKG_NAME"), " ver ", env!("CARGO_PKG_VERSION")))) 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 .description(format!("Invite me: https://discord.com/api/oauth2/authorize?client_id={}&permissions=3165184&scope=applications.commands%20bot
**Welcome to SoundFX!** **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 **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.get())))
)
.await?;
Ok(()) Ok(())
} }

View File

@ -1,4 +1,7 @@
use poise::serenity_prelude::{Attachment, GuildId, RoleId}; use poise::{
serenity_prelude::{Attachment, CreateAttachment, GuildId, RoleId},
CreateReply,
};
use tokio::fs::File; use tokio::fs::File;
#[cfg(feature = "metrics")] #[cfg(feature = "metrics")]
@ -63,11 +66,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;
} }
@ -120,8 +124,8 @@ pub async fn delete_sound(
let pool = ctx.data().database.clone(); let pool = ctx.data().database.clone();
let uid = ctx.author().id.0; let uid = ctx.author().id.get();
let gid = ctx.guild_id().unwrap().0; let gid = ctx.guild_id().unwrap().get();
let sound_vec = ctx.data().search_for_sound(&name, gid, uid, true).await?; let sound_vec = ctx.data().search_for_sound(&name, gid, uid, true).await?;
let sound_result = sound_vec.first(); let sound_result = sound_vec.first();
@ -173,8 +177,8 @@ pub async fn change_public(
) -> Result<(), Error> { ) -> Result<(), Error> {
let pool = ctx.data().database.clone(); let pool = ctx.data().database.clone();
let uid = ctx.author().id.0; let uid = ctx.author().id.get();
let gid = ctx.guild_id().unwrap().0; let gid = ctx.guild_id().unwrap().get();
let mut sound_vec = ctx.data().search_for_sound(&name, gid, uid, true).await?; let mut sound_vec = ctx.data().search_for_sound(&name, gid, uid, true).await?;
let sound_result = sound_vec.first_mut(); let sound_result = sound_vec.first_mut();
@ -228,7 +232,10 @@ pub async fn download_file(
let file = File::open(&source).await?; let file = File::open(&source).await?;
let name = format!("{}-{}.opus", sound.id, sound.name); let name = format!("{}-{}.opus", sound.id, sound.name);
ctx.send(|m| m.attachment((&file, name.as_str()).into())) ctx.send(
CreateReply::default()
.attachment(CreateAttachment::file(&file, name.as_str()).await?),
)
.await?; .await?;
} }

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,8 +1,10 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use poise::serenity_prelude::{ use poise::{
builder::CreateActionRow, model::application::component::ButtonStyle, GuildChannel, serenity_prelude::{
ReactionType, builder::CreateActionRow, ButtonStyle, CreateButton, GuildChannel, ReactionType,
},
CreateReply,
}; };
#[cfg(feature = "metrics")] #[cfg(feature = "metrics")]
@ -74,8 +76,7 @@ pub async fn play_random(
match channel_to_join { match channel_to_join {
Some(channel) => { Some(channel) => {
let (call_handler, _) = let call = join_channel(ctx.serenity_context(), &guild.clone(), channel).await?;
join_channel(ctx.serenity_context(), guild.clone(), channel).await;
let sounds = ctx.data().guild_sounds(guild.id, None).await?; let sounds = ctx.data().guild_sounds(guild.id, None).await?;
if sounds.len() == 0 { if sounds.len() == 0 {
@ -92,7 +93,7 @@ pub async fn play_random(
match sounds.get(ts.subsec_micros() as usize % sounds.len()) { match sounds.get(ts.subsec_micros() as usize % sounds.len()) {
Some(sound) => { Some(sound) => {
let guild_data = ctx.data().guild_data(guild.id).await.unwrap(); let guild_data = ctx.data().guild_data(guild.id).await.unwrap();
let mut lock = call_handler.lock().await; let mut lock = call.lock().await;
play_audio( play_audio(
sound, sound,
@ -218,8 +219,7 @@ pub async fn queue_play(
match channel_to_join { match channel_to_join {
Some(user_channel) => { Some(user_channel) => {
let (call_handler, _) = let call = join_channel(ctx.serenity_context(), guild, user_channel).await?;
join_channel(ctx.serenity_context(), guild.clone(), user_channel).await;
let guild_data = ctx let guild_data = ctx
.data() .data()
@ -227,8 +227,6 @@ pub async fn queue_play(
.await .await
.unwrap(); .unwrap();
let mut lock = call_handler.lock().await;
let query_terms = [ let query_terms = [
Some(sound_1), Some(sound_1),
Some(sound_2), Some(sound_2),
@ -270,6 +268,9 @@ pub async fn queue_play(
} }
} }
{
let mut lock = call.lock().await;
queue_audio( queue_audio(
&sounds, &sounds,
guild_data.read().await.volume, guild_data.read().await.volume,
@ -278,6 +279,7 @@ pub async fn queue_play(
) )
.await .await
.unwrap(); .unwrap();
}
ctx.say(format!("Queued {} sounds!", sounds.len())).await?; ctx.say(format!("Queued {} sounds!", sounds.len())).await?;
} }
@ -434,50 +436,49 @@ pub async fn soundboard(
} }
} }
ctx.send(|m| { let components = {
m.content("**Play a sound:**").components(|c| { let mut c = vec![];
for row in sounds.as_slice().chunks(5) { for row in sounds.as_slice().chunks(5) {
let mut action_row: CreateActionRow = Default::default(); let mut action_row = vec![];
for sound in row { for sound in row {
action_row.create_button(|b| { action_row.push(
b.style(ButtonStyle::Primary) CreateButton::new(sound.id.to_string())
.label(&sound.name) .style(ButtonStyle::Primary)
.custom_id(sound.id) .label(&sound.name),
}); );
} }
c.add_action_row(action_row); c.push(CreateActionRow::Buttons(action_row));
} }
c.create_action_row(|r| { c.push(CreateActionRow::Buttons(vec![
r.create_button(|b| { CreateButton::new("#stop")
b.label("Stop") .label("Stop")
.emoji(ReactionType::Unicode("".to_string())) .emoji(ReactionType::Unicode("".to_string()))
.style(ButtonStyle::Danger) .style(ButtonStyle::Danger),
.custom_id("#stop") CreateButton::new("#mode")
}) .label("Mode:")
.create_button(|b| {
b.label("Mode:")
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary)
.disabled(true) .disabled(true),
.custom_id("#mode") CreateButton::new("#instant")
}) .label("Instant")
.create_button(|b| {
b.label("Instant")
.emoji(ReactionType::Unicode("".to_string())) .emoji(ReactionType::Unicode("".to_string()))
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary)
.disabled(true) .disabled(true),
.custom_id("#instant") CreateButton::new("#loop")
}) .label("Loop")
.create_button(|b| {
b.label("Loop")
.emoji(ReactionType::Unicode("🔁".to_string())) .emoji(ReactionType::Unicode("🔁".to_string()))
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary),
.custom_id("#loop") ]));
})
}) c
}) };
})
ctx.send(
CreateReply::default()
.content("**Play a sound:**")
.components(components),
)
.await?; .await?;
Ok(()) Ok(())

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, CreateButton, 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;
@ -32,7 +30,7 @@ fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> {
current_character_count <= MESSAGE_CODE_LIMIT - title.len() current_character_count <= MESSAGE_CODE_LIMIT - title.len()
}); });
builder.embed(|e| e.title(title).fields(field_iter)); builder.embed(CreateEmbed::default().title(title).fields(field_iter));
builder builder
} }
@ -124,10 +122,8 @@ impl SoundPager {
} }
fn create_action_row(&self, max_page: u64) -> CreateActionRow { fn create_action_row(&self, max_page: u64) -> CreateActionRow {
let mut row = CreateActionRow::default(); let mut row = CreateActionRow::Buttons(vec![
CreateButton::new(
row.create_button(|b| {
b.custom_id(
serde_json::to_string(&SoundPager { serde_json::to_string(&SoundPager {
nonce: 0, nonce: 0,
page: 0, page: 0,
@ -137,10 +133,8 @@ impl SoundPager {
) )
.style(ButtonStyle::Primary) .style(ButtonStyle::Primary)
.label("") .label("")
.disabled(self.page == 0) .disabled(self.page == 0),
}) CreateButton::new(
.create_button(|b| {
b.custom_id(
serde_json::to_string(&SoundPager { serde_json::to_string(&SoundPager {
nonce: 1, nonce: 1,
page: self.page.saturating_sub(1), page: self.page.saturating_sub(1),
@ -150,16 +144,12 @@ impl SoundPager {
) )
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary)
.label("◀️") .label("◀️")
.disabled(self.page == 0) .disabled(self.page == 0),
}) CreateButton::new("pid")
.create_button(|b| {
b.custom_id("pid")
.style(ButtonStyle::Success) .style(ButtonStyle::Success)
.label(format!("Page {}", self.page + 1)) .label(format!("Page {}", self.page + 1))
.disabled(true) .disabled(true),
}) CreateButton::new(
.create_button(|b| {
b.custom_id(
serde_json::to_string(&SoundPager { serde_json::to_string(&SoundPager {
nonce: 2, nonce: 2,
page: self.page.saturating_add(1), page: self.page.saturating_add(1),
@ -169,10 +159,8 @@ impl SoundPager {
) )
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary)
.label("▶️") .label("▶️")
.disabled(self.page == max_page) .disabled(self.page == max_page),
}) CreateButton::new(
.create_button(|b| {
b.custom_id(
serde_json::to_string(&SoundPager { serde_json::to_string(&SoundPager {
nonce: 3, nonce: 3,
page: max_page, page: max_page,
@ -182,16 +170,14 @@ impl SoundPager {
) )
.style(ButtonStyle::Primary) .style(ButtonStyle::Primary)
.label("") .label("")
.disabled(self.page == max_page) .disabled(self.page == max_page),
}); ]);
row row
} }
fn embed(&self, sounds: &[Sound], count: u64) -> CreateEmbed { fn embed(&self, sounds: &[Sound], count: u64) -> CreateEmbed {
let mut embed = CreateEmbed::default(); CreateEmbed::default()
embed
.color(THEME_COLOR) .color(THEME_COLOR)
.title(self.context.title()) .title(self.context.title())
.description(format!("**{}** sounds:", count)) .description(format!("**{}** sounds:", count))
@ -205,15 +191,13 @@ impl SoundPager {
), ),
true, true,
) )
})); }))
embed
} }
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();
@ -227,14 +211,12 @@ impl SoundPager {
}; };
interaction interaction
.create_interaction_response(&ctx, |r| { .edit_response(
r.kind(InteractionResponseType::UpdateMessage) &ctx,
.interaction_response_data(|d| { EditInteractionResponse::default()
d.ephemeral(true)
.add_embed(pager.embed(&sounds, count)) .add_embed(pager.embed(&sounds, count))
.components(|c| c.add_action_row(pager.create_action_row(count / 25))) .components(vec![pager.create_action_row(count / 25)]),
}) )
})
.await?; .await?;
Ok(()) Ok(())
@ -254,14 +236,12 @@ impl SoundPager {
} }
}; };
ctx.send(|r| { ctx.send(
r.ephemeral(true) CreateReply::default()
.embed(|e| { .ephemeral(true)
*e = self.embed(&sounds, count); .embed(self.embed(&sounds, count))
e .components(vec![self.create_action_row(count / 25)]),
}) )
.components(|c| c.add_action_row(self.create_action_row(count / 25)))
})
.await?; .await?;
Ok(()) Ok(())
@ -284,11 +264,7 @@ pub async fn search_sounds(
.search_for_sound(&query, ctx.guild_id().unwrap(), ctx.author().id, false) .search_for_sound(&query, ctx.guild_id().unwrap(), ctx.author().id, false)
.await?; .await?;
ctx.send(|m| { ctx.send(format_search_results(search_results)).await?;
*m = format_search_results(search_results);
m
})
.await?;
Ok(()) Ok(())
} }

View File

@ -1,4 +1,7 @@
use poise::serenity_prelude::{GuildId, User}; use poise::{
serenity_prelude::{GuildId, User},
CreateReply,
};
use crate::{ use crate::{
cmds::autocomplete_sound, cmds::autocomplete_sound,
@ -60,14 +63,14 @@ pub async fn set_guild_greet_sound(
#[description = "User to set join sound for"] user: User, #[description = "User to set join sound for"] user: User,
) -> Result<(), Error> { ) -> Result<(), Error> {
if user.id != ctx.author().id { if user.id != ctx.author().id {
let guild = ctx.guild().unwrap(); let permissions = ctx.author_member().await.unwrap().permissions(&ctx.cache());
let permissions = guild.member_permissions(&ctx, ctx.author().id).await;
if permissions.map_or(true, |p| !p.manage_guild()) { if permissions.map_or(true, |p| !p.manage_guild()) {
ctx.send(|b| { ctx.send(
b.ephemeral(true) CreateReply::default()
.content("Only admins can change other user's greet sounds.") .ephemeral(true)
}) .content("Only admins can change other user's greet sounds."),
)
.await?; .await?;
return Ok(()); return Ok(());
@ -107,14 +110,14 @@ pub async fn unset_guild_greet_sound(
#[description = "User to set join sound for"] user: User, #[description = "User to set join sound for"] user: User,
) -> Result<(), Error> { ) -> Result<(), Error> {
if user.id != ctx.author().id { if user.id != ctx.author().id {
let guild = ctx.guild().unwrap(); let permissions = ctx.author_member().await.unwrap().permissions(&ctx.cache());
let permissions = guild.member_permissions(&ctx, ctx.author().id).await;
if permissions.map_or(true, |p| !p.manage_guild()) { if permissions.map_or(true, |p| !p.manage_guild()) {
ctx.send(|b| { ctx.send(
b.ephemeral(true) CreateReply::default()
.content("Only admins can change other user's greet sounds.") .ephemeral(true)
}) .content("Only admins can change other user's greet sounds."),
)
.await?; .await?;
return Ok(()); return Ok(());
@ -155,20 +158,19 @@ pub async fn set_user_greet_sound(
.update_join_sound(ctx.author().id, None::<GuildId>, Some(sound.id)) .update_join_sound(ctx.author().id, None::<GuildId>, Some(sound.id))
.await?; .await?;
ctx.send(|b| { ctx.send(CreateReply::default().ephemeral(true).content(format!(
b.ephemeral(true).content(format!(
"Greet sound has been set to {} (ID {})", "Greet sound has been set to {} (ID {})",
sound.name, sound.id sound.name, sound.id
)) )))
})
.await?; .await?;
} }
None => { None => {
ctx.send(|b| { ctx.send(
b.ephemeral(true) CreateReply::default()
.content("Could not find a sound by that name.") .ephemeral(true)
}) .content("Could not find a sound by that name."),
)
.await?; .await?;
} }
} }
@ -183,7 +185,11 @@ pub async fn unset_user_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
.update_join_sound(ctx.author().id, None::<GuildId>, None) .update_join_sound(ctx.author().id, None::<GuildId>, None)
.await?; .await?;
ctx.send(|b| b.ephemeral(true).content("Greet sound has been unset")) ctx.send(
CreateReply::default()
.ephemeral(true)
.content("Greet sound has been unset"),
)
.await?; .await?;
Ok(()) Ok(())

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 {
@ -123,7 +122,7 @@ pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> R
.create_interaction_response(ctx, |r| { .create_interaction_response(ctx, |r| {
r.kind(InteractionResponseType::UpdateMessage) r.kind(InteractionResponseType::UpdateMessage)
.interaction_response_data(|d| { .interaction_response_data(|d| {
let mut c: CreateComponents = Default::default(); let mut c = vec![];
for action_row in &component.message.components { for action_row in &component.message.components {
let mut a: CreateActionRow = Default::default(); let mut a: CreateActionRow = Default::default();
@ -176,7 +175,7 @@ pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> R
} }
} }
c.add_action_row(a); c.push(a);
} }
d.set_components(c) d.set_components(c)

View File

@ -14,13 +14,13 @@ use std::{env, path::Path, sync::Arc};
use dashmap::DashMap; use dashmap::DashMap;
use poise::serenity_prelude::{ use poise::serenity_prelude::{
builder::CreateApplicationCommands,
model::{ model::{
gateway::GatewayIntents, gateway::GatewayIntents,
id::{GuildId, UserId}, id::{GuildId, UserId},
}, },
ClientBuilder,
}; };
use songbird::SerenityInit; use serde_json::Value;
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -40,29 +40,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(())
} }
@ -149,13 +140,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
tokio::spawn(async { metrics::serve().await }); tokio::spawn(async { metrics::serve().await });
} }
poise::Framework::builder() let framework = poise::Framework::builder()
.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,
@ -165,10 +153,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}) })
}) })
.options(options) .options(options)
.client_settings(move |client_builder| client_builder.register_songbird()) .build();
.intents(GatewayIntents::GUILD_VOICE_STATES | GatewayIntents::GUILDS)
.run_autosharded() let mut client = ClientBuilder::new(
&discord_token,
GatewayIntents::GUILD_VOICE_STATES | GatewayIntents::GUILDS,
)
.framework(framework)
.await?; .await?;
client.start_autosharded().await.unwrap();
Ok(()) Ok(())
} }

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.get()
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.get()
",
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

@ -51,10 +51,9 @@ 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.get(),
user_id.as_u64(), guild_id.map(|g| g.get())
guild_id.map(|g| g.0)
) )
.fetch_one(&self.database) .fetch_one(&self.database)
.await .await
@ -66,10 +65,9 @@ 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.get(),
user_id.as_u64(), guild_id.map(|g| g.get())
guild_id.map(|g| g.0)
) )
.fetch_one(&self.database) .fetch_one(&self.database)
.await .await
@ -111,17 +109,17 @@ SELECT join_sound_id
Some(join_id) => { Some(join_id) => {
sqlx::query!( sqlx::query!(
"DELETE FROM join_sounds WHERE user = ? AND guild <=> ?", "DELETE FROM join_sounds WHERE user = ? AND guild <=> ?",
user_id.0, user_id.get(),
guild_id.map(|g| g.0) guild_id.map(|g| g.get())
) )
.execute(&mut transaction) .execute(&mut transaction)
.await?; .await?;
sqlx::query!( sqlx::query!(
"INSERT INTO join_sounds (user, join_sound_id, guild) VALUES (?, ?, ?)", "INSERT INTO join_sounds (user, join_sound_id, guild) VALUES (?, ?, ?)",
user_id.0, user_id.get(),
join_id, join_id,
guild_id.map(|g| g.0) guild_id.map(|g| g.get())
) )
.execute(&mut transaction) .execute(&mut transaction)
.await?; .await?;
@ -130,8 +128,8 @@ SELECT join_sound_id
None => { None => {
sqlx::query!( sqlx::query!(
"DELETE FROM join_sounds WHERE user = ? AND guild <=> ?", "DELETE FROM join_sounds WHERE user = ? AND guild <=> ?",
user_id.0, user_id.get(),
guild_id.map(|g| g.0) guild_id.map(|g| g.get())
) )
.execute(&mut transaction) .execute(&mut transaction)
.await?; .await?;

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,13 @@
use std::sync::Arc; use std::{ops::Deref, sync::Arc};
use poise::serenity_prelude::model::{ use poise::serenity_prelude::{
channel::Channel, model::{
guild::Guild, guild::Guild,
id::{ChannelId, UserId}, id::{ChannelId, UserId},
},
EditVoiceState,
}; };
use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call}; use songbird::{tracks::TrackHandle, Call};
use sqlx::Executor; use sqlx::Executor;
use tokio::sync::{Mutex, MutexGuard}; use tokio::sync::{Mutex, MutexGuard};
@ -24,19 +26,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_input(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 +47,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_input(track).await;
let _ = b.set_volume(volume as f32 / 100.0); handle.set_volume(volume as f32 / 100.0)?;
call_handler.enqueue(a);
} }
Ok(()) Ok(())
@ -58,60 +58,62 @@ 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 Deref<Target = Guild>,
channel_id: ChannelId, channel_id: ChannelId,
) -> (Arc<Mutex<Call>>, JoinResult<()>) { ) -> Result<Arc<Mutex<Call>>, Box<dyn std::error::Error + Send + Sync>> {
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
.deref()
.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.deref().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.deref().id, channel_id).await
(call, res)
} }
} else { } else {
let (call, res) = songbird.join(guild.id, channel_id).await; songbird.join(guild.deref().id, channel_id).await
}?;
(call, res)
};
{ {
// set call to deafen // set call to deafen
let _ = call.lock().await.deafen(true).await; let _ = call.lock().await.deafen(true).await;
} }
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) { if let Some(channel) = channel_id.to_channel_cached(&ctx.cache).as_deref() {
let _ = channel channel
.edit_voice_state(&ctx, ctx.cache.current_user(), |v| v.suppress(false)) .edit_voice_state(
.await; &ctx,
ctx.cache.current_user().id,
EditVoiceState::new().suppress(true),
)
.await?;
} }
(call, res) Ok(call)
} }
pub async fn play_from_query( pub async fn play_from_query(
ctx: &poise::serenity_prelude::Context, ctx: &poise::serenity_prelude::Context,
data: &Data, data: &Data,
guild: Guild, guild: impl Deref<Target = Guild>,
user_id: UserId, user_id: UserId,
channel: Option<ChannelId>, channel: Option<ChannelId>,
query: &str, query: &str,
r#loop: bool, r#loop: bool,
) -> String { ) -> String {
let guild_id = guild.id; let guild_id = guild.deref().id;
let channel_to_join = channel.or_else(|| { let channel_to_join = channel.or_else(|| {
guild guild
.deref()
.voice_states .voice_states
.get(&user_id) .get(&user_id)
.and_then(|voice_state| voice_state.channel_id) .and_then(|voice_state| voice_state.channel_id)
@ -129,8 +131,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, user_channel).await.unwrap();
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();