Compare commits
1 Commits
rewrite
...
jude/index
Author | SHA1 | Date | |
---|---|---|---|
|
a2ac7050d7 |
3126
Cargo.lock
generated
3126
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
29
Cargo.toml
29
Cargo.toml
@ -2,36 +2,29 @@
|
||||
name = "soundfx-rs"
|
||||
description = "Discord bot for custom sound effects and soundboards"
|
||||
license = "AGPL-3.0-only"
|
||||
version = "1.5.18"
|
||||
version = "1.5.11"
|
||||
authors = ["jellywx <judesouthworth@pm.me>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
songbird = { version = "0.4", features = ["builtin-queue"] }
|
||||
poise = "0.6.1-rc1"
|
||||
sqlx = { version = "0.7.3", default-features = false, features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "migrate"] }
|
||||
tokio = { version = "1", features = ["fs", "process", "io-util", "rt-multi-thread"] }
|
||||
songbird = { version = "0.3", features = ["builtin-queue"] }
|
||||
poise = "0.5.5"
|
||||
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.12"
|
||||
env_logger = "0.11"
|
||||
regex = "1.10"
|
||||
reqwest = "0.11"
|
||||
env_logger = "0.10"
|
||||
regex = "1.4"
|
||||
log = "0.4"
|
||||
serde_json = "1.0"
|
||||
dashmap = "6.0"
|
||||
dashmap = "5.3"
|
||||
serde = "1.0"
|
||||
dotenv = "0.15.0"
|
||||
prometheus = { version = "0.13.3", optional = true }
|
||||
axum = { version = "0.7.2", optional = true }
|
||||
|
||||
[dependencies.symphonia]
|
||||
version = "0.5"
|
||||
features = ["ogg"]
|
||||
|
||||
[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"
|
||||
suggests = "mysql-server-8.0"
|
||||
maintainer-scripts = "debian"
|
||||
|
6
migrations/20231021161427_add_indexes.sql
Normal file
6
migrations/20231021161427_add_indexes.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- Add migration script here
|
||||
ALTER TABLE `sounds` ADD UNIQUE INDEX `uploader_id_name` (`uploader_id`, `name`);
|
||||
ALTER TABLE `sounds` ADD INDEX `name` (`name`);
|
||||
ALTER TABLE `sounds` ADD INDEX `public` (`public`);
|
||||
ALTER TABLE `sounds` ADD INDEX `uploader_id` (`uploader_id`);
|
||||
ALTER TABLE `sounds` ADD INDEX `server_id` (`server_id`);
|
@ -1,23 +1,19 @@
|
||||
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(
|
||||
CreateReply::default().ephemeral(true).embed(
|
||||
CreateEmbed::new()
|
||||
.title("Help")
|
||||
ctx.send(|m| {
|
||||
m.ephemeral(true).embed(|e| {
|
||||
e.title("Help")
|
||||
.color(THEME_COLOR)
|
||||
.footer(CreateEmbedFooter::new(concat!(
|
||||
.footer(|f| {
|
||||
f.text(concat!(
|
||||
env!("CARGO_PKG_NAME"),
|
||||
" ver ",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)))
|
||||
))
|
||||
})
|
||||
.description(
|
||||
"__Info Commands__
|
||||
`/help` `/info`
|
||||
@ -53,9 +49,9 @@ __Setting Commands__
|
||||
|
||||
__Advanced Commands__
|
||||
`/soundboard` - Create a soundboard",
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@ -64,18 +60,14 @@ __Advanced Commands__
|
||||
/// Get additional information about the bot
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let current_user = ctx.serenity_context().cache.current_user().id.get();
|
||||
let current_user = ctx.serenity_context().cache.current_user();
|
||||
|
||||
ctx.send(
|
||||
CreateReply::default().ephemeral(true).embed(
|
||||
CreateEmbed::new()
|
||||
ctx.send(|m| m.ephemeral(true)
|
||||
.embed(|e| e
|
||||
.title("Info")
|
||||
.color(THEME_COLOR)
|
||||
.footer(CreateEmbedFooter::new(concat!(
|
||||
env!("CARGO_PKG_NAME"),
|
||||
" ver ",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)))
|
||||
.footer(|f| f
|
||||
.text(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!**
|
||||
@ -84,9 +76,7 @@ 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)))
|
||||
)
|
||||
.await?;
|
||||
current_user.id.as_u64())))).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,10 +1,6 @@
|
||||
use poise::{
|
||||
serenity_prelude::{Attachment, CreateAttachment, GuildId, RoleId},
|
||||
CreateReply,
|
||||
};
|
||||
use poise::serenity_prelude::{Attachment, GuildId, RoleId};
|
||||
use tokio::fs::File;
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
use crate::metrics::{DELETE_COUNTER, UPLOAD_COUNTER};
|
||||
use crate::{
|
||||
cmds::autocomplete_sound,
|
||||
consts::{MAX_SOUNDS, PATREON_GUILD, PATREON_ROLE},
|
||||
@ -25,9 +21,6 @@ pub async fn upload_new_sound(
|
||||
#[description = "Name to upload sound to"] name: String,
|
||||
#[description = "Sound file (max. 2MB)"] file: Attachment,
|
||||
) -> Result<(), Error> {
|
||||
#[cfg(feature = "metrics")]
|
||||
UPLOAD_COUNTER.inc();
|
||||
|
||||
ctx.defer().await?;
|
||||
|
||||
fn is_numeric(s: &String) -> bool {
|
||||
@ -65,12 +58,11 @@ pub async fn upload_new_sound(
|
||||
|
||||
// need to check if user is Patreon or not
|
||||
if count >= *MAX_SOUNDS {
|
||||
let patreon_guild_member = GuildId::from(*PATREON_GUILD)
|
||||
.member(ctx, ctx.author().id)
|
||||
.await;
|
||||
let patreon_guild_member =
|
||||
GuildId(*PATREON_GUILD).member(ctx, ctx.author().id).await;
|
||||
|
||||
if let Ok(member) = patreon_guild_member {
|
||||
permit_upload = member.roles.contains(&RoleId::from(*PATREON_ROLE));
|
||||
permit_upload = member.roles.contains(&RoleId(*PATREON_ROLE));
|
||||
} else {
|
||||
permit_upload = false;
|
||||
}
|
||||
@ -118,13 +110,10 @@ pub async fn delete_sound(
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
#[cfg(feature = "metrics")]
|
||||
DELETE_COUNTER.inc();
|
||||
|
||||
let pool = ctx.data().database.clone();
|
||||
|
||||
let uid = ctx.author().id.get();
|
||||
let gid = ctx.guild_id().unwrap().get();
|
||||
let uid = ctx.author().id.0;
|
||||
let gid = ctx.guild_id().unwrap().0;
|
||||
|
||||
let sound_vec = ctx.data().search_for_sound(&name, gid, uid, true).await?;
|
||||
let sound_result = sound_vec.first();
|
||||
@ -176,8 +165,8 @@ pub async fn change_public(
|
||||
) -> Result<(), Error> {
|
||||
let pool = ctx.data().database.clone();
|
||||
|
||||
let uid = ctx.author().id.get();
|
||||
let gid = ctx.guild_id().unwrap().get();
|
||||
let uid = ctx.author().id.0;
|
||||
let gid = ctx.guild_id().unwrap().0;
|
||||
|
||||
let mut sound_vec = ctx.data().search_for_sound(&name, gid, uid, true).await?;
|
||||
let sound_result = sound_vec.first_mut();
|
||||
@ -226,12 +215,12 @@ pub async fn download_file(
|
||||
|
||||
match sound.first() {
|
||||
Some(sound) => {
|
||||
let source = sound.store_sound_source(&ctx.data().database).await?;
|
||||
|
||||
let file = File::open(&source).await?;
|
||||
let name = format!("{}-{}.opus", sound.id, sound.name);
|
||||
|
||||
ctx.send(CreateReply::default().attachment(CreateAttachment::bytes(
|
||||
sound.src(&ctx.data().database).await,
|
||||
name.as_str(),
|
||||
)))
|
||||
ctx.send(|m| m.attachment((&file, name.as_str()).into()))
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
use poise::serenity_prelude::AutocompleteChoice;
|
||||
|
||||
use crate::{models::sound::SoundCtx, Context};
|
||||
|
||||
pub mod favorite;
|
||||
@ -10,22 +8,34 @@ pub mod search;
|
||||
pub mod settings;
|
||||
pub mod stop;
|
||||
|
||||
pub async fn autocomplete_sound(ctx: Context<'_>, partial: &str) -> Vec<AutocompleteChoice> {
|
||||
pub async fn autocomplete_sound(
|
||||
ctx: Context<'_>,
|
||||
partial: &str,
|
||||
) -> Vec<poise::AutocompleteChoice<String>> {
|
||||
ctx.data()
|
||||
.autocomplete_user_sounds(&partial, ctx.author().id, ctx.guild_id().unwrap())
|
||||
.await
|
||||
.unwrap_or(vec![])
|
||||
.iter()
|
||||
.map(|s| AutocompleteChoice::new(s.name.clone(), s.id.to_string()))
|
||||
.map(|s| poise::AutocompleteChoice {
|
||||
name: s.name.clone(),
|
||||
value: s.id.to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn autocomplete_favorite(ctx: Context<'_>, partial: &str) -> Vec<AutocompleteChoice> {
|
||||
pub async fn autocomplete_favorite(
|
||||
ctx: Context<'_>,
|
||||
partial: &str,
|
||||
) -> Vec<poise::AutocompleteChoice<String>> {
|
||||
ctx.data()
|
||||
.autocomplete_favorite_sounds(&partial, ctx.author().id)
|
||||
.await
|
||||
.unwrap_or(vec![])
|
||||
.iter()
|
||||
.map(|s| AutocompleteChoice::new(s.name.clone(), s.id.to_string()))
|
||||
.map(|s| poise::AutocompleteChoice {
|
||||
name: s.name.clone(),
|
||||
value: s.id.to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
192
src/cmds/play.rs
192
src/cmds/play.rs
@ -1,18 +1,12 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use poise::{
|
||||
serenity_prelude::{
|
||||
builder::CreateActionRow, ButtonStyle, CreateButton, GuildChannel, ReactionType,
|
||||
},
|
||||
CreateReply,
|
||||
use poise::serenity_prelude::{
|
||||
builder::CreateActionRow, model::application::component::ButtonStyle, GuildChannel,
|
||||
ReactionType,
|
||||
};
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
use crate::metrics::PLAY_COUNTER;
|
||||
use crate::{
|
||||
cmds::autocomplete_sound,
|
||||
models::{guild_data::CtxGuildData, sound::SoundCtx},
|
||||
utils::{join_channel, play_audio, play_from_query, queue_audio},
|
||||
utils::{join_channel, play_from_query, queue_audio},
|
||||
Context, Error,
|
||||
};
|
||||
|
||||
@ -27,18 +21,15 @@ pub async fn play(
|
||||
#[channel_types("Voice")]
|
||||
channel: Option<GuildChannel>,
|
||||
) -> Result<(), Error> {
|
||||
#[cfg(feature = "metrics")]
|
||||
PLAY_COUNTER.inc();
|
||||
|
||||
ctx.defer().await?;
|
||||
|
||||
let guild = ctx.guild().map(|g| g.clone()).unwrap();
|
||||
let guild = ctx.guild().unwrap();
|
||||
|
||||
ctx.say(
|
||||
play_from_query(
|
||||
&ctx.serenity_context(),
|
||||
&ctx.data(),
|
||||
&guild,
|
||||
guild,
|
||||
ctx.author().id,
|
||||
channel.map(|c| c.id),
|
||||
&name,
|
||||
@ -51,84 +42,6 @@ pub async fn play(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Play a random sound from this server
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
rename = "random",
|
||||
default_member_permissions = "SPEAK",
|
||||
guild_only = true
|
||||
)]
|
||||
pub async fn play_random(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Channel to play in (default: your current voice channel)"]
|
||||
#[channel_types("Voice")]
|
||||
channel: Option<GuildChannel>,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
|
||||
let (channel_to_join, guild_id) = {
|
||||
let guild = ctx.guild().unwrap();
|
||||
|
||||
(
|
||||
channel.map(|c| c.id).or_else(|| {
|
||||
guild
|
||||
.voice_states
|
||||
.get(&ctx.author().id)
|
||||
.and_then(|voice_state| voice_state.channel_id)
|
||||
}),
|
||||
guild.id,
|
||||
)
|
||||
};
|
||||
|
||||
match channel_to_join {
|
||||
Some(channel) => {
|
||||
let call = join_channel(ctx.serenity_context(), guild_id, channel).await?;
|
||||
|
||||
let sounds = ctx.data().guild_sounds(guild_id, None).await?;
|
||||
if sounds.len() == 0 {
|
||||
ctx.say("No sounds in this server!").await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
|
||||
println!("{}", ts.subsec_micros());
|
||||
|
||||
// This is far cheaper and easier than using an RNG. No reason to use a full RNG here
|
||||
// anyway.
|
||||
match sounds.get(ts.subsec_micros() as usize % sounds.len()) {
|
||||
Some(sound) => {
|
||||
let guild_data = ctx.data().guild_data(guild_id).await.unwrap();
|
||||
let mut lock = call.lock().await;
|
||||
|
||||
play_audio(
|
||||
sound,
|
||||
guild_data.read().await.volume,
|
||||
&mut lock,
|
||||
&ctx.data().database,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
ctx.say(format!("Playing {} (ID {})", sound.name, sound.id))
|
||||
.await?;
|
||||
}
|
||||
|
||||
None => {
|
||||
ctx.say("No sounds in this server!").await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None => {
|
||||
ctx.say("You are not in a voice chat!").await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Play up to 25 sounds on queue
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
@ -216,23 +129,25 @@ pub async fn queue_play(
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
|
||||
let (channel_to_join, guild_id) = {
|
||||
let guild = ctx.guild().unwrap();
|
||||
|
||||
(
|
||||
guild
|
||||
let channel_to_join = guild
|
||||
.voice_states
|
||||
.get(&ctx.author().id)
|
||||
.and_then(|voice_state| voice_state.channel_id),
|
||||
guild.id,
|
||||
)
|
||||
};
|
||||
.and_then(|voice_state| voice_state.channel_id);
|
||||
|
||||
match channel_to_join {
|
||||
Some(user_channel) => {
|
||||
let call = join_channel(ctx.serenity_context(), guild_id, user_channel).await?;
|
||||
let (call_handler, _) =
|
||||
join_channel(ctx.serenity_context(), guild.clone(), user_channel).await;
|
||||
|
||||
let guild_data = ctx.data().guild_data(guild_id).await.unwrap();
|
||||
let guild_data = ctx
|
||||
.data()
|
||||
.guild_data(ctx.guild_id().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut lock = call_handler.lock().await;
|
||||
|
||||
let query_terms = [
|
||||
Some(sound_1),
|
||||
@ -275,9 +190,6 @@ pub async fn queue_play(
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut lock = call.lock().await;
|
||||
|
||||
queue_audio(
|
||||
&sounds,
|
||||
guild_data.read().await.volume,
|
||||
@ -286,7 +198,6 @@ pub async fn queue_play(
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
ctx.say(format!("Queued {} sounds!", sounds.len())).await?;
|
||||
}
|
||||
@ -313,13 +224,13 @@ pub async fn loop_play(
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
|
||||
let guild = ctx.guild().map(|g| g.clone()).unwrap();
|
||||
let guild = ctx.guild().unwrap();
|
||||
|
||||
ctx.say(
|
||||
play_from_query(
|
||||
&ctx.serenity_context(),
|
||||
&ctx.data(),
|
||||
&guild,
|
||||
guild,
|
||||
ctx.author().id,
|
||||
None,
|
||||
&name,
|
||||
@ -443,49 +354,50 @@ pub async fn soundboard(
|
||||
}
|
||||
}
|
||||
|
||||
let components = {
|
||||
let mut c = vec![];
|
||||
ctx.send(|m| {
|
||||
m.content("**Play a sound:**").components(|c| {
|
||||
for row in sounds.as_slice().chunks(5) {
|
||||
let mut action_row = vec![];
|
||||
let mut action_row: CreateActionRow = Default::default();
|
||||
for sound in row {
|
||||
action_row.push(
|
||||
CreateButton::new(sound.id.to_string())
|
||||
.style(ButtonStyle::Primary)
|
||||
.label(&sound.name),
|
||||
);
|
||||
action_row.create_button(|b| {
|
||||
b.style(ButtonStyle::Primary)
|
||||
.label(&sound.name)
|
||||
.custom_id(sound.id)
|
||||
});
|
||||
}
|
||||
|
||||
c.push(CreateActionRow::Buttons(action_row));
|
||||
c.add_action_row(action_row);
|
||||
}
|
||||
|
||||
c.push(CreateActionRow::Buttons(vec![
|
||||
CreateButton::new("#stop")
|
||||
.label("Stop")
|
||||
c.create_action_row(|r| {
|
||||
r.create_button(|b| {
|
||||
b.label("Stop")
|
||||
.emoji(ReactionType::Unicode("⏹".to_string()))
|
||||
.style(ButtonStyle::Danger),
|
||||
CreateButton::new("#mode")
|
||||
.label("Mode:")
|
||||
.style(ButtonStyle::Danger)
|
||||
.custom_id("#stop")
|
||||
})
|
||||
.create_button(|b| {
|
||||
b.label("Mode:")
|
||||
.style(ButtonStyle::Secondary)
|
||||
.disabled(true),
|
||||
CreateButton::new("#instant")
|
||||
.label("Instant")
|
||||
.disabled(true)
|
||||
.custom_id("#mode")
|
||||
})
|
||||
.create_button(|b| {
|
||||
b.label("Instant")
|
||||
.emoji(ReactionType::Unicode("▶".to_string()))
|
||||
.style(ButtonStyle::Secondary)
|
||||
.disabled(true),
|
||||
CreateButton::new("#loop")
|
||||
.label("Loop")
|
||||
.disabled(true)
|
||||
.custom_id("#instant")
|
||||
})
|
||||
.create_button(|b| {
|
||||
b.label("Loop")
|
||||
.emoji(ReactionType::Unicode("🔁".to_string()))
|
||||
.style(ButtonStyle::Secondary),
|
||||
]));
|
||||
|
||||
c
|
||||
};
|
||||
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.content("**Play a sound:**")
|
||||
.components(components),
|
||||
)
|
||||
.style(ButtonStyle::Secondary)
|
||||
.custom_id("#loop")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
|
@ -1,8 +1,10 @@
|
||||
use poise::{
|
||||
serenity_prelude,
|
||||
serenity_prelude::{
|
||||
constants::MESSAGE_CODE_LIMIT, ButtonStyle, ComponentInteraction, CreateActionRow,
|
||||
CreateButton, CreateEmbed, EditInteractionResponse, GuildId, UserId,
|
||||
application::component::ButtonStyle,
|
||||
constants::MESSAGE_CODE_LIMIT,
|
||||
interaction::{message_component::MessageComponentInteraction, InteractionResponseType},
|
||||
CreateActionRow, CreateEmbed, GuildId, UserId,
|
||||
},
|
||||
CreateReply,
|
||||
};
|
||||
@ -14,8 +16,8 @@ use crate::{
|
||||
Context, Data, Error,
|
||||
};
|
||||
|
||||
fn format_search_results(search_results: Vec<Sound>) -> CreateReply {
|
||||
let builder = CreateReply::default();
|
||||
fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> {
|
||||
let mut builder = CreateReply::default();
|
||||
|
||||
let mut current_character_count = 0;
|
||||
let title = "Public sounds matching filter:";
|
||||
@ -30,7 +32,9 @@ fn format_search_results(search_results: Vec<Sound>) -> CreateReply {
|
||||
current_character_count <= MESSAGE_CODE_LIMIT - title.len()
|
||||
});
|
||||
|
||||
builder.embed(CreateEmbed::default().title(title).fields(field_iter))
|
||||
builder.embed(|e| e.title(title).fields(field_iter));
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
/// Show uploaded sounds
|
||||
@ -120,8 +124,10 @@ impl SoundPager {
|
||||
}
|
||||
|
||||
fn create_action_row(&self, max_page: u64) -> CreateActionRow {
|
||||
let row = CreateActionRow::Buttons(vec![
|
||||
CreateButton::new(
|
||||
let mut row = CreateActionRow::default();
|
||||
|
||||
row.create_button(|b| {
|
||||
b.custom_id(
|
||||
serde_json::to_string(&SoundPager {
|
||||
nonce: 0,
|
||||
page: 0,
|
||||
@ -131,8 +137,10 @@ impl SoundPager {
|
||||
)
|
||||
.style(ButtonStyle::Primary)
|
||||
.label("⏪")
|
||||
.disabled(self.page == 0),
|
||||
CreateButton::new(
|
||||
.disabled(self.page == 0)
|
||||
})
|
||||
.create_button(|b| {
|
||||
b.custom_id(
|
||||
serde_json::to_string(&SoundPager {
|
||||
nonce: 1,
|
||||
page: self.page.saturating_sub(1),
|
||||
@ -142,12 +150,16 @@ impl SoundPager {
|
||||
)
|
||||
.style(ButtonStyle::Secondary)
|
||||
.label("◀️")
|
||||
.disabled(self.page == 0),
|
||||
CreateButton::new("pid")
|
||||
.disabled(self.page == 0)
|
||||
})
|
||||
.create_button(|b| {
|
||||
b.custom_id("pid")
|
||||
.style(ButtonStyle::Success)
|
||||
.label(format!("Page {}", self.page + 1))
|
||||
.disabled(true),
|
||||
CreateButton::new(
|
||||
.disabled(true)
|
||||
})
|
||||
.create_button(|b| {
|
||||
b.custom_id(
|
||||
serde_json::to_string(&SoundPager {
|
||||
nonce: 2,
|
||||
page: self.page.saturating_add(1),
|
||||
@ -157,8 +169,10 @@ impl SoundPager {
|
||||
)
|
||||
.style(ButtonStyle::Secondary)
|
||||
.label("▶️")
|
||||
.disabled(self.page == max_page),
|
||||
CreateButton::new(
|
||||
.disabled(self.page == max_page)
|
||||
})
|
||||
.create_button(|b| {
|
||||
b.custom_id(
|
||||
serde_json::to_string(&SoundPager {
|
||||
nonce: 3,
|
||||
page: max_page,
|
||||
@ -168,14 +182,16 @@ impl SoundPager {
|
||||
)
|
||||
.style(ButtonStyle::Primary)
|
||||
.label("⏩")
|
||||
.disabled(self.page == max_page),
|
||||
]);
|
||||
.disabled(self.page == max_page)
|
||||
});
|
||||
|
||||
row
|
||||
}
|
||||
|
||||
fn embed(&self, sounds: &[Sound], count: u64) -> CreateEmbed {
|
||||
CreateEmbed::default()
|
||||
let mut embed = CreateEmbed::default();
|
||||
|
||||
embed
|
||||
.color(THEME_COLOR)
|
||||
.title(self.context.title())
|
||||
.description(format!("**{}** sounds:", count))
|
||||
@ -189,13 +205,15 @@ impl SoundPager {
|
||||
),
|
||||
true,
|
||||
)
|
||||
}))
|
||||
}));
|
||||
|
||||
embed
|
||||
}
|
||||
|
||||
pub async fn handle_interaction(
|
||||
ctx: &serenity_prelude::Context,
|
||||
data: &Data,
|
||||
interaction: &ComponentInteraction,
|
||||
interaction: &MessageComponentInteraction,
|
||||
) -> Result<(), Error> {
|
||||
let user_id = interaction.user.id;
|
||||
let guild_id = interaction.guild_id.unwrap();
|
||||
@ -209,12 +227,14 @@ impl SoundPager {
|
||||
};
|
||||
|
||||
interaction
|
||||
.edit_response(
|
||||
&ctx,
|
||||
EditInteractionResponse::default()
|
||||
.create_interaction_response(&ctx, |r| {
|
||||
r.kind(InteractionResponseType::UpdateMessage)
|
||||
.interaction_response_data(|d| {
|
||||
d.ephemeral(true)
|
||||
.add_embed(pager.embed(&sounds, count))
|
||||
.components(vec![pager.create_action_row(count / 25)]),
|
||||
)
|
||||
.components(|c| c.add_action_row(pager.create_action_row(count / 25)))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@ -234,12 +254,14 @@ impl SoundPager {
|
||||
}
|
||||
};
|
||||
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.ephemeral(true)
|
||||
.embed(self.embed(&sounds, count))
|
||||
.components(vec![self.create_action_row(count / 25)]),
|
||||
)
|
||||
ctx.send(|r| {
|
||||
r.ephemeral(true)
|
||||
.embed(|e| {
|
||||
*e = self.embed(&sounds, count);
|
||||
e
|
||||
})
|
||||
.components(|c| c.add_action_row(self.create_action_row(count / 25)))
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@ -262,7 +284,11 @@ pub async fn search_sounds(
|
||||
.search_for_sound(&query, ctx.guild_id().unwrap(), ctx.author().id, false)
|
||||
.await?;
|
||||
|
||||
ctx.send(format_search_results(search_results)).await?;
|
||||
ctx.send(|m| {
|
||||
*m = format_search_results(search_results);
|
||||
m
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,7 +1,4 @@
|
||||
use poise::{
|
||||
serenity_prelude::{GuildId, User},
|
||||
CreateReply,
|
||||
};
|
||||
use poise::serenity_prelude::{GuildId, User};
|
||||
|
||||
use crate::{
|
||||
cmds::autocomplete_sound,
|
||||
@ -63,14 +60,14 @@ pub async fn set_guild_greet_sound(
|
||||
#[description = "User to set join sound for"] user: User,
|
||||
) -> Result<(), Error> {
|
||||
if user.id != ctx.author().id {
|
||||
let permissions = ctx.author_member().await.unwrap().permissions(&ctx.cache());
|
||||
let guild = ctx.guild().unwrap();
|
||||
let permissions = guild.member_permissions(&ctx, ctx.author().id).await;
|
||||
|
||||
if permissions.map_or(true, |p| !p.manage_guild()) {
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.ephemeral(true)
|
||||
.content("Only admins can change other user's greet sounds."),
|
||||
)
|
||||
ctx.send(|b| {
|
||||
b.ephemeral(true)
|
||||
.content("Only admins can change other user's greet sounds.")
|
||||
})
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
@ -110,14 +107,14 @@ pub async fn unset_guild_greet_sound(
|
||||
#[description = "User to set join sound for"] user: User,
|
||||
) -> Result<(), Error> {
|
||||
if user.id != ctx.author().id {
|
||||
let permissions = ctx.author_member().await.unwrap().permissions(&ctx.cache());
|
||||
let guild = ctx.guild().unwrap();
|
||||
let permissions = guild.member_permissions(&ctx, ctx.author().id).await;
|
||||
|
||||
if permissions.map_or(true, |p| !p.manage_guild()) {
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.ephemeral(true)
|
||||
.content("Only admins can change other user's greet sounds."),
|
||||
)
|
||||
ctx.send(|b| {
|
||||
b.ephemeral(true)
|
||||
.content("Only admins can change other user's greet sounds.")
|
||||
})
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
@ -158,19 +155,20 @@ pub async fn set_user_greet_sound(
|
||||
.update_join_sound(ctx.author().id, None::<GuildId>, Some(sound.id))
|
||||
.await?;
|
||||
|
||||
ctx.send(CreateReply::default().ephemeral(true).content(format!(
|
||||
ctx.send(|b| {
|
||||
b.ephemeral(true).content(format!(
|
||||
"Greet sound has been set to {} (ID {})",
|
||||
sound.name, sound.id
|
||||
)))
|
||||
))
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
None => {
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.ephemeral(true)
|
||||
.content("Could not find a sound by that name."),
|
||||
)
|
||||
ctx.send(|b| {
|
||||
b.ephemeral(true)
|
||||
.content("Could not find a sound by that name.")
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
@ -185,11 +183,7 @@ pub async fn unset_user_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
|
||||
.update_join_sound(ctx.author().id, None::<GuildId>, None)
|
||||
.await?;
|
||||
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.ephemeral(true)
|
||||
.content("Greet sound has been unset"),
|
||||
)
|
||||
ctx.send(|b| b.ephemeral(true).content("Greet sound has been unset"))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
|
@ -1,10 +1,14 @@
|
||||
use std::{collections::HashMap, env};
|
||||
|
||||
use poise::serenity_prelude::{
|
||||
ActionRowComponent, ButtonKind, Context, CreateActionRow, CreateButton,
|
||||
EditInteractionResponse, FullEvent, Interaction,
|
||||
model::{
|
||||
application::interaction::{Interaction, InteractionResponseType},
|
||||
channel::Channel,
|
||||
},
|
||||
utils::shard_id,
|
||||
ActionRowComponent, Activity, Context, CreateActionRow, CreateComponents,
|
||||
};
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
use crate::metrics::GREET_COUNTER;
|
||||
use crate::{
|
||||
cmds::search::SoundPager,
|
||||
models::{
|
||||
@ -16,29 +20,67 @@ use crate::{
|
||||
Data, Error,
|
||||
};
|
||||
|
||||
pub async fn listener(ctx: &Context, event: &FullEvent, data: &Data) -> Result<(), Error> {
|
||||
pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> {
|
||||
match event {
|
||||
FullEvent::VoiceStateUpdate { old, new, .. } => {
|
||||
poise::Event::Ready { .. } => {
|
||||
ctx.set_activity(Activity::watching("for /play")).await;
|
||||
}
|
||||
poise::Event::GuildCreate { guild, is_new, .. } => {
|
||||
if *is_new {
|
||||
if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
|
||||
let shard_count = ctx.cache.shard_count();
|
||||
let current_shard_id = shard_id(guild.id.as_u64().to_owned(), shard_count);
|
||||
|
||||
let guild_count = ctx
|
||||
.cache
|
||||
.guilds()
|
||||
.iter()
|
||||
.filter(|g| {
|
||||
shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id
|
||||
})
|
||||
.count() as u64;
|
||||
|
||||
let mut hm = HashMap::new();
|
||||
hm.insert("server_count", guild_count);
|
||||
hm.insert("shard_id", current_shard_id);
|
||||
hm.insert("shard_count", shard_count);
|
||||
|
||||
let response = data
|
||||
.http
|
||||
.post(
|
||||
format!(
|
||||
"https://top.gg/api/bots/{}/stats",
|
||||
ctx.cache.current_user_id().as_u64()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.header("Authorization", token)
|
||||
.json(&hm)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if let Err(res) = response {
|
||||
println!("DiscordBots Response: {:?}", res);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
poise::Event::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 {
|
||||
let is_okay = ctx
|
||||
.cache
|
||||
.channel(channel_id)
|
||||
.map(|c| c.members(&ctx).ok().map(|m| m.len()))
|
||||
.flatten()
|
||||
.unwrap_or(0)
|
||||
<= 1;
|
||||
|
||||
if is_okay {
|
||||
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
|
||||
if channel.members(&ctx).await.map(|m| m.len()).unwrap_or(0) <= 1 {
|
||||
let songbird = songbird::get(ctx).await.unwrap();
|
||||
|
||||
songbird.remove(guild_id).await?;
|
||||
let _ = songbird.remove(guild_id).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let (Some(guild_id), Some(user_channel)) = (new.guild_id, new.channel_id) {
|
||||
let guild_data_opt = data.guild_data(guild_id).await;
|
||||
if let Some(guild) = ctx.cache.guild(guild_id) {
|
||||
let guild_data_opt = data.guild_data(guild.id).await;
|
||||
|
||||
if let Ok(guild_data) = guild_data_opt {
|
||||
let volume;
|
||||
@ -63,24 +105,22 @@ pub async fn listener(ctx: &Context, event: &FullEvent, data: &Data) -> Result<(
|
||||
let mut sound = sqlx::query_as_unchecked!(
|
||||
Sound,
|
||||
"
|
||||
SELECT name, id, public, server_id, uploader_id
|
||||
SELECT name, id, public, server_id, uploader_id
|
||||
FROM sounds
|
||||
WHERE id = ?",
|
||||
WHERE id = ?
|
||||
",
|
||||
join_id
|
||||
)
|
||||
.fetch_one(&data.database)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let call = join_channel(&ctx, guild_id, user_channel).await?;
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
GREET_COUNTER.inc();
|
||||
let (handler, _) = join_channel(&ctx, guild, user_channel).await;
|
||||
|
||||
play_audio(
|
||||
&mut sound,
|
||||
volume,
|
||||
&mut call.lock().await,
|
||||
&mut handler.lock().await,
|
||||
&data.database,
|
||||
false,
|
||||
)
|
||||
@ -91,15 +131,21 @@ pub async fn listener(ctx: &Context, event: &FullEvent, data: &Data) -> Result<(
|
||||
}
|
||||
}
|
||||
}
|
||||
FullEvent::InteractionCreate { interaction } => match interaction {
|
||||
Interaction::Component(component) => {
|
||||
}
|
||||
poise::Event::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 {
|
||||
} else {
|
||||
let mode = component.data.custom_id.as_str();
|
||||
match mode {
|
||||
"#stop" => {
|
||||
component.defer(&ctx).await.unwrap();
|
||||
component
|
||||
.create_interaction_response(ctx, |r| {
|
||||
r.kind(InteractionResponseType::DeferredUpdateMessage)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let songbird = songbird::get(ctx).await.unwrap();
|
||||
let call_opt = songbird.get(guild_id);
|
||||
@ -112,72 +158,89 @@ pub async fn listener(ctx: &Context, event: &FullEvent, data: &Data) -> Result<(
|
||||
}
|
||||
|
||||
"#loop" | "#queue" | "#instant" => {
|
||||
let components = {
|
||||
let mut c = vec![];
|
||||
component
|
||||
.create_interaction_response(ctx, |r| {
|
||||
r.kind(InteractionResponseType::UpdateMessage)
|
||||
.interaction_response_data(|d| {
|
||||
let mut c: CreateComponents = Default::default();
|
||||
|
||||
for action_row in &component.message.components {
|
||||
let mut row = vec![];
|
||||
let mut a: CreateActionRow = Default::default();
|
||||
// These are always buttons
|
||||
for component in &action_row.components {
|
||||
match component {
|
||||
ActionRowComponent::Button(button) => match &button
|
||||
.data
|
||||
ActionRowComponent::Button(button) => {
|
||||
a.create_button(|b| {
|
||||
if let Some(label) =
|
||||
&button.label
|
||||
{
|
||||
ButtonKind::Link { .. } => {}
|
||||
ButtonKind::NonLink { custom_id, style } => row
|
||||
.push(
|
||||
CreateButton::new(
|
||||
if custom_id.starts_with('#') {
|
||||
custom_id.to_string()
|
||||
b.label(label);
|
||||
}
|
||||
if let Some(emoji) =
|
||||
&button.emoji
|
||||
{
|
||||
b.emoji(emoji.clone());
|
||||
}
|
||||
if let Some(custom_id) =
|
||||
&button.custom_id
|
||||
{
|
||||
if custom_id
|
||||
.starts_with('#')
|
||||
{
|
||||
b.custom_id(custom_id)
|
||||
.disabled(
|
||||
custom_id
|
||||
== "#mode"
|
||||
|| custom_id
|
||||
== mode,
|
||||
);
|
||||
} else {
|
||||
format!(
|
||||
b.custom_id(format!(
|
||||
"{}{}",
|
||||
custom_id
|
||||
.split('#')
|
||||
.next()
|
||||
.unwrap(),
|
||||
mode
|
||||
)
|
||||
},
|
||||
)
|
||||
.label(button.label.clone().unwrap())
|
||||
.emoji(button.emoji.clone().unwrap())
|
||||
.disabled(
|
||||
custom_id == "#mode"
|
||||
|| custom_id == mode,
|
||||
)
|
||||
.style(*style),
|
||||
),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
b.style(button.style);
|
||||
|
||||
b
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
c.push(CreateActionRow::Buttons(row));
|
||||
c.add_action_row(a);
|
||||
}
|
||||
c
|
||||
};
|
||||
|
||||
let response =
|
||||
EditInteractionResponse::default().components(components);
|
||||
|
||||
component.edit_response(&ctx, response).await.unwrap();
|
||||
d.set_components(c)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
id_mode => {
|
||||
component.defer(&ctx).await.unwrap();
|
||||
component
|
||||
.create_interaction_response(ctx, |r| {
|
||||
r.kind(InteractionResponseType::DeferredUpdateMessage)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut it = id_mode.split('#');
|
||||
let id = it.next().unwrap();
|
||||
let mode = it.next().unwrap_or("instant");
|
||||
|
||||
let guild =
|
||||
guild_id.to_guild_cached(&ctx).map(|g| g.clone()).unwrap();
|
||||
|
||||
play_from_query(
|
||||
&ctx,
|
||||
&data,
|
||||
&guild,
|
||||
guild_id.to_guild_cached(&ctx).unwrap(),
|
||||
component.user.id,
|
||||
None,
|
||||
id.split('#').next().unwrap(),
|
||||
|
65
src/main.rs
65
src/main.rs
@ -5,8 +5,6 @@ mod cmds;
|
||||
mod consts;
|
||||
mod error;
|
||||
mod event_handlers;
|
||||
#[cfg(feature = "metrics")]
|
||||
mod metrics;
|
||||
mod models;
|
||||
mod utils;
|
||||
|
||||
@ -14,11 +12,11 @@ use std::{env, path::Path, sync::Arc};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use poise::serenity_prelude::{
|
||||
builder::CreateApplicationCommands,
|
||||
model::{
|
||||
gateway::GatewayIntents,
|
||||
id::{GuildId, UserId},
|
||||
},
|
||||
ActivityData, ClientBuilder,
|
||||
};
|
||||
use songbird::SerenityInit;
|
||||
use sqlx::{MySql, Pool};
|
||||
@ -30,6 +28,7 @@ type Database = MySql;
|
||||
|
||||
pub struct Data {
|
||||
database: Pool<Database>,
|
||||
http: reqwest::Client,
|
||||
guild_data_cache: DashMap<GuildId, Arc<RwLock<GuildData>>>,
|
||||
join_sound_cache: DashMap<UserId, DashMap<Option<GuildId>, Option<u32>>>,
|
||||
}
|
||||
@ -37,6 +36,36 @@ pub struct Data {
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
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 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);
|
||||
}
|
||||
if let Some(context_menu_command) = command.create_as_context_menu_command() {
|
||||
commands_builder.add_application_command(context_menu_command);
|
||||
}
|
||||
}
|
||||
let commands_builder = poise::serenity_prelude::json::Value::Array(commands_builder.0);
|
||||
|
||||
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?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if Path::new("/etc/soundfx-rs/config.env").exists() {
|
||||
@ -56,7 +85,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
cmds::manage::download_file(),
|
||||
cmds::manage::delete_sound(),
|
||||
cmds::play::play(),
|
||||
cmds::play::play_random(),
|
||||
cmds::play::queue_play(),
|
||||
cmds::play::loop_play(),
|
||||
cmds::play::soundboard(),
|
||||
@ -75,6 +103,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
],
|
||||
..cmds::favorite::favorites()
|
||||
},
|
||||
cmds::search::show_random_sounds(),
|
||||
cmds::search::search_sounds(),
|
||||
cmds::stop::stop_playing(),
|
||||
cmds::stop::disconnect(),
|
||||
@ -113,18 +142,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
|
||||
sqlx::migrate!().run(&database).await?;
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
metrics::init_metrics();
|
||||
tokio::spawn(async { metrics::serve().await });
|
||||
}
|
||||
|
||||
let framework = poise::Framework::builder()
|
||||
poise::Framework::builder()
|
||||
.token(discord_token)
|
||||
.setup(move |ctx, _bot, framework| {
|
||||
Box::pin(async move {
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
register_application_commands(ctx, framework, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(Data {
|
||||
http: reqwest::Client::new(),
|
||||
database,
|
||||
guild_data_cache: Default::default(),
|
||||
join_sound_cache: Default::default(),
|
||||
@ -132,18 +159,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
})
|
||||
})
|
||||
.options(options)
|
||||
.build();
|
||||
|
||||
let mut client = ClientBuilder::new(
|
||||
&discord_token,
|
||||
GatewayIntents::GUILD_VOICE_STATES | GatewayIntents::GUILDS,
|
||||
)
|
||||
.activity(ActivityData::watching("for /play"))
|
||||
.framework(framework)
|
||||
.register_songbird()
|
||||
.client_settings(move |client_builder| client_builder.register_songbird())
|
||||
.intents(GatewayIntents::GUILD_VOICE_STATES | GatewayIntents::GUILDS)
|
||||
.run_autosharded()
|
||||
.await?;
|
||||
|
||||
client.start_autosharded().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,46 +0,0 @@
|
||||
use axum::{routing::get, Router};
|
||||
use lazy_static;
|
||||
use log::warn;
|
||||
use prometheus::{register_int_counter, IntCounter, Registry};
|
||||
|
||||
lazy_static! {
|
||||
static ref REGISTRY: Registry = Registry::new();
|
||||
pub static ref PLAY_COUNTER: IntCounter =
|
||||
register_int_counter!("play_cmd", "Number of calls to /play").unwrap();
|
||||
pub static ref UPLOAD_COUNTER: IntCounter =
|
||||
register_int_counter!("upload_cmd", "Number of calls to /upload").unwrap();
|
||||
pub static ref DELETE_COUNTER: IntCounter =
|
||||
register_int_counter!("delete_cmd", "Number of calls to /delete").unwrap();
|
||||
pub static ref GREET_COUNTER: IntCounter =
|
||||
register_int_counter!("greet_invoke", "Number of greet sounds played").unwrap();
|
||||
}
|
||||
|
||||
pub fn init_metrics() {
|
||||
REGISTRY.register(Box::new(PLAY_COUNTER.clone())).unwrap();
|
||||
REGISTRY.register(Box::new(UPLOAD_COUNTER.clone())).unwrap();
|
||||
REGISTRY.register(Box::new(DELETE_COUNTER.clone())).unwrap();
|
||||
REGISTRY.register(Box::new(GREET_COUNTER.clone())).unwrap();
|
||||
}
|
||||
|
||||
pub async fn serve() {
|
||||
let app = Router::new().route("/metrics", get(metrics));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("localhost:31755")
|
||||
.await
|
||||
.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
async fn metrics() -> String {
|
||||
let encoder = prometheus::TextEncoder::new();
|
||||
let res_custom = encoder.encode_to_string(®ISTRY.gather());
|
||||
|
||||
match res_custom {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
warn!("Error encoding metrics: {:?}", e);
|
||||
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
@ -78,10 +78,12 @@ 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.get()
|
||||
WHERE id = ?
|
||||
",
|
||||
guild_id.as_u64()
|
||||
)
|
||||
.fetch_one(db_pool)
|
||||
.await;
|
||||
@ -102,15 +104,17 @@ impl GuildData {
|
||||
let guild_id = guild_id.into();
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO servers (id)
|
||||
VALUES (?)",
|
||||
guild_id.get()
|
||||
"
|
||||
INSERT INTO servers (id)
|
||||
VALUES (?)
|
||||
",
|
||||
guild_id.as_u64()
|
||||
)
|
||||
.execute(db_pool)
|
||||
.await?;
|
||||
|
||||
Ok(GuildData {
|
||||
id: guild_id.get(),
|
||||
id: guild_id.as_u64().to_owned(),
|
||||
prefix: String::from("?"),
|
||||
volume: 100,
|
||||
allow_greets: AllowGreet::Enabled,
|
||||
|
@ -1,5 +1,4 @@
|
||||
use poise::serenity_prelude::{async_trait, model::id::UserId, GuildId};
|
||||
use sqlx::Acquire;
|
||||
|
||||
use crate::Data;
|
||||
|
||||
@ -48,13 +47,14 @@ impl JoinSoundCtx for Data {
|
||||
sqlx::query_as!(
|
||||
JoinSound,
|
||||
"
|
||||
SELECT join_sound_id
|
||||
SELECT join_sound_id
|
||||
FROM join_sounds
|
||||
WHERE user = ?
|
||||
AND guild = ?
|
||||
ORDER BY guild IS NULL",
|
||||
user_id.get(),
|
||||
guild_id.map(|g| g.get())
|
||||
ORDER BY guild IS NULL
|
||||
",
|
||||
user_id.as_u64(),
|
||||
guild_id.map(|g| g.0)
|
||||
)
|
||||
.fetch_one(&self.database)
|
||||
.await
|
||||
@ -62,13 +62,14 @@ impl JoinSoundCtx for Data {
|
||||
sqlx::query_as!(
|
||||
JoinSound,
|
||||
"
|
||||
SELECT join_sound_id
|
||||
SELECT join_sound_id
|
||||
FROM join_sounds
|
||||
WHERE user = ?
|
||||
AND (guild IS NULL OR guild = ?)
|
||||
ORDER BY guild IS NULL",
|
||||
user_id.get(),
|
||||
guild_id.map(|g| g.get())
|
||||
ORDER BY guild IS NULL
|
||||
",
|
||||
user_id.as_u64(),
|
||||
guild_id.map(|g| g.0)
|
||||
)
|
||||
.fetch_one(&self.database)
|
||||
.await
|
||||
@ -110,29 +111,29 @@ impl JoinSoundCtx for Data {
|
||||
Some(join_id) => {
|
||||
sqlx::query!(
|
||||
"DELETE FROM join_sounds WHERE user = ? AND guild <=> ?",
|
||||
user_id.get(),
|
||||
guild_id.map(|g| g.get())
|
||||
user_id.0,
|
||||
guild_id.map(|g| g.0)
|
||||
)
|
||||
.execute(transaction.acquire().await?)
|
||||
.execute(&mut transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO join_sounds (user, join_sound_id, guild) VALUES (?, ?, ?)",
|
||||
user_id.get(),
|
||||
user_id.0,
|
||||
join_id,
|
||||
guild_id.map(|g| g.get())
|
||||
guild_id.map(|g| g.0)
|
||||
)
|
||||
.execute(transaction.acquire().await?)
|
||||
.execute(&mut transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
None => {
|
||||
sqlx::query!(
|
||||
"DELETE FROM join_sounds WHERE user = ? AND guild <=> ?",
|
||||
user_id.get(),
|
||||
guild_id.map(|g| g.get())
|
||||
user_id.0,
|
||||
guild_id.map(|g| g.0)
|
||||
)
|
||||
.execute(transaction.acquire().await?)
|
||||
.execute(&mut transaction)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
use std::{env, path::Path};
|
||||
|
||||
use poise::serenity_prelude::async_trait;
|
||||
use songbird::input::Input;
|
||||
use songbird::input::restartable::Restartable;
|
||||
use sqlx::Executor;
|
||||
use tokio::process::Command;
|
||||
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
|
||||
|
||||
use crate::{consts::UPLOAD_MAX_SIZE, error::ErrorTypes, Data, Database};
|
||||
|
||||
@ -397,7 +399,7 @@ impl SoundCtx for Data {
|
||||
}
|
||||
|
||||
impl Sound {
|
||||
pub(crate) async fn src(&self, db_pool: impl Executor<'_, Database = Database>) -> Vec<u8> {
|
||||
async fn src(&self, db_pool: impl Executor<'_, Database = Database>) -> Vec<u8> {
|
||||
struct Src {
|
||||
src: Vec<u8>,
|
||||
}
|
||||
@ -418,11 +420,33 @@ impl Sound {
|
||||
record.src
|
||||
}
|
||||
|
||||
pub async fn store_sound_source(
|
||||
&self,
|
||||
db_pool: impl Executor<'_, Database = Database>,
|
||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let caching_location = env::var("CACHING_LOCATION").unwrap_or(String::from("/tmp"));
|
||||
|
||||
let path_name = format!("{}/sound-{}", caching_location, self.id);
|
||||
let path = Path::new(&path_name);
|
||||
|
||||
if !path.exists() {
|
||||
let mut file = File::create(&path).await?;
|
||||
|
||||
file.write_all(&self.src(db_pool).await).await?;
|
||||
}
|
||||
|
||||
Ok(path_name)
|
||||
}
|
||||
|
||||
pub async fn playable(
|
||||
&self,
|
||||
db_pool: impl Executor<'_, Database = Database>,
|
||||
) -> Result<Input, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(Input::from(self.src(db_pool).await))
|
||||
) -> Result<Restartable, 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!"))
|
||||
}
|
||||
|
||||
pub async fn count_user_sounds<U: Into<u64>>(
|
||||
|
92
src/utils.rs
92
src/utils.rs
@ -1,13 +1,11 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use poise::serenity_prelude::{
|
||||
model::{
|
||||
use poise::serenity_prelude::model::{
|
||||
channel::Channel,
|
||||
guild::Guild,
|
||||
id::{ChannelId, UserId},
|
||||
},
|
||||
ChannelType, EditVoiceState, GuildId,
|
||||
};
|
||||
use songbird::{tracks::TrackHandle, Call};
|
||||
use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call};
|
||||
use sqlx::Executor;
|
||||
use tokio::sync::{Mutex, MutexGuard};
|
||||
|
||||
@ -26,18 +24,19 @@ 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 = sound.playable(db_pool).await?;
|
||||
let handle = call_handler.play_input(track);
|
||||
let (track, track_handler) = create_player(sound.playable(db_pool).await?.into());
|
||||
|
||||
handle.set_volume(volume as f32 / 100.0)?;
|
||||
let _ = track_handler.set_volume(volume as f32 / 100.0);
|
||||
|
||||
if r#loop {
|
||||
handle.enable_loop()?;
|
||||
let _ = track_handler.enable_loop();
|
||||
} else {
|
||||
handle.disable_loop()?;
|
||||
let _ = track_handler.disable_loop();
|
||||
}
|
||||
|
||||
Ok(handle)
|
||||
call_handler.play(track);
|
||||
|
||||
Ok(track_handler)
|
||||
}
|
||||
|
||||
pub async fn queue_audio(
|
||||
@ -47,10 +46,11 @@ 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 track = sound.playable(db_pool).await?;
|
||||
let handle = call_handler.enqueue_input(track).await;
|
||||
let (a, b) = create_player(sound.playable(db_pool).await?.into());
|
||||
|
||||
handle.set_volume(volume as f32 / 100.0)?;
|
||||
let _ = b.set_volume(volume as f32 / 100.0);
|
||||
|
||||
call_handler.enqueue(a);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -58,65 +58,60 @@ pub async fn queue_audio(
|
||||
|
||||
pub async fn join_channel(
|
||||
ctx: &poise::serenity_prelude::Context,
|
||||
guild_id: GuildId,
|
||||
guild: Guild,
|
||||
channel_id: ChannelId,
|
||||
) -> Result<Arc<Mutex<Call>>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
) -> (Arc<Mutex<Call>>, JoinResult<()>) {
|
||||
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 = ctx
|
||||
.cache
|
||||
.guild(guild_id)
|
||||
.map(|g| {
|
||||
g.voice_states
|
||||
let current_voice_state = guild
|
||||
.voice_states
|
||||
.get(¤t_user)
|
||||
.and_then(|voice_state| voice_state.channel_id)
|
||||
})
|
||||
.flatten();
|
||||
.and_then(|voice_state| voice_state.channel_id);
|
||||
|
||||
let call = if current_voice_state == Some(channel_id) {
|
||||
let call_opt = songbird.get(guild_id);
|
||||
let (call, res) = if current_voice_state == Some(channel_id) {
|
||||
let call_opt = songbird.get(guild.id);
|
||||
|
||||
if let Some(call) = call_opt {
|
||||
Ok(call)
|
||||
(call, Ok(()))
|
||||
} else {
|
||||
songbird.join(guild_id, channel_id).await
|
||||
let (call, res) = songbird.join(guild.id, channel_id).await;
|
||||
|
||||
(call, res)
|
||||
}
|
||||
} else {
|
||||
songbird.join(guild_id, channel_id).await
|
||||
}?;
|
||||
let (call, res) = songbird.join(guild.id, channel_id).await;
|
||||
|
||||
(call, res)
|
||||
};
|
||||
|
||||
{
|
||||
call.lock().await.deafen(true).await?;
|
||||
// set call to deafen
|
||||
let _ = call.lock().await.deafen(true).await;
|
||||
}
|
||||
|
||||
if let Some(channel) = ctx.cache.channel(channel_id).map(|c| c.clone()) {
|
||||
if channel.kind == ChannelType::Stage {
|
||||
let user_id = ctx.cache.current_user().id.clone();
|
||||
|
||||
channel
|
||||
.edit_voice_state(&ctx, user_id, EditVoiceState::new().suppress(true))
|
||||
.await?;
|
||||
}
|
||||
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
|
||||
let _ = channel
|
||||
.edit_voice_state(&ctx, ctx.cache.current_user(), |v| v.suppress(false))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(call)
|
||||
(call, res)
|
||||
}
|
||||
|
||||
pub async fn play_from_query(
|
||||
ctx: &poise::serenity_prelude::Context,
|
||||
data: &Data,
|
||||
guild: impl Deref<Target = Guild> + Send + Sync,
|
||||
guild: Guild,
|
||||
user_id: UserId,
|
||||
channel: Option<ChannelId>,
|
||||
query: &str,
|
||||
r#loop: bool,
|
||||
loop_: bool,
|
||||
) -> String {
|
||||
let guild_id = guild.deref().id;
|
||||
let guild_id = guild.id;
|
||||
|
||||
let channel_to_join = channel.or_else(|| {
|
||||
guild
|
||||
.deref()
|
||||
.voice_states
|
||||
.get(&user_id)
|
||||
.and_then(|voice_state| voice_state.channel_id)
|
||||
@ -134,7 +129,8 @@ pub async fn play_from_query(
|
||||
match sound_res {
|
||||
Some(sound) => {
|
||||
{
|
||||
let call_handler = join_channel(ctx, guild_id, user_channel).await.unwrap();
|
||||
let (call_handler, _) =
|
||||
join_channel(ctx, guild.clone(), user_channel).await;
|
||||
|
||||
let guild_data = data.guild_data(guild_id).await.unwrap();
|
||||
|
||||
@ -145,7 +141,7 @@ pub async fn play_from_query(
|
||||
guild_data.read().await.volume,
|
||||
&mut lock,
|
||||
&data.database,
|
||||
r#loop,
|
||||
loop_,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
Loading…
Reference in New Issue
Block a user