Merge pull request #3 from JellyWX/soundboard

Soundboard
This commit is contained in:
Jude Southworth 2021-11-21 10:42:05 +00:00 committed by GitHub
commit 53a8bb3127
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 439 additions and 677 deletions

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="SqlDialectMappings"> <component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/create.sql" dialect="GenericSQL" /> <file url="file://$PROJECT_DIR$/migrations/create.sql" dialect="GenericSQL" />
<file url="PROJECT" dialect="MySQL" /> <file url="PROJECT" dialect="MySQL" />
</component> </component>
</project> </project>

448
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "soundfx-rs" name = "soundfx-rs"
version = "1.4.0" version = "1.4.3"
authors = ["jellywx <judesouthworth@pm.me>"] authors = ["jellywx <judesouthworth@pm.me>"]
edition = "2018" edition = "2018"

View File

@ -0,0 +1,2 @@
ALTER TABLE servers ADD COLUMN allowed_role BIGINT;
ALTER TABLE servers DROP COLUMN name;

2
rustfmt.toml Normal file
View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View File

@ -1,15 +1,13 @@
use regex_command_attr::command; use std::{collections::HashMap, sync::Arc};
use regex_command_attr::command;
use serenity::{client::Context, framework::standard::CommandResult}; use serenity::{client::Context, framework::standard::CommandResult};
use crate::{ use crate::{
framework::{Args, CommandInvoke, CreateGenericResponse, RegexFramework}, framework::{Args, CommandInvoke, CommandKind, CreateGenericResponse, RegexFramework},
THEME_COLOR, THEME_COLOR,
}; };
use crate::framework::CommandKind;
use std::{collections::HashMap, sync::Arc};
#[command] #[command]
#[group("Information")] #[group("Information")]
#[description("Get information on the commands of the bot")] #[description("Get information on the commands of the bot")]
@ -147,7 +145,7 @@ pub async fn help(
CreateGenericResponse::new().embed(|e| { CreateGenericResponse::new().embed(|e| {
e.title("Invalid Command") e.title("Invalid Command")
.color(THEME_COLOR) .color(THEME_COLOR)
.description("Type `/help command` to view help about a command below:") .description("Type `/help command` to view more about a command below:")
.fields(groups_iter) .fields(groups_iter)
}), }),
) )
@ -173,7 +171,10 @@ pub async fn help(
CreateGenericResponse::new().embed(|e| { CreateGenericResponse::new().embed(|e| {
e.title("Help") e.title("Help")
.color(THEME_COLOR) .color(THEME_COLOR)
.description("Type `/help command` to view help about a command below:") .description("**Welcome to SoundFX!**
To get started, upload a sound with `/upload`, or use `/search` and `/play` to look at some of the public sounds
Type `/help command` to view help about a command below:")
.fields(groups_iter) .fields(groups_iter)
}), }),
) )

View File

@ -1,5 +1,6 @@
use regex_command_attr::command; use std::time::Duration;
use regex_command_attr::command;
use serenity::{ use serenity::{
client::Context, client::Context,
framework::standard::CommandResult, framework::standard::CommandResult,
@ -12,8 +13,6 @@ use crate::{
MySQL, MAX_SOUNDS, PATREON_GUILD, PATREON_ROLE, MySQL, MAX_SOUNDS, PATREON_GUILD, PATREON_ROLE,
}; };
use std::time::Duration;
#[command("upload")] #[command("upload")]
#[group("Manage")] #[group("Manage")]
#[description("Upload a new sound to the bot")] #[description("Upload a new sound to the bot")]
@ -156,7 +155,7 @@ pub async fn upload_new_sound(
invoke.respond( invoke.respond(
ctx.http.clone(), ctx.http.clone(),
CreateGenericResponse::new().content(format!( CreateGenericResponse::new().content(format!(
"You have reached the maximum number of sounds ({}). Either delete some with `?delete` or join our Patreon for unlimited uploads at **https://patreon.com/jellywx**", "You have reached the maximum number of sounds ({}). Either delete some with `/delete` or join our Patreon for unlimited uploads at **https://patreon.com/jellywx**",
*MAX_SOUNDS, *MAX_SOUNDS,
))).await?; ))).await?;
} }
@ -171,7 +170,7 @@ pub async fn upload_new_sound(
.await?; .await?;
} }
} else { } else {
invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("Usage: `?upload <name>`. Please ensure the name provided is less than 20 characters in length")).await?; invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("Usage: `/upload <name>`. Please ensure the name provided is less than 20 characters in length")).await?;
} }
Ok(()) Ok(())

View File

@ -1,12 +1,12 @@
use regex_command_attr::command; use std::{convert::TryFrom, time::Duration};
use regex_command_attr::command;
use serenity::{ use serenity::{
builder::CreateActionRow, builder::CreateActionRow,
client::Context, client::Context,
framework::standard::CommandResult, framework::standard::CommandResult,
model::interactions::{message_component::ButtonStyle, InteractionResponseType}, model::interactions::{message_component::ButtonStyle, InteractionResponseType},
}; };
use songbird::{ use songbird::{
create_player, ffmpeg, create_player, ffmpeg,
input::{cached::Memory, Input}, input::{cached::Memory, Input},
@ -22,8 +22,6 @@ use crate::{
AudioIndex, MySQL, AudioIndex, MySQL,
}; };
use std::{convert::TryFrom, time::Duration};
#[command] #[command]
#[aliases("p")] #[aliases("p")]
#[required_permissions(Managed)] #[required_permissions(Managed)]
@ -110,60 +108,78 @@ pub async fn play_ambience(
match channel_to_join { match channel_to_join {
Some(user_channel) => { Some(user_channel) => {
let search_name = args.named("name").unwrap().to_lowercase();
let audio_index = ctx.data.read().await.get::<AudioIndex>().cloned().unwrap(); let audio_index = ctx.data.read().await.get::<AudioIndex>().cloned().unwrap();
if let Some(filename) = audio_index.get(&search_name) { if let Some(search_name) = args.named("name") {
let (track, track_handler) = create_player( if let Some(filename) = audio_index.get(search_name) {
Input::try_from( let (track, track_handler) = create_player(
Memory::new(ffmpeg(format!("audio/{}", filename)).await.unwrap()).unwrap(), Input::try_from(
) Memory::new(ffmpeg(format!("audio/{}", filename)).await.unwrap())
.unwrap(), .unwrap(),
); )
.unwrap(),
);
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 = ctx.guild_data(guild).await.unwrap(); let guild_data = ctx.guild_data(guild).await.unwrap();
{ {
let mut lock = call_handler.lock().await; let mut lock = call_handler.lock().await;
lock.play(track); lock.play(track);
}
let _ = track_handler.set_volume(guild_data.read().await.volume as f32 / 100.0);
let _ = track_handler.add_event(
Event::Periodic(
track_handler.metadata().duration.unwrap() - Duration::from_millis(200),
None,
),
RestartTrack {},
);
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content(format!("Playing ambience **{}**", search_name)),
)
.await?;
} else {
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().embed(|e| {
e.title("Not Found").description(format!(
"Could not find ambience sound by name **{}**
__Available ambience sounds:__
{}",
search_name,
audio_index
.keys()
.into_iter()
.map(|i| i.as_str())
.collect::<Vec<&str>>()
.join("\n")
))
}),
)
.await?;
} }
let _ = track_handler.set_volume(guild_data.read().await.volume as f32 / 100.0);
let _ = track_handler.add_event(
Event::Periodic(
track_handler.metadata().duration.unwrap() - Duration::from_millis(200),
None,
),
RestartTrack {},
);
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content(format!("Playing ambience **{}**", search_name)),
)
.await?;
} else { } else {
invoke invoke
.respond( .respond(
ctx.http.clone(), ctx.http.clone(),
CreateGenericResponse::new().embed(|e| { CreateGenericResponse::new().embed(|e| {
e.title("Not Found").description(format!( e.title("Available Sounds").description(
"Could not find ambience sound by name **{}**
__Available ambience sounds:__
{}",
search_name,
audio_index audio_index
.keys() .keys()
.into_iter() .into_iter()
.map(|i| i.as_str()) .map(|i| i.as_str())
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.join("\n") .join("\n"),
)) )
}), }),
) )
.await?; .await?;
@ -374,7 +390,9 @@ pub async fn soundboard(
.await?; .await?;
if let Some(sound) = search.first() { if let Some(sound) = search.first() {
sounds.push(sound.clone()); if !sounds.contains(sound) {
sounds.push(sound.clone());
}
} }
} }

View File

@ -1,5 +1,4 @@
use regex_command_attr::command; use regex_command_attr::command;
use serenity::{client::Context, framework::standard::CommandResult}; use serenity::{client::Context, framework::standard::CommandResult};
use crate::{ use crate::{
@ -15,13 +14,7 @@ fn format_search_results(search_results: Vec<Sound>) -> CreateGenericResponse {
let field_iter = search_results let field_iter = search_results
.iter() .iter()
.take(25) .take(25)
.map(|item| { .map(|item| (&item.name, format!("ID: {}", item.id), true))
(
&item.name,
format!("ID: {}\nPlays: {}", item.id, item.plays),
true,
)
})
.filter(|item| { .filter(|item| {
current_character_count += item.0.len() + item.1.len(); current_character_count += item.0.len() + item.1.len();
@ -59,11 +52,11 @@ pub async fn list_sounds(
let mut message_buffer; let mut message_buffer;
if args.named("me").map(|i| i.to_owned()) == Some("me".to_string()) { if args.named("me").map(|i| i.to_owned()) == Some("me".to_string()) {
sounds = Sound::get_user_sounds(invoke.author_id(), pool).await?; sounds = Sound::user_sounds(invoke.author_id(), pool).await?;
message_buffer = "All your sounds: ".to_string(); message_buffer = "All your sounds: ".to_string();
} else { } else {
sounds = Sound::get_guild_sounds(invoke.guild_id().unwrap(), pool).await?; sounds = Sound::guild_sounds(invoke.guild_id().unwrap(), pool).await?;
message_buffer = "All sounds on this server: ".to_string(); message_buffer = "All sounds on this server: ".to_string();
} }
@ -142,42 +135,6 @@ pub async fn search_sounds(
Ok(()) Ok(())
} }
#[command("popular")]
#[group("Search")]
#[description("Show popular sounds")]
pub async fn show_popular_sounds(
ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send),
_args: Args,
) -> CommandResult {
let pool = ctx
.data
.read()
.await
.get::<MySQL>()
.cloned()
.expect("Could not get SQLPool from data");
let search_results = sqlx::query_as_unchecked!(
Sound,
"
SELECT name, id, plays, public, server_id, uploader_id
FROM sounds
WHERE public = 1
ORDER BY plays DESC
LIMIT 25
"
)
.fetch_all(&pool)
.await?;
invoke
.respond(ctx.http.clone(), format_search_results(search_results))
.await?;
Ok(())
}
#[command("random")] #[command("random")]
#[group("Search")] #[group("Search")]
#[description("Show a page of random sounds")] #[description("Show a page of random sounds")]
@ -197,7 +154,7 @@ pub async fn show_random_sounds(
let search_results = sqlx::query_as_unchecked!( let search_results = sqlx::query_as_unchecked!(
Sound, Sound,
" "
SELECT name, id, plays, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
WHERE public = 1 WHERE public = 1
ORDER BY rand() ORDER BY rand()

View File

@ -1,5 +1,4 @@
use regex_command_attr::command; use regex_command_attr::command;
use serenity::{client::Context, framework::standard::CommandResult}; use serenity::{client::Context, framework::standard::CommandResult};
use crate::{ use crate::{
@ -145,23 +144,21 @@ pub async fn change_prefix(
#[command("roles")] #[command("roles")]
#[required_permissions(Restricted)] #[required_permissions(Restricted)]
#[kind(Text)]
#[group("Settings")] #[group("Settings")]
#[description("Change the roles allowed to use the bot")] #[description("Change the role allowed to use the bot")]
#[arg( #[arg(
name = "roles", name = "role",
kind = "String", kind = "Role",
description = "The role mentions to enlist", description = "A role to allow to use the bot. Use @everyone to allow all server members",
required = true required = true
)] )]
#[example("`/roles @everyone` - allow all server members to use the bot")]
#[example("`/roles @DJ` - allow only server members with the 'DJ' role to use the bot")]
pub async fn set_allowed_roles( pub async fn set_allowed_roles(
ctx: &Context, ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send), invoke: &(dyn CommandInvoke + Sync + Send),
args: Args, args: Args,
) -> CommandResult { ) -> CommandResult {
let msg = invoke.msg().unwrap();
let guild_id = *msg.guild_id.unwrap().as_u64();
let pool = ctx let pool = ctx
.data .data
.read() .read()
@ -170,73 +167,19 @@ pub async fn set_allowed_roles(
.cloned() .cloned()
.expect("Could not get SQLPool from data"); .expect("Could not get SQLPool from data");
if args.is_empty() { let role_id = args.named("role").unwrap().parse::<u64>().unwrap();
let roles = sqlx::query!( let guild_data = ctx.guild_data(invoke.guild_id().unwrap()).await.unwrap();
"
SELECT role guild_data.write().await.allowed_role = Some(role_id);
FROM roles guild_data.read().await.commit(pool).await?;
WHERE guild_id = ?
", invoke
guild_id .respond(
ctx.http.clone(),
CreateGenericResponse::new().content(format!("Allowed role set to <@&{}>", role_id)),
) )
.fetch_all(&pool)
.await?; .await?;
let all_roles = roles
.iter()
.map(|i| format!("<@&{}>", i.role.to_string()))
.collect::<Vec<String>>()
.join(", ");
msg.channel_id.say(&ctx, format!("Usage: `?roles <role mentions or anything else to disable>`. Current roles: {}", all_roles)).await?;
} else {
sqlx::query!(
"
DELETE FROM roles
WHERE guild_id = ?
",
guild_id
)
.execute(&pool)
.await?;
if msg.mention_roles.len() > 0 {
for role in msg.mention_roles.iter().map(|r| *r.as_u64()) {
sqlx::query!(
"
INSERT INTO roles (guild_id, role)
VALUES
(?, ?)
",
guild_id,
role
)
.execute(&pool)
.await?;
}
msg.channel_id
.say(&ctx, "Specified roles whitelisted")
.await?;
} else {
sqlx::query!(
"
INSERT INTO roles (guild_id, role)
VALUES
(?, ?)
",
guild_id,
guild_id
)
.execute(&pool)
.await?;
msg.channel_id
.say(&ctx, "Role whitelisting disabled")
.await?;
}
}
Ok(()) Ok(())
} }

View File

@ -1,11 +1,9 @@
use regex_command_attr::command; use regex_command_attr::command;
use serenity::{client::Context, framework::standard::CommandResult}; use serenity::{client::Context, framework::standard::CommandResult};
use songbird;
use crate::framework::{Args, CommandInvoke, CreateGenericResponse}; use crate::framework::{Args, CommandInvoke, CreateGenericResponse};
use songbird;
#[command("stop")] #[command("stop")]
#[required_permissions(Managed)] #[required_permissions(Managed)]
#[group("Stop")] #[group("Stop")]

View File

@ -1,10 +1,4 @@
use crate::{ use std::{collections::HashMap, env};
framework::RegexFramework,
guild_data::CtxGuildData,
join_channel, play_audio, play_from_query,
sound::{JoinSoundCtx, Sound},
MySQL, ReqwestClient,
};
use serenity::{ use serenity::{
async_trait, async_trait,
@ -19,12 +13,15 @@ use serenity::{
}, },
utils::shard_id, utils::shard_id,
}; };
use songbird::{Event, EventContext, EventHandler as SongbirdEventHandler}; use songbird::{Event, EventContext, EventHandler as SongbirdEventHandler};
use crate::framework::Args; use crate::{
framework::{Args, RegexFramework},
use std::{collections::HashMap, env}; guild_data::CtxGuildData,
join_channel, play_audio, play_from_query,
sound::{JoinSoundCtx, Sound},
MySQL, ReqwestClient,
};
pub struct RestartTrack; pub struct RestartTrack;
@ -47,18 +44,6 @@ impl EventHandler for Handler {
ctx.set_activity(Activity::watching("for /play")).await; ctx.set_activity(Activity::watching("for /play")).await;
} }
async fn cache_ready(&self, ctx: Context, _: Vec<GuildId>) {
let framework = ctx
.data
.read()
.await
.get::<RegexFramework>()
.cloned()
.expect("RegexFramework not found in context");
framework.build_slash(ctx).await;
}
async fn guild_create(&self, ctx: Context, guild: Guild, is_new: bool) { async fn guild_create(&self, ctx: Context, guild: Guild, is_new: bool) {
if is_new { if is_new {
if let Ok(token) = env::var("DISCORDBOTS_TOKEN") { if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
@ -152,7 +137,7 @@ impl EventHandler for Handler {
let mut sound = sqlx::query_as_unchecked!( let mut sound = sqlx::query_as_unchecked!(
Sound, Sound,
" "
SELECT name, id, plays, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
WHERE id = ? WHERE id = ?
", ",

View File

@ -1,6 +1,16 @@
use std::{
collections::{HashMap, HashSet},
env, fmt,
hash::{Hash, Hasher},
sync::Arc,
};
use log::{debug, error, info, warn};
use regex::{Match, Regex, RegexBuilder};
use serde_json::Value;
use serenity::{ use serenity::{
async_trait, async_trait,
builder::CreateEmbed, builder::{CreateApplicationCommands, CreateComponents, CreateEmbed},
cache::Cache, cache::Cache,
client::Context, client::Context,
framework::{standard::CommandResult, Framework}, framework::{standard::CommandResult, Framework},
@ -9,7 +19,7 @@ use serenity::{
model::{ model::{
channel::{Channel, GuildChannel, Message}, channel::{Channel, GuildChannel, Message},
guild::{Guild, Member}, guild::{Guild, Member},
id::{ChannelId, GuildId, UserId}, id::{ChannelId, GuildId, RoleId, UserId},
interactions::{ interactions::{
application_command::{ application_command::{
ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType, ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType,
@ -21,20 +31,7 @@ use serenity::{
Result as SerenityResult, Result as SerenityResult,
}; };
use log::{debug, error, info, warn}; use crate::guild_data::CtxGuildData;
use regex::{Match, Regex, RegexBuilder};
use std::{
collections::{HashMap, HashSet},
env, fmt,
hash::{Hash, Hasher},
sync::Arc,
};
use crate::{guild_data::CtxGuildData, MySQL};
use serde_json::Value;
use serenity::builder::CreateComponents;
type CommandFn = for<'fut> fn( type CommandFn = for<'fut> fn(
&'fut Context, &'fut Context,
@ -74,10 +71,6 @@ impl Args {
Self { args } Self { args }
} }
pub fn is_empty(&self) -> bool {
self.args.is_empty()
}
pub fn named<D: ToString>(&self, name: D) -> Option<&String> { pub fn named<D: ToString>(&self, name: D) -> Option<&String> {
let name = name.to_string(); let name = name.to_string();
@ -397,42 +390,14 @@ impl Command {
} }
if self.required_permissions == PermissionLevel::Managed { if self.required_permissions == PermissionLevel::Managed {
let pool = ctx match ctx.guild_data(guild.id).await {
.data Ok(guild_data) => guild_data.read().await.allowed_role.map_or(true, |role| {
.read() role == guild.id.0 || {
.await let role_id = RoleId(role);
.get::<MySQL>()
.cloned()
.expect("Could not get SQLPool from data");
match sqlx::query!( member.roles.contains(&role_id)
"
SELECT role
FROM roles
WHERE guild_id = ?
",
guild.id.as_u64()
)
.fetch_all(&pool)
.await
{
Ok(rows) => {
let role_ids = member
.roles
.iter()
.map(|r| *r.as_u64())
.collect::<Vec<u64>>();
for row in rows {
if role_ids.contains(&row.role) || &row.role == guild.id.as_u64() {
return true;
}
} }
}),
false
}
Err(sqlx::Error::RowNotFound) => false,
Err(e) => { Err(e) => {
warn!("Unexpected error occurred querying roles: {:?}", e); warn!("Unexpected error occurred querying roles: {:?}", e);
@ -540,132 +505,55 @@ impl RegexFramework {
self self
} }
fn _populate_commands<'a>(
&self,
commands: &'a mut CreateApplicationCommands,
) -> &'a mut CreateApplicationCommands {
for command in &self.commands_ {
commands.create_application_command(|c| {
c.name(command.names[0]).description(command.desc);
for arg in command.args {
c.create_option(|o| {
o.name(arg.name)
.description(arg.description)
.kind(arg.kind)
.required(arg.required)
});
}
c
});
}
commands
}
pub async fn build_slash(&self, http: impl AsRef<Http>) { pub async fn build_slash(&self, http: impl AsRef<Http>) {
info!("Building slash commands..."); info!("Building slash commands...");
let mut count = 0; match env::var("TEST_GUILD")
.map(|i| i.parse::<u64>().ok())
if let Some(guild_id) = env::var("TEST_GUILD")
.map(|v| v.parse::<u64>().ok())
.ok() .ok()
.flatten() .flatten()
.map(|v| GuildId(v)) .map(|i| GuildId(i))
{ {
for command in self None => {
.commands_ ApplicationCommand::set_global_application_commands(&http, |c| {
.iter() self._populate_commands(c)
.filter(|c| c.kind != CommandKind::Text) })
{
guild_id
.create_application_command(&http, |a| {
a.name(command.names[0]).description(command.desc);
for arg in command.args {
a.create_option(|o| {
o.name(arg.name)
.description(arg.description)
.kind(arg.kind)
.required(arg.required)
});
}
a
})
.await
.expect(&format!(
"Failed to create application command for {}",
command.names[0]
));
count += 1;
}
} else {
info!("Checking for existing commands...");
let current_commands = ApplicationCommand::get_global_application_commands(&http)
.await .await
.expect("Failed to fetch existing commands"); .unwrap();
debug!("Existing commands: {:?}", current_commands);
// delete commands not in use
for command in &current_commands {
if self
.commands_
.iter()
.find(|c| c.names[0] == command.name)
.is_none()
{
info!("Deleting command {}", command.name);
ApplicationCommand::delete_global_application_command(&http, command.id)
.await
.expect("Failed to delete an unused command");
}
} }
Some(debug_guild) => {
for command in self debug_guild
.commands_ .set_application_commands(&http, |c| self._populate_commands(c))
.iter()
.filter(|c| c.kind != CommandKind::Text)
{
let already_created = if let Some(current_command) = current_commands
.iter()
.find(|curr| curr.name == command.names[0])
{
if current_command.description == command.desc
&& current_command.options.len() == command.args.len()
{
let mut has_different_arg = false;
for (arg, option) in
command.args.iter().zip(current_command.options.clone())
{
if arg.required != option.required
|| arg.name != option.name
|| arg.description != option.description
|| arg.kind != option.kind
{
has_different_arg = true;
break;
}
}
!has_different_arg
} else {
false
}
} else {
false
};
if !already_created {
ApplicationCommand::create_global_application_command(&http, |a| {
a.name(command.names[0]).description(command.desc);
for arg in command.args {
a.create_option(|o| {
o.name(arg.name)
.description(arg.description)
.kind(arg.kind)
.required(arg.required)
});
}
a
})
.await .await
.expect(&format!( .unwrap();
"Failed to create application command for {}",
command.names[0]
));
count += 1;
}
} }
} }
info!("{} slash commands built! Ready to go", count); info!("Slash commands built!");
} }
pub async fn execute(&self, ctx: Context, interaction: ApplicationCommandInteraction) { pub async fn execute(&self, ctx: Context, interaction: ApplicationCommandInteraction) {
@ -709,6 +597,14 @@ impl RegexFramework {
); );
} }
info!(
"[Shard {}] [Guild {}] /{} {:?}",
ctx.shard_id,
interaction.guild_id.unwrap(),
interaction.data.name,
args
);
(command.fun)(&ctx, &interaction, Args { args }) (command.fun)(&ctx, &interaction, Args { args })
.await .await
.unwrap(); .unwrap();
@ -716,7 +612,7 @@ impl RegexFramework {
let _ = interaction let _ = interaction
.respond( .respond(
ctx.http.clone(), ctx.http.clone(),
CreateGenericResponse::new().content("You must either be an Admin or have a role specified in `?roles` to do this command") CreateGenericResponse::new().content("You must either be an Admin or have a role specified by `/roles` to do this command")
) )
.await; .await;
} else if command.required_permissions == PermissionLevel::Restricted { } else if command.required_permissions == PermissionLevel::Restricted {
@ -794,6 +690,14 @@ impl Framework for RegexFramework {
let member = guild.member(&ctx, &msg.author).await.unwrap(); let member = guild.member(&ctx, &msg.author).await.unwrap();
if command.check_permissions(&ctx, &guild, &member).await { if command.check_permissions(&ctx, &guild, &member).await {
let _ = msg.channel_id.say(
&ctx,
format!(
"You **must** begin to switch to slash commands. All commands are available via slash commands now. If slash commands don't display in your server, please use this link: https://discord.com/api/oauth2/authorize?client_id={}&permissions=3165184&scope=applications.commands%20bot",
ctx.cache.current_user().id
)
).await;
(command.fun)(&ctx, &msg, Args::from(&args, command.args)) (command.fun)(&ctx, &msg, Args::from(&args, command.args))
.await .await
.unwrap(); .unwrap();

View File

@ -1,15 +1,18 @@
use crate::{GuildDataCache, MySQL}; use std::sync::Arc;
use serenity::{async_trait, model::id::GuildId, prelude::Context}; use serenity::{async_trait, model::id::GuildId, prelude::Context};
use sqlx::mysql::MySqlPool; use sqlx::mysql::MySqlPool;
use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::{GuildDataCache, MySQL};
#[derive(Clone)] #[derive(Clone)]
pub struct GuildData { pub struct GuildData {
pub id: u64, pub id: u64,
pub prefix: String, pub prefix: String,
pub volume: u8, pub volume: u8,
pub allow_greets: bool, pub allow_greets: bool,
pub allowed_role: Option<u64>,
} }
#[async_trait] #[async_trait]
@ -41,7 +44,7 @@ impl CtxGuildData for Context {
} else { } else {
let pool = self.data.read().await.get::<MySQL>().cloned().unwrap(); let pool = self.data.read().await.get::<MySQL>().cloned().unwrap();
match GuildData::get_from_id(guild_id, pool).await { match GuildData::from_id(guild_id, pool).await {
Ok(d) => { Ok(d) => {
let lock = Arc::new(RwLock::new(d)); let lock = Arc::new(RwLock::new(d));
@ -59,7 +62,7 @@ impl CtxGuildData for Context {
} }
impl GuildData { impl GuildData {
pub async fn get_from_id<G: Into<GuildId>>( pub async fn from_id<G: Into<GuildId>>(
guild_id: G, guild_id: G,
db_pool: MySqlPool, db_pool: MySqlPool,
) -> Result<GuildData, sqlx::Error> { ) -> Result<GuildData, sqlx::Error> {
@ -68,7 +71,7 @@ impl GuildData {
let guild_data = sqlx::query_as_unchecked!( let guild_data = sqlx::query_as_unchecked!(
GuildData, GuildData,
" "
SELECT id, prefix, volume, allow_greets SELECT id, prefix, volume, allow_greets, allowed_role
FROM servers FROM servers
WHERE id = ? WHERE id = ?
", ",
@ -102,22 +105,12 @@ INSERT INTO servers (id)
.execute(&db_pool) .execute(&db_pool)
.await?; .await?;
sqlx::query!(
"
INSERT IGNORE INTO roles (guild_id, role)
VALUES (?, ?)
",
guild_id.as_u64(),
guild_id.as_u64()
)
.execute(&db_pool)
.await?;
Ok(GuildData { Ok(GuildData {
id: guild_id.as_u64().to_owned(), id: guild_id.as_u64().to_owned(),
prefix: String::from("?"), prefix: String::from("?"),
volume: 100, volume: 100,
allow_greets: true, allow_greets: true,
allowed_role: None,
}) })
} }
@ -131,13 +124,15 @@ UPDATE servers
SET SET
prefix = ?, prefix = ?,
volume = ?, volume = ?,
allow_greets = ? allow_greets = ?,
allowed_role = ?
WHERE WHERE
id = ? id = ?
", ",
self.prefix, self.prefix,
self.volume, self.volume,
self.allow_greets, self.allow_greets,
self.allowed_role,
self.id self.id
) )
.execute(&db_pool) .execute(&db_pool)

View File

@ -8,15 +8,11 @@ mod framework;
mod guild_data; mod guild_data;
mod sound; mod sound;
use crate::{ use std::{collections::HashMap, env, sync::Arc};
event_handlers::Handler,
framework::{Args, RegexFramework},
guild_data::{CtxGuildData, GuildData},
sound::Sound,
};
use dashmap::DashMap;
use dotenv::dotenv;
use log::info; use log::info;
use serenity::{ use serenity::{
client::{bridge::gateway::GatewayIntents, Client, Context}, client::{bridge::gateway::GatewayIntents, Client, Context},
http::Http, http::Http,
@ -27,19 +23,17 @@ use serenity::{
}, },
prelude::{Mutex, TypeMapKey}, prelude::{Mutex, TypeMapKey},
}; };
use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call, SerenityInit}; use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call, SerenityInit};
use sqlx::mysql::MySqlPool; use sqlx::mysql::MySqlPool;
use dotenv::dotenv;
use dashmap::DashMap;
use std::{collections::HashMap, env, sync::Arc};
use tokio::sync::{MutexGuard, RwLock}; use tokio::sync::{MutexGuard, RwLock};
use crate::{
event_handlers::Handler,
framework::{Args, RegexFramework},
guild_data::{CtxGuildData, GuildData},
sound::Sound,
};
struct MySQL; struct MySQL;
impl TypeMapKey for MySQL { impl TypeMapKey for MySQL {
@ -98,9 +92,6 @@ async fn play_audio(
call_handler.play(track); call_handler.play(track);
sound.plays += 1;
sound.commit(mysql_pool).await?;
Ok(track_handler) Ok(track_handler)
} }
@ -268,7 +259,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// search commands // search commands
.add_command(&cmds::search::LIST_SOUNDS_COMMAND) .add_command(&cmds::search::LIST_SOUNDS_COMMAND)
.add_command(&cmds::search::SEARCH_SOUNDS_COMMAND) .add_command(&cmds::search::SEARCH_SOUNDS_COMMAND)
.add_command(&cmds::search::SHOW_POPULAR_SOUNDS_COMMAND)
.add_command(&cmds::search::SHOW_RANDOM_SOUNDS_COMMAND); .add_command(&cmds::search::SHOW_RANDOM_SOUNDS_COMMAND);
if audio_index.is_some() { if audio_index.is_some() {
@ -314,6 +304,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
} }
} }
framework_arc.build_slash(&client.cache_and_http.http).await;
if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| { if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| {
let mut split = sr let mut split = sr
.split(',') .split(',')

View File

@ -1,15 +1,12 @@
use super::error::ErrorTypes;
use sqlx::mysql::MySqlPool;
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
use songbird::input::restartable::Restartable;
use std::{env, path::Path}; use std::{env, path::Path};
use crate::{JoinSoundCache, MySQL};
use serenity::{async_trait, model::id::UserId, prelude::Context}; use serenity::{async_trait, model::id::UserId, prelude::Context};
use songbird::input::restartable::Restartable;
use sqlx::mysql::MySqlPool;
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
use super::error::ErrorTypes;
use crate::{JoinSoundCache, MySQL};
#[async_trait] #[async_trait]
pub trait JoinSoundCtx { pub trait JoinSoundCtx {
@ -83,17 +80,15 @@ SELECT join_sound_id
let pool = self.data.read().await.get::<MySQL>().cloned().unwrap(); let pool = self.data.read().await.get::<MySQL>().cloned().unwrap();
if join_sound_cache.get(&user_id).is_none() { let _ = sqlx::query!(
let _ = sqlx::query!( "
"
INSERT IGNORE INTO users (user) INSERT IGNORE INTO users (user)
VALUES (?) VALUES (?)
", ",
user_id.as_u64() user_id.as_u64()
) )
.execute(&pool) .execute(&pool)
.await; .await;
}
let _ = sqlx::query!( let _ = sqlx::query!(
" "
@ -115,12 +110,17 @@ WHERE
pub struct Sound { pub struct Sound {
pub name: String, pub name: String,
pub id: u32, pub id: u32,
pub plays: u32,
pub public: bool, pub public: bool,
pub server_id: u64, pub server_id: u64,
pub uploader_id: Option<u64>, pub uploader_id: Option<u64>,
} }
impl PartialEq for Sound {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Sound { impl Sound {
pub async fn search_for_sound<G: Into<u64>, U: Into<u64>>( pub async fn search_for_sound<G: Into<u64>, U: Into<u64>>(
query: &str, query: &str,
@ -150,7 +150,7 @@ impl Sound {
let sound = sqlx::query_as_unchecked!( let sound = sqlx::query_as_unchecked!(
Self, Self,
" "
SELECT name, id, plays, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
WHERE id = ? AND ( WHERE id = ? AND (
public = 1 OR public = 1 OR
@ -174,7 +174,7 @@ SELECT name, id, plays, public, server_id, uploader_id
sound = sqlx::query_as_unchecked!( sound = sqlx::query_as_unchecked!(
Self, Self,
" "
SELECT name, id, plays, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
WHERE name = ? AND ( WHERE name = ? AND (
public = 1 OR public = 1 OR
@ -195,7 +195,7 @@ SELECT name, id, plays, public, server_id, uploader_id
sound = sqlx::query_as_unchecked!( sound = sqlx::query_as_unchecked!(
Self, Self,
" "
SELECT name, id, plays, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
WHERE name LIKE CONCAT('%', ?, '%') AND ( WHERE name LIKE CONCAT('%', ?, '%') AND (
public = 1 OR public = 1 OR
@ -310,12 +310,10 @@ SELECT COUNT(1) as count
" "
UPDATE sounds UPDATE sounds
SET SET
plays = ?,
public = ? public = ?
WHERE WHERE
id = ? id = ?
", ",
self.plays,
self.public, self.public,
self.id self.id
) )
@ -407,14 +405,14 @@ INSERT INTO sounds (name, server_id, uploader_id, public, src)
} }
} }
pub async fn get_user_sounds<U: Into<u64>>( pub async fn user_sounds<U: Into<u64>>(
user_id: U, user_id: U,
db_pool: MySqlPool, db_pool: MySqlPool,
) -> Result<Vec<Sound>, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Vec<Sound>, Box<dyn std::error::Error + Send + Sync>> {
let sounds = sqlx::query_as_unchecked!( let sounds = sqlx::query_as_unchecked!(
Sound, Sound,
" "
SELECT name, id, plays, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
WHERE uploader_id = ? WHERE uploader_id = ?
", ",
@ -426,14 +424,14 @@ SELECT name, id, plays, public, server_id, uploader_id
Ok(sounds) Ok(sounds)
} }
pub async fn get_guild_sounds<G: Into<u64>>( pub async fn guild_sounds<G: Into<u64>>(
guild_id: G, guild_id: G,
db_pool: MySqlPool, db_pool: MySqlPool,
) -> Result<Vec<Sound>, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Vec<Sound>, Box<dyn std::error::Error + Send + Sync>> {
let sounds = sqlx::query_as_unchecked!( let sounds = sqlx::query_as_unchecked!(
Sound, Sound,
" "
SELECT name, id, plays, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
WHERE server_id = ? WHERE server_id = ?
", ",