restructured a lot. everything updated to poise.
This commit is contained in:
parent
bb54c0d2c0
commit
b350007dae
@ -1,4 +1,4 @@
|
||||
use crate::{Context, Error, THEME_COLOR};
|
||||
use crate::{consts::THEME_COLOR, Context, Error};
|
||||
|
||||
/// Get additional information about the bot
|
||||
#[poise::command(slash_command, category = "Information")]
|
||||
|
@ -1,8 +1,14 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use poise::serenity::model::id::{GuildId, RoleId};
|
||||
use tokio::fs::File;
|
||||
|
||||
use crate::{sound::Sound, Context, Error, MAX_SOUNDS, PATREON_GUILD, PATREON_ROLE};
|
||||
use crate::{
|
||||
cmds::autocomplete_sound,
|
||||
consts::{MAX_SOUNDS, PATREON_GUILD, PATREON_ROLE},
|
||||
models::sound::{Sound, SoundCtx},
|
||||
Context, Error,
|
||||
};
|
||||
|
||||
/// Upload a new sound to the bot
|
||||
#[poise::command(slash_command, rename = "upload", category = "Manage")]
|
||||
@ -123,14 +129,16 @@ pub async fn upload_new_sound(
|
||||
#[poise::command(slash_command, rename = "delete", category = "Manage")]
|
||||
pub async fn delete_sound(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Name or ID of sound to delete"] name: String,
|
||||
#[description = "Name or ID of sound to delete"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
let pool = ctx.data().database.clone();
|
||||
|
||||
let uid = ctx.author().id.0;
|
||||
let gid = ctx.guild_id().unwrap().0;
|
||||
|
||||
let sound_vec = Sound::search_for_sound(&name, gid, uid, pool.clone(), true).await?;
|
||||
let sound_vec = ctx.data().search_for_sound(&name, gid, uid, true).await?;
|
||||
let sound_result = sound_vec.first();
|
||||
|
||||
match sound_result {
|
||||
@ -174,14 +182,16 @@ pub async fn delete_sound(
|
||||
#[poise::command(slash_command, rename = "public", category = "Manage")]
|
||||
pub async fn change_public(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Name or ID of sound to change privacy setting of"] name: String,
|
||||
#[description = "Name or ID of sound to change privacy setting of"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
let pool = ctx.data().database.clone();
|
||||
|
||||
let uid = ctx.author().id.0;
|
||||
let gid = ctx.guild_id().unwrap().0;
|
||||
|
||||
let mut sound_vec = Sound::search_for_sound(&name, gid, uid, pool.clone(), true).await?;
|
||||
let mut sound_vec = ctx.data().search_for_sound(&name, gid, uid, true).await?;
|
||||
let sound_result = sound_vec.first_mut();
|
||||
|
||||
match sound_result {
|
||||
@ -210,3 +220,39 @@ pub async fn change_public(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Download a sound file from the bot
|
||||
#[poise::command(slash_command, rename = "download", category = "Manage")]
|
||||
pub async fn download_file(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Name or ID of sound to download"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
|
||||
let sound = ctx
|
||||
.data()
|
||||
.search_for_sound(&name, ctx.guild_id().unwrap(), ctx.author().id, true)
|
||||
.await?;
|
||||
|
||||
match sound.first() {
|
||||
Some(sound) => {
|
||||
let source = sound
|
||||
.store_sound_source(ctx.data().database.clone())
|
||||
.await?;
|
||||
|
||||
let file = File::open(&source).await?;
|
||||
let name = format!("{}-{}.opus", sound.id, sound.name);
|
||||
|
||||
ctx.send(|m| m.attachment((&file, name.as_str()).into()))
|
||||
.await?;
|
||||
}
|
||||
|
||||
None => {
|
||||
ctx.say("No sound found by specified name/ID").await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,6 +1,24 @@
|
||||
use crate::{models::sound::SoundCtx, Context};
|
||||
|
||||
pub mod info;
|
||||
pub mod manage;
|
||||
pub mod play;
|
||||
// pub mod search;
|
||||
// pub mod settings;
|
||||
// pub mod stop;
|
||||
pub mod search;
|
||||
pub mod settings;
|
||||
pub mod stop;
|
||||
|
||||
pub async fn autocomplete_sound(
|
||||
ctx: Context<'_>,
|
||||
partial: String,
|
||||
) -> Vec<poise::AutocompleteChoice<String>> {
|
||||
ctx.data()
|
||||
.autocomplete_user_sounds(&partial, ctx.author().id, ctx.guild_id().unwrap())
|
||||
.await
|
||||
.unwrap_or(vec![])
|
||||
.iter()
|
||||
.map(|s| poise::AutocompleteChoice {
|
||||
name: s.name.clone(),
|
||||
value: s.id.to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
150
src/cmds/play.rs
150
src/cmds/play.rs
@ -2,31 +2,57 @@ use poise::serenity::{
|
||||
builder::CreateActionRow, model::interactions::message_component::ButtonStyle,
|
||||
};
|
||||
|
||||
use crate::{play_from_query, sound::Sound, Context, Error};
|
||||
use crate::{
|
||||
cmds::autocomplete_sound, models::sound::SoundCtx, utils::play_from_query, Context, Error,
|
||||
};
|
||||
|
||||
/// Play a sound in your current voice channel
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn play(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Name or ID of sound to play"] name: String,
|
||||
#[description = "Name or ID of sound to play"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
let guild = ctx.guild().unwrap();
|
||||
|
||||
ctx.say(play_from_query(&ctx, guild, ctx.author().id, &name, false).await)
|
||||
ctx.say(
|
||||
play_from_query(
|
||||
&ctx.discord(),
|
||||
&ctx.data(),
|
||||
guild,
|
||||
ctx.author().id,
|
||||
&name,
|
||||
false,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loop a sound in your current voice channel
|
||||
#[poise::command(slash_command)]
|
||||
#[poise::command(slash_command, rename = "loop")]
|
||||
pub async fn loop_play(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Name or ID of sound to loop"] name: String,
|
||||
#[description = "Name or ID of sound to loop"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
let guild = ctx.guild().unwrap();
|
||||
|
||||
ctx.say(play_from_query(&ctx, guild, ctx.author().id, &name, true).await)
|
||||
ctx.say(
|
||||
play_from_query(
|
||||
&ctx.discord(),
|
||||
&ctx.data(),
|
||||
guild,
|
||||
ctx.author().id,
|
||||
&name,
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@ -36,36 +62,84 @@ pub async fn loop_play(
|
||||
#[poise::command(slash_command, rename = "soundboard", category = "Play")]
|
||||
pub async fn soundboard(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Name or ID of sound for button 1"] sound_1: String,
|
||||
#[description = "Name or ID of sound for button 2"] sound_2: Option<String>,
|
||||
#[description = "Name or ID of sound for button 3"] sound_3: Option<String>,
|
||||
#[description = "Name or ID of sound for button 4"] sound_4: Option<String>,
|
||||
#[description = "Name or ID of sound for button 5"] sound_5: Option<String>,
|
||||
#[description = "Name or ID of sound for button 6"] sound_6: Option<String>,
|
||||
#[description = "Name or ID of sound for button 7"] sound_7: Option<String>,
|
||||
#[description = "Name or ID of sound for button 8"] sound_8: Option<String>,
|
||||
#[description = "Name or ID of sound for button 9"] sound_9: Option<String>,
|
||||
#[description = "Name or ID of sound for button 10"] sound_10: Option<String>,
|
||||
#[description = "Name or ID of sound for button 11"] sound_11: Option<String>,
|
||||
#[description = "Name or ID of sound for button 12"] sound_12: Option<String>,
|
||||
#[description = "Name or ID of sound for button 13"] sound_13: Option<String>,
|
||||
#[description = "Name or ID of sound for button 14"] sound_14: Option<String>,
|
||||
#[description = "Name or ID of sound for button 15"] sound_15: Option<String>,
|
||||
#[description = "Name or ID of sound for button 16"] sound_16: Option<String>,
|
||||
#[description = "Name or ID of sound for button 17"] sound_17: Option<String>,
|
||||
#[description = "Name or ID of sound for button 18"] sound_18: Option<String>,
|
||||
#[description = "Name or ID of sound for button 19"] sound_19: Option<String>,
|
||||
#[description = "Name or ID of sound for button 20"] sound_20: Option<String>,
|
||||
#[description = "Name or ID of sound for button 21"] sound_21: Option<String>,
|
||||
#[description = "Name or ID of sound for button 22"] sound_22: Option<String>,
|
||||
#[description = "Name or ID of sound for button 23"] sound_23: Option<String>,
|
||||
#[description = "Name or ID of sound for button 24"] sound_24: Option<String>,
|
||||
#[description = "Name or ID of sound for button 25"] sound_25: Option<String>,
|
||||
#[description = "Name or ID of sound for button 1"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_1: String,
|
||||
#[description = "Name or ID of sound for button 2"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_2: Option<String>,
|
||||
#[description = "Name or ID of sound for button 3"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_3: Option<String>,
|
||||
#[description = "Name or ID of sound for button 4"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_4: Option<String>,
|
||||
#[description = "Name or ID of sound for button 5"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_5: Option<String>,
|
||||
#[description = "Name or ID of sound for button 6"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_6: Option<String>,
|
||||
#[description = "Name or ID of sound for button 7"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_7: Option<String>,
|
||||
#[description = "Name or ID of sound for button 8"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_8: Option<String>,
|
||||
#[description = "Name or ID of sound for button 9"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_9: Option<String>,
|
||||
#[description = "Name or ID of sound for button 10"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_10: Option<String>,
|
||||
#[description = "Name or ID of sound for button 11"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_11: Option<String>,
|
||||
#[description = "Name or ID of sound for button 12"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_12: Option<String>,
|
||||
#[description = "Name or ID of sound for button 13"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_13: Option<String>,
|
||||
#[description = "Name or ID of sound for button 14"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_14: Option<String>,
|
||||
#[description = "Name or ID of sound for button 15"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_15: Option<String>,
|
||||
#[description = "Name or ID of sound for button 16"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_16: Option<String>,
|
||||
#[description = "Name or ID of sound for button 17"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_17: Option<String>,
|
||||
#[description = "Name or ID of sound for button 18"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_18: Option<String>,
|
||||
#[description = "Name or ID of sound for button 19"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_19: Option<String>,
|
||||
#[description = "Name or ID of sound for button 20"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_20: Option<String>,
|
||||
#[description = "Name or ID of sound for button 21"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_21: Option<String>,
|
||||
#[description = "Name or ID of sound for button 22"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_22: Option<String>,
|
||||
#[description = "Name or ID of sound for button 23"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_23: Option<String>,
|
||||
#[description = "Name or ID of sound for button 24"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_24: Option<String>,
|
||||
#[description = "Name or ID of sound for button 25"]
|
||||
#[autocomplete = "autocomplete_sound"]
|
||||
sound_25: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
|
||||
let pool = ctx.data().database.clone();
|
||||
|
||||
let query_terms = [
|
||||
Some(sound_1),
|
||||
sound_2,
|
||||
@ -97,13 +171,9 @@ pub async fn soundboard(
|
||||
let mut sounds = vec![];
|
||||
|
||||
for sound in query_terms.iter().flatten() {
|
||||
let search = Sound::search_for_sound(
|
||||
&sound,
|
||||
ctx.guild_id().unwrap(),
|
||||
ctx.author().id,
|
||||
pool.clone(),
|
||||
true,
|
||||
)
|
||||
let search = ctx
|
||||
.data()
|
||||
.search_for_sound(&sound, ctx.guild_id().unwrap(), ctx.author().id, true)
|
||||
.await?;
|
||||
|
||||
if let Some(sound) = search.first() {
|
||||
|
@ -1,6 +1,13 @@
|
||||
use crate::sound::Sound;
|
||||
use poise::{serenity::constants::MESSAGE_CODE_LIMIT, CreateReply};
|
||||
|
||||
use crate::{
|
||||
models::sound::{Sound, SoundCtx},
|
||||
Context, Error,
|
||||
};
|
||||
|
||||
fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> {
|
||||
let mut builder = CreateReply::default();
|
||||
|
||||
fn format_search_results(search_results: Vec<Sound>) -> CreateGenericResponse {
|
||||
let mut current_character_count = 0;
|
||||
let title = "Public sounds matching filter:";
|
||||
|
||||
@ -11,49 +18,25 @@ fn format_search_results(search_results: Vec<Sound>) -> CreateGenericResponse {
|
||||
.filter(|item| {
|
||||
current_character_count += item.0.len() + item.1.len();
|
||||
|
||||
current_character_count <= serenity::constants::MESSAGE_CODE_LIMIT - title.len()
|
||||
current_character_count <= MESSAGE_CODE_LIMIT - title.len()
|
||||
});
|
||||
|
||||
CreateGenericResponse::new().embed(|e| e.title(title).fields(field_iter))
|
||||
builder.embed(|e| e.title(title).fields(field_iter));
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
#[command("list")]
|
||||
#[group("Search")]
|
||||
#[description("Show the sounds uploaded by you or to your server")]
|
||||
#[arg(
|
||||
name = "me",
|
||||
description = "Whether to list your sounds or server sounds (default: server)",
|
||||
kind = "Boolean",
|
||||
required = false
|
||||
)]
|
||||
#[example("`/list` - list sounds uploaded to the server you're in")]
|
||||
#[example("`/list [me: True]` - list sounds you have uploaded across all servers")]
|
||||
pub async fn list_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");
|
||||
|
||||
/// Show the sounds uploaded to this server
|
||||
#[poise::command(slash_command, rename = "list")]
|
||||
pub async fn list_sounds(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let sounds;
|
||||
let mut message_buffer;
|
||||
|
||||
if args.named("me").map(|i| i.to_owned()) == Some("me".to_string()) {
|
||||
sounds = Sound::user_sounds(invoke.author_id(), pool).await?;
|
||||
sounds = ctx.data().guild_sounds(ctx.guild_id().unwrap()).await?;
|
||||
|
||||
message_buffer = "All your sounds: ".to_string();
|
||||
} else {
|
||||
sounds = Sound::guild_sounds(invoke.guild_id().unwrap(), pool).await?;
|
||||
|
||||
message_buffer = "All sounds on this server: ".to_string();
|
||||
}
|
||||
message_buffer = "Sounds on this server: ".to_string();
|
||||
|
||||
// todo change this to iterator
|
||||
for sound in sounds {
|
||||
message_buffer.push_str(
|
||||
format!(
|
||||
@ -65,85 +48,77 @@ pub async fn list_sounds(
|
||||
);
|
||||
|
||||
if message_buffer.len() > 2000 {
|
||||
invoke
|
||||
.respond(
|
||||
ctx.http.clone(),
|
||||
CreateGenericResponse::new().content(message_buffer),
|
||||
)
|
||||
.await?;
|
||||
ctx.say(message_buffer).await?;
|
||||
|
||||
message_buffer = "".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if message_buffer.len() > 0 {
|
||||
invoke
|
||||
.respond(
|
||||
ctx.http.clone(),
|
||||
CreateGenericResponse::new().content(message_buffer),
|
||||
)
|
||||
.await?;
|
||||
ctx.say(message_buffer).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command("search")]
|
||||
#[group("Search")]
|
||||
#[description("Search for sounds")]
|
||||
#[arg(
|
||||
name = "query",
|
||||
kind = "String",
|
||||
description = "Sound name to search for",
|
||||
required = true
|
||||
)]
|
||||
/// Show all sounds you have uploaded
|
||||
#[poise::command(slash_command, rename = "me")]
|
||||
pub async fn list_user_sounds(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let sounds;
|
||||
let mut message_buffer;
|
||||
|
||||
sounds = ctx.data().user_sounds(ctx.author().id).await?;
|
||||
|
||||
message_buffer = "Sounds on this server: ".to_string();
|
||||
|
||||
// todo change this to iterator
|
||||
for sound in sounds {
|
||||
message_buffer.push_str(
|
||||
format!(
|
||||
"**{}** ({}), ",
|
||||
sound.name,
|
||||
if sound.public { "🔓" } else { "🔒" }
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
if message_buffer.len() > 2000 {
|
||||
ctx.say(message_buffer).await?;
|
||||
|
||||
message_buffer = "".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if message_buffer.len() > 0 {
|
||||
ctx.say(message_buffer).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Search for sounds
|
||||
#[poise::command(slash_command, rename = "search", category = "Search")]
|
||||
pub async fn search_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 query = args.named("query").unwrap();
|
||||
|
||||
let search_results = Sound::search_for_sound(
|
||||
query,
|
||||
invoke.guild_id().unwrap(),
|
||||
invoke.author_id(),
|
||||
pool,
|
||||
false,
|
||||
)
|
||||
ctx: Context<'_>,
|
||||
#[description = "Sound name to search for"] query: String,
|
||||
) -> Result<(), Error> {
|
||||
let search_results = ctx
|
||||
.data()
|
||||
.search_for_sound(&query, ctx.guild_id().unwrap(), ctx.author().id, false)
|
||||
.await?;
|
||||
|
||||
invoke
|
||||
.respond(ctx.http.clone(), format_search_results(search_results))
|
||||
ctx.send(|m| {
|
||||
*m = format_search_results(search_results);
|
||||
m
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command("random")]
|
||||
#[group("Search")]
|
||||
#[description("Show a page of random sounds")]
|
||||
pub async fn show_random_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");
|
||||
|
||||
/// Show a page of random sounds
|
||||
#[poise::command(slash_command, rename = "random")]
|
||||
pub async fn show_random_sounds(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let search_results = sqlx::query_as_unchecked!(
|
||||
Sound,
|
||||
"
|
||||
@ -154,12 +129,13 @@ SELECT name, id, public, server_id, uploader_id
|
||||
LIMIT 25
|
||||
"
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
.fetch_all(&ctx.data().database)
|
||||
.await?;
|
||||
|
||||
invoke
|
||||
.respond(ctx.http.clone(), format_search_results(search_results))
|
||||
ctx.send(|m| {
|
||||
*m = format_search_results(search_results);
|
||||
m
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
|
@ -1,307 +1,126 @@
|
||||
use regex_command_attr::command;
|
||||
use serenity::{client::Context, framework::standard::CommandResult};
|
||||
|
||||
use crate::{
|
||||
framework::{Args, CommandInvoke, CreateGenericResponse},
|
||||
guild_data::CtxGuildData,
|
||||
sound::{JoinSoundCtx, Sound},
|
||||
MySQL,
|
||||
models::{guild_data::CtxGuildData, join_sound::JoinSoundCtx, sound::SoundCtx},
|
||||
Context, Error,
|
||||
};
|
||||
|
||||
#[command("volume")]
|
||||
#[aliases("vol")]
|
||||
#[required_permissions(Managed)]
|
||||
#[group("Settings")]
|
||||
#[description("Change the bot's volume in this server")]
|
||||
#[arg(
|
||||
name = "volume",
|
||||
description = "New volume for the bot to use",
|
||||
kind = "Integer",
|
||||
required = false
|
||||
)]
|
||||
#[example("`/volume` - check the volume on the current server")]
|
||||
#[example("`/volume 100` - reset the volume on the current server")]
|
||||
#[example("`/volume 10` - set the volume on the current server to 10%")]
|
||||
/// Change the bot's volume in this server
|
||||
#[poise::command(slash_command, rename = "volume")]
|
||||
pub async fn change_volume(
|
||||
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 guild_data_opt = ctx.guild_data(invoke.guild_id().unwrap()).await;
|
||||
ctx: Context<'_>,
|
||||
#[description = "New volume as a percentage"] volume: Option<usize>,
|
||||
) -> Result<(), Error> {
|
||||
let guild_data_opt = ctx.guild_data(ctx.guild_id().unwrap()).await;
|
||||
let guild_data = guild_data_opt.unwrap();
|
||||
|
||||
if let Some(volume) = args.named("volume").map(|i| i.parse::<u8>().ok()).flatten() {
|
||||
guild_data.write().await.volume = volume;
|
||||
if let Some(volume) = volume {
|
||||
guild_data.write().await.volume = volume as u8;
|
||||
|
||||
guild_data.read().await.commit(pool).await?;
|
||||
|
||||
invoke
|
||||
.respond(
|
||||
ctx.http.clone(),
|
||||
CreateGenericResponse::new().content(format!("Volume changed to {}%", volume)),
|
||||
)
|
||||
guild_data
|
||||
.read()
|
||||
.await
|
||||
.commit(ctx.data().database.clone())
|
||||
.await?;
|
||||
|
||||
ctx.say(format!("Volume changed to {}%", volume)).await?;
|
||||
} else {
|
||||
let read = guild_data.read().await;
|
||||
|
||||
invoke
|
||||
.respond(
|
||||
ctx.http.clone(),
|
||||
CreateGenericResponse::new().content(format!(
|
||||
ctx.say(format!(
|
||||
"Current server volume: {vol}%. Change the volume with `/volume <new volume>`",
|
||||
vol = read.volume
|
||||
)),
|
||||
)
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command("prefix")]
|
||||
#[required_permissions(Restricted)]
|
||||
#[kind(Text)]
|
||||
#[group("Settings")]
|
||||
#[description("Change the prefix of the bot for using non-slash commands")]
|
||||
#[arg(
|
||||
name = "prefix",
|
||||
kind = "String",
|
||||
description = "The new prefix to use for the bot",
|
||||
required = true
|
||||
)]
|
||||
pub async fn change_prefix(
|
||||
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 guild_data;
|
||||
|
||||
{
|
||||
let guild_data_opt = ctx.guild_data(invoke.guild_id().unwrap()).await;
|
||||
|
||||
guild_data = guild_data_opt.unwrap();
|
||||
}
|
||||
|
||||
if let Some(prefix) = args.named("prefix") {
|
||||
if prefix.len() <= 5 && !prefix.is_empty() {
|
||||
let reply = format!("Prefix changed to `{}`", prefix);
|
||||
|
||||
{
|
||||
guild_data.write().await.prefix = prefix.to_string();
|
||||
}
|
||||
|
||||
{
|
||||
let read = guild_data.read().await;
|
||||
|
||||
read.commit(pool).await?;
|
||||
}
|
||||
|
||||
invoke
|
||||
.respond(
|
||||
ctx.http.clone(),
|
||||
CreateGenericResponse::new().content(reply),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
invoke
|
||||
.respond(
|
||||
ctx.http.clone(),
|
||||
CreateGenericResponse::new()
|
||||
.content("Prefix must be less than 5 characters long"),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
invoke
|
||||
.respond(
|
||||
ctx.http.clone(),
|
||||
CreateGenericResponse::new().content(format!(
|
||||
"Usage: `{prefix}prefix <new prefix>`",
|
||||
prefix = guild_data.read().await.prefix
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
/// Manage greet sounds on this server
|
||||
#[poise::command(slash_command, rename = "greet")]
|
||||
pub async fn greet_sound(_ctx: Context<'_>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command("roles")]
|
||||
#[required_permissions(Restricted)]
|
||||
#[group("Settings")]
|
||||
#[description("Change the role allowed to use the bot")]
|
||||
#[arg(
|
||||
name = "role",
|
||||
kind = "Role",
|
||||
description = "A role to allow to use the bot. Use @everyone to allow all server members",
|
||||
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(
|
||||
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 role_id = args.named("role").unwrap().parse::<u64>().unwrap();
|
||||
let guild_data = ctx.guild_data(invoke.guild_id().unwrap()).await.unwrap();
|
||||
|
||||
guild_data.write().await.allowed_role = Some(role_id);
|
||||
guild_data.read().await.commit(pool).await?;
|
||||
|
||||
invoke
|
||||
.respond(
|
||||
ctx.http.clone(),
|
||||
CreateGenericResponse::new().content(format!("Allowed role set to <@&{}>", role_id)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command("greet")]
|
||||
#[group("Settings")]
|
||||
#[description("Set a join sound")]
|
||||
#[arg(
|
||||
name = "query",
|
||||
kind = "String",
|
||||
description = "Name or ID of sound to set as your greet sound",
|
||||
required = false
|
||||
)]
|
||||
#[example("`/greet` - remove your join sound")]
|
||||
#[example("`/greet 1523` - set your join sound to sound with ID 1523")]
|
||||
/// Set a join sound
|
||||
#[poise::command(slash_command, rename = "set")]
|
||||
pub async fn set_greet_sound(
|
||||
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 query = args
|
||||
.named("query")
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or(String::new());
|
||||
let user_id = invoke.author_id();
|
||||
|
||||
if query.len() == 0 {
|
||||
ctx.update_join_sound(user_id, None).await;
|
||||
|
||||
invoke
|
||||
.respond(
|
||||
ctx.http.clone(),
|
||||
CreateGenericResponse::new().content("Your greet sound has been unset."),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
let sound_vec = Sound::search_for_sound(
|
||||
&query,
|
||||
invoke.guild_id().unwrap(),
|
||||
user_id,
|
||||
pool.clone(),
|
||||
true,
|
||||
)
|
||||
ctx: Context<'_>,
|
||||
#[description = "Name or ID of sound to set as your join sound"] name: String,
|
||||
) -> Result<(), Error> {
|
||||
let sound_vec = ctx
|
||||
.data()
|
||||
.search_for_sound(&name, ctx.guild_id().unwrap(), ctx.author().id, true)
|
||||
.await?;
|
||||
|
||||
match sound_vec.first() {
|
||||
Some(sound) => {
|
||||
ctx.update_join_sound(user_id, Some(sound.id)).await;
|
||||
ctx.data()
|
||||
.update_join_sound(ctx.author().id, Some(sound.id))
|
||||
.await;
|
||||
|
||||
invoke
|
||||
.respond(
|
||||
ctx.http.clone(),
|
||||
CreateGenericResponse::new().content(format!(
|
||||
ctx.say(format!(
|
||||
"Greet sound has been set to {} (ID {})",
|
||||
sound.name, sound.id
|
||||
)),
|
||||
)
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
None => {
|
||||
invoke
|
||||
.respond(
|
||||
ctx.http.clone(),
|
||||
CreateGenericResponse::new()
|
||||
.content("Could not find a sound by that name."),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ctx.say("Could not find a sound by that name.").await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command("allow_greet")]
|
||||
#[group("Settings")]
|
||||
#[description("Configure whether users should be able to use join sounds")]
|
||||
#[required_permissions(Restricted)]
|
||||
#[example("`/allow_greet` - disable greet sounds in the server")]
|
||||
#[example("`/allow_greet` - re-enable greet sounds in the server")]
|
||||
pub async fn allow_greet_sounds(
|
||||
ctx: &Context,
|
||||
invoke: &(dyn CommandInvoke + Sync + Send),
|
||||
_args: Args,
|
||||
) -> CommandResult {
|
||||
let pool = ctx
|
||||
.data
|
||||
.read()
|
||||
.await
|
||||
.get::<MySQL>()
|
||||
.cloned()
|
||||
.expect("Could not acquire SQL pool from data");
|
||||
/// Set a join sound
|
||||
#[poise::command(slash_command, rename = "unset")]
|
||||
pub async fn unset_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
|
||||
ctx.data().update_join_sound(ctx.author().id, None).await;
|
||||
|
||||
let guild_data_opt = ctx.guild_data(invoke.guild_id().unwrap()).await;
|
||||
ctx.say("Greet sound has been unset").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disable greet sounds on this server
|
||||
#[poise::command(slash_command, rename = "disable")]
|
||||
pub async fn disable_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let guild_data_opt = ctx.guild_data(ctx.guild_id().unwrap()).await;
|
||||
|
||||
if let Ok(guild_data) = guild_data_opt {
|
||||
let current = guild_data.read().await.allow_greets;
|
||||
guild_data.write().await.allow_greets = false;
|
||||
|
||||
{
|
||||
guild_data.write().await.allow_greets = !current;
|
||||
}
|
||||
|
||||
guild_data.read().await.commit(pool).await?;
|
||||
|
||||
invoke
|
||||
.respond(
|
||||
ctx.http.clone(),
|
||||
CreateGenericResponse::new().content(format!(
|
||||
"Greet sounds have been {}abled in this server",
|
||||
if !current { "en" } else { "dis" }
|
||||
)),
|
||||
)
|
||||
guild_data
|
||||
.read()
|
||||
.await
|
||||
.commit(ctx.data().database.clone())
|
||||
.await?;
|
||||
}
|
||||
|
||||
ctx.say("Greet sounds have been disabled in this server")
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enable greet sounds on this server
|
||||
#[poise::command(slash_command, rename = "enable")]
|
||||
pub async fn enable_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let guild_data_opt = ctx.guild_data(ctx.guild_id().unwrap()).await;
|
||||
|
||||
if let Ok(guild_data) = guild_data_opt {
|
||||
guild_data.write().await.allow_greets = true;
|
||||
|
||||
guild_data
|
||||
.read()
|
||||
.await
|
||||
.commit(ctx.data().database.clone())
|
||||
.await?;
|
||||
}
|
||||
|
||||
ctx.say("Greet sounds have been enable in this server")
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,22 +1,12 @@
|
||||
use regex_command_attr::command;
|
||||
use serenity::{client::Context, framework::standard::CommandResult};
|
||||
use songbird;
|
||||
|
||||
use crate::framework::{Args, CommandInvoke, CreateGenericResponse};
|
||||
use crate::{Context, Error};
|
||||
|
||||
#[command("stop")]
|
||||
#[required_permissions(Managed)]
|
||||
#[group("Stop")]
|
||||
#[description("Stop the bot from playing")]
|
||||
pub async fn stop_playing(
|
||||
ctx: &Context,
|
||||
invoke: &(dyn CommandInvoke + Sync + Send),
|
||||
_args: Args,
|
||||
) -> CommandResult {
|
||||
let guild_id = invoke.guild_id().unwrap();
|
||||
|
||||
let songbird = songbird::get(ctx).await.unwrap();
|
||||
let call_opt = songbird.get(guild_id);
|
||||
/// Stop the bot from playing
|
||||
#[poise::command(slash_command, rename = "stop")]
|
||||
pub async fn stop_playing(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let songbird = songbird::get(ctx.discord()).await.unwrap();
|
||||
let call_opt = songbird.get(ctx.guild_id().unwrap());
|
||||
|
||||
if let Some(call) = call_opt {
|
||||
let mut lock = call.lock().await;
|
||||
@ -24,31 +14,18 @@ pub async fn stop_playing(
|
||||
lock.stop();
|
||||
}
|
||||
|
||||
invoke
|
||||
.respond(ctx.http.clone(), CreateGenericResponse::new().content("👍"))
|
||||
.await?;
|
||||
ctx.say("👍").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[aliases("dc")]
|
||||
#[required_permissions(Managed)]
|
||||
#[group("Stop")]
|
||||
#[description("Disconnect the bot")]
|
||||
pub async fn disconnect(
|
||||
ctx: &Context,
|
||||
invoke: &(dyn CommandInvoke + Sync + Send),
|
||||
_args: Args,
|
||||
) -> CommandResult {
|
||||
let guild_id = invoke.guild_id().unwrap();
|
||||
/// Disconnect the bot
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn disconnect(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let songbird = songbird::get(ctx.discord()).await.unwrap();
|
||||
let _ = songbird.leave(ctx.guild_id().unwrap()).await;
|
||||
|
||||
let songbird = songbird::get(ctx).await.unwrap();
|
||||
let _ = songbird.leave(guild_id).await;
|
||||
|
||||
invoke
|
||||
.respond(ctx.http.clone(), CreateGenericResponse::new().content("👍"))
|
||||
.await?;
|
||||
ctx.say("👍").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
9
src/consts.rs
Normal file
9
src/consts.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use std::env;
|
||||
|
||||
pub const THEME_COLOR: u32 = 0x00e0f3;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref MAX_SOUNDS: u32 = env::var("MAX_SOUNDS").unwrap().parse::<u32>().unwrap();
|
||||
pub static ref PATREON_GUILD: u64 = env::var("PATREON_GUILD").unwrap().parse::<u64>().unwrap();
|
||||
pub static ref PATREON_ROLE: u64 = env::var("PATREON_ROLE").unwrap().parse::<u64>().unwrap();
|
||||
}
|
@ -1,27 +1,19 @@
|
||||
use std::{collections::HashMap, env};
|
||||
|
||||
use poise::serenity::{async_trait, model::channel::Channel, prelude::Context, utils::shard_id};
|
||||
use songbird::{Event, EventContext, EventHandler as SongbirdEventHandler};
|
||||
|
||||
use crate::{
|
||||
guild_data::CtxGuildData,
|
||||
join_channel, play_audio,
|
||||
sound::{JoinSoundCtx, Sound},
|
||||
Data, Error,
|
||||
use poise::serenity::{
|
||||
model::{
|
||||
channel::Channel,
|
||||
interactions::{Interaction, InteractionResponseType},
|
||||
},
|
||||
prelude::Context,
|
||||
utils::shard_id,
|
||||
};
|
||||
|
||||
pub struct RestartTrack;
|
||||
|
||||
#[async_trait]
|
||||
impl SongbirdEventHandler for RestartTrack {
|
||||
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
||||
if let EventContext::Track(&[(_state, track)]) = ctx {
|
||||
let _ = track.seek_time(Default::default());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
use crate::{
|
||||
models::{guild_data::CtxGuildData, join_sound::JoinSoundCtx, sound::Sound},
|
||||
utils::{join_channel, play_audio, play_from_query},
|
||||
Data, Error,
|
||||
};
|
||||
|
||||
pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> {
|
||||
match event {
|
||||
@ -126,6 +118,29 @@ SELECT name, id, public, server_id, uploader_id
|
||||
}
|
||||
}
|
||||
}
|
||||
poise::Event::InteractionCreate { interaction } => match interaction {
|
||||
Interaction::MessageComponent(component) => {
|
||||
if component.guild_id.is_some() {
|
||||
play_from_query(
|
||||
&ctx,
|
||||
&data,
|
||||
component.guild_id.unwrap().to_guild_cached(&ctx).unwrap(),
|
||||
component.user.id,
|
||||
&component.data.custom_id,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
component
|
||||
.create_interaction_response(ctx, |r| {
|
||||
r.kind(InteractionResponseType::DeferredUpdateMessage)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
168
src/main.rs
168
src/main.rs
@ -2,10 +2,11 @@
|
||||
extern crate lazy_static;
|
||||
|
||||
mod cmds;
|
||||
mod consts;
|
||||
mod error;
|
||||
mod event_handlers;
|
||||
mod guild_data;
|
||||
mod sound;
|
||||
mod models;
|
||||
mod utils;
|
||||
|
||||
use std::{env, sync::Arc};
|
||||
|
||||
@ -14,21 +15,15 @@ use dotenv::dotenv;
|
||||
use poise::serenity::{
|
||||
builder::CreateApplicationCommands,
|
||||
model::{
|
||||
channel::Channel,
|
||||
gateway::{Activity, GatewayIntents},
|
||||
guild::Guild,
|
||||
id::{ChannelId, GuildId, UserId},
|
||||
id::{GuildId, UserId},
|
||||
},
|
||||
};
|
||||
use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call, SerenityInit};
|
||||
use songbird::SerenityInit;
|
||||
use sqlx::mysql::MySqlPool;
|
||||
use tokio::sync::{Mutex, MutexGuard, RwLock};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
event_handlers::listener,
|
||||
guild_data::{CtxGuildData, GuildData},
|
||||
sound::Sound,
|
||||
};
|
||||
use crate::{event_handlers::listener, models::guild_data::GuildData};
|
||||
|
||||
pub struct Data {
|
||||
database: MySqlPool,
|
||||
@ -40,137 +35,6 @@ pub struct Data {
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
type Context<'a> = poise::Context<'a, Data, Error>;
|
||||
|
||||
const THEME_COLOR: u32 = 0x00e0f3;
|
||||
|
||||
lazy_static! {
|
||||
static ref MAX_SOUNDS: u32 = env::var("MAX_SOUNDS").unwrap().parse::<u32>().unwrap();
|
||||
static ref PATREON_GUILD: u64 = env::var("PATREON_GUILD").unwrap().parse::<u64>().unwrap();
|
||||
static ref PATREON_ROLE: u64 = env::var("PATREON_ROLE").unwrap().parse::<u64>().unwrap();
|
||||
}
|
||||
|
||||
async fn play_audio(
|
||||
sound: &mut Sound,
|
||||
volume: u8,
|
||||
call_handler: &mut MutexGuard<'_, Call>,
|
||||
mysql_pool: MySqlPool,
|
||||
loop_: bool,
|
||||
) -> Result<TrackHandle, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let (track, track_handler) =
|
||||
create_player(sound.store_sound_source(mysql_pool.clone()).await?.into());
|
||||
|
||||
let _ = track_handler.set_volume(volume as f32 / 100.0);
|
||||
|
||||
if loop_ {
|
||||
let _ = track_handler.enable_loop();
|
||||
} else {
|
||||
let _ = track_handler.disable_loop();
|
||||
}
|
||||
|
||||
call_handler.play(track);
|
||||
|
||||
Ok(track_handler)
|
||||
}
|
||||
|
||||
async fn join_channel(
|
||||
ctx: &poise::serenity_prelude::Context,
|
||||
guild: Guild,
|
||||
channel_id: ChannelId,
|
||||
) -> (Arc<Mutex<Call>>, JoinResult<()>) {
|
||||
let songbird = songbird::get(ctx).await.unwrap();
|
||||
let current_user = ctx.cache.current_user_id();
|
||||
|
||||
let current_voice_state = guild
|
||||
.voice_states
|
||||
.get(¤t_user)
|
||||
.and_then(|voice_state| voice_state.channel_id);
|
||||
|
||||
let (call, res) = if current_voice_state == Some(channel_id) {
|
||||
let call_opt = songbird.get(guild.id);
|
||||
|
||||
if let Some(call) = call_opt {
|
||||
(call, Ok(()))
|
||||
} else {
|
||||
let (call, res) = songbird.join(guild.id, channel_id).await;
|
||||
|
||||
(call, res)
|
||||
}
|
||||
} else {
|
||||
let (call, res) = songbird.join(guild.id, channel_id).await;
|
||||
|
||||
(call, res)
|
||||
};
|
||||
|
||||
{
|
||||
// set call to deafen
|
||||
let _ = call.lock().await.deafen(true).await;
|
||||
}
|
||||
|
||||
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
|
||||
channel
|
||||
.edit_voice_state(&ctx, ctx.cache.current_user(), |v| v.suppress(false))
|
||||
.await;
|
||||
}
|
||||
|
||||
(call, res)
|
||||
}
|
||||
|
||||
async fn play_from_query(
|
||||
ctx: &Context<'_>,
|
||||
guild: Guild,
|
||||
user_id: UserId,
|
||||
query: &str,
|
||||
loop_: bool,
|
||||
) -> String {
|
||||
let guild_id = guild.id;
|
||||
|
||||
let channel_to_join = guild
|
||||
.voice_states
|
||||
.get(&user_id)
|
||||
.and_then(|voice_state| voice_state.channel_id);
|
||||
|
||||
match channel_to_join {
|
||||
Some(user_channel) => {
|
||||
let pool = ctx.data().database.clone();
|
||||
|
||||
let mut sound_vec =
|
||||
Sound::search_for_sound(query, guild_id, user_id, pool.clone(), true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let sound_res = sound_vec.first_mut();
|
||||
|
||||
match sound_res {
|
||||
Some(sound) => {
|
||||
{
|
||||
let (call_handler, _) =
|
||||
join_channel(ctx.discord(), guild.clone(), user_channel).await;
|
||||
|
||||
let guild_data = ctx.guild_data(guild_id).await.unwrap();
|
||||
|
||||
let mut lock = call_handler.lock().await;
|
||||
|
||||
play_audio(
|
||||
sound,
|
||||
guild_data.read().await.volume,
|
||||
&mut lock,
|
||||
pool,
|
||||
loop_,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
format!("Playing sound {} with ID {}", sound.name, sound.id)
|
||||
}
|
||||
|
||||
None => "Couldn't find sound by term provided".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
None => "You are not in a voice chat!".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register_application_commands(
|
||||
ctx: &poise::serenity::client::Context,
|
||||
framework: &poise::Framework<Data, Error>,
|
||||
@ -215,10 +79,28 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
cmds::info::info(),
|
||||
cmds::manage::change_public(),
|
||||
cmds::manage::upload_new_sound(),
|
||||
cmds::manage::download_file(),
|
||||
cmds::manage::delete_sound(),
|
||||
cmds::play::play(),
|
||||
cmds::play::loop_play(),
|
||||
cmds::play::soundboard(),
|
||||
cmds::search::list_sounds(),
|
||||
cmds::search::list_user_sounds(),
|
||||
cmds::search::show_random_sounds(),
|
||||
cmds::search::search_sounds(),
|
||||
cmds::stop::stop_playing(),
|
||||
cmds::stop::disconnect(),
|
||||
cmds::settings::change_volume(),
|
||||
poise::Command {
|
||||
subcommands: vec![
|
||||
cmds::settings::disable_greet_sound(),
|
||||
cmds::settings::enable_greet_sound(),
|
||||
cmds::settings::set_greet_sound(),
|
||||
cmds::settings::unset_greet_sound(),
|
||||
cmds::settings::greet_sound(),
|
||||
],
|
||||
..cmds::settings::greet_sound()
|
||||
},
|
||||
],
|
||||
allowed_mentions: None,
|
||||
listener: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)),
|
||||
|
87
src/models/join_sound.rs
Normal file
87
src/models/join_sound.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use poise::serenity::{async_trait, model::id::UserId};
|
||||
|
||||
use crate::Data;
|
||||
|
||||
#[async_trait]
|
||||
pub trait JoinSoundCtx {
|
||||
async fn join_sound<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Option<u32>;
|
||||
async fn update_join_sound<U: Into<UserId> + Send + Sync>(
|
||||
&self,
|
||||
user_id: U,
|
||||
join_id: Option<u32>,
|
||||
);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl JoinSoundCtx for Data {
|
||||
async fn join_sound<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Option<u32> {
|
||||
let user_id = user_id.into();
|
||||
|
||||
let x = if let Some(join_sound_id) = self.join_sound_cache.get(&user_id) {
|
||||
join_sound_id.value().clone()
|
||||
} else {
|
||||
let join_sound_id = {
|
||||
let pool = self.database.clone();
|
||||
|
||||
let join_id_res = sqlx::query!(
|
||||
"
|
||||
SELECT join_sound_id
|
||||
FROM users
|
||||
WHERE user = ?
|
||||
",
|
||||
user_id.as_u64()
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await;
|
||||
|
||||
if let Ok(row) = join_id_res {
|
||||
row.join_sound_id
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
self.join_sound_cache.insert(user_id, join_sound_id);
|
||||
|
||||
join_sound_id
|
||||
};
|
||||
|
||||
x
|
||||
}
|
||||
|
||||
async fn update_join_sound<U: Into<UserId> + Send + Sync>(
|
||||
&self,
|
||||
user_id: U,
|
||||
join_id: Option<u32>,
|
||||
) {
|
||||
let user_id = user_id.into();
|
||||
|
||||
self.join_sound_cache.insert(user_id, join_id);
|
||||
|
||||
let pool = self.database.clone();
|
||||
|
||||
let _ = sqlx::query!(
|
||||
"
|
||||
INSERT IGNORE INTO users (user)
|
||||
VALUES (?)
|
||||
",
|
||||
user_id.as_u64()
|
||||
)
|
||||
.execute(&pool)
|
||||
.await;
|
||||
|
||||
let _ = sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET
|
||||
join_sound_id = ?
|
||||
WHERE
|
||||
user = ?
|
||||
",
|
||||
join_id,
|
||||
user_id.as_u64()
|
||||
)
|
||||
.execute(&pool)
|
||||
.await;
|
||||
}
|
||||
}
|
3
src/models/mod.rs
Normal file
3
src/models/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod guild_data;
|
||||
pub mod join_sound;
|
||||
pub mod sound;
|
@ -1,96 +1,11 @@
|
||||
use std::{env, path::Path};
|
||||
|
||||
use poise::serenity::{async_trait, model::id::UserId};
|
||||
use poise::serenity::async_trait;
|
||||
use songbird::input::restartable::Restartable;
|
||||
use sqlx::mysql::MySqlPool;
|
||||
use sqlx::{mysql::MySqlPool, Error};
|
||||
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
|
||||
|
||||
use super::error::ErrorTypes;
|
||||
use crate::Data;
|
||||
|
||||
#[async_trait]
|
||||
pub trait JoinSoundCtx {
|
||||
async fn join_sound<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Option<u32>;
|
||||
async fn update_join_sound<U: Into<UserId> + Send + Sync>(
|
||||
&self,
|
||||
user_id: U,
|
||||
join_id: Option<u32>,
|
||||
);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl JoinSoundCtx for Data {
|
||||
async fn join_sound<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Option<u32> {
|
||||
let user_id = user_id.into();
|
||||
|
||||
let x = if let Some(join_sound_id) = self.join_sound_cache.get(&user_id) {
|
||||
join_sound_id.value().clone()
|
||||
} else {
|
||||
let join_sound_id = {
|
||||
let pool = self.database.clone();
|
||||
|
||||
let join_id_res = sqlx::query!(
|
||||
"
|
||||
SELECT join_sound_id
|
||||
FROM users
|
||||
WHERE user = ?
|
||||
",
|
||||
user_id.as_u64()
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await;
|
||||
|
||||
if let Ok(row) = join_id_res {
|
||||
row.join_sound_id
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
self.join_sound_cache.insert(user_id, join_sound_id);
|
||||
|
||||
join_sound_id
|
||||
};
|
||||
|
||||
x
|
||||
}
|
||||
|
||||
async fn update_join_sound<U: Into<UserId> + Send + Sync>(
|
||||
&self,
|
||||
user_id: U,
|
||||
join_id: Option<u32>,
|
||||
) {
|
||||
let user_id = user_id.into();
|
||||
|
||||
self.join_sound_cache.insert(user_id, join_id);
|
||||
|
||||
let pool = self.database.clone();
|
||||
|
||||
let _ = sqlx::query!(
|
||||
"
|
||||
INSERT IGNORE INTO users (user)
|
||||
VALUES (?)
|
||||
",
|
||||
user_id.as_u64()
|
||||
)
|
||||
.execute(&pool)
|
||||
.await;
|
||||
|
||||
let _ = sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET
|
||||
join_sound_id = ?
|
||||
WHERE
|
||||
user = ?
|
||||
",
|
||||
join_id,
|
||||
user_id.as_u64()
|
||||
)
|
||||
.execute(&pool)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
use crate::{error::ErrorTypes, Data};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Sound {
|
||||
@ -107,16 +22,41 @@ impl PartialEq for Sound {
|
||||
}
|
||||
}
|
||||
|
||||
impl Sound {
|
||||
pub async fn search_for_sound<G: Into<u64>, U: Into<u64>>(
|
||||
#[async_trait]
|
||||
pub trait SoundCtx {
|
||||
async fn search_for_sound<G: Into<u64> + Send, U: Into<u64> + Send>(
|
||||
&self,
|
||||
query: &str,
|
||||
guild_id: G,
|
||||
user_id: U,
|
||||
strict: bool,
|
||||
) -> Result<Vec<Sound>, sqlx::Error>;
|
||||
async fn autocomplete_user_sounds<U: Into<u64> + Send, G: Into<u64> + Send>(
|
||||
&self,
|
||||
query: &str,
|
||||
user_id: U,
|
||||
guild_id: G,
|
||||
) -> Result<Vec<Sound>, sqlx::Error>;
|
||||
async fn user_sounds<U: Into<u64> + Send>(&self, user_id: U)
|
||||
-> Result<Vec<Sound>, sqlx::Error>;
|
||||
async fn guild_sounds<G: Into<u64> + Send>(
|
||||
&self,
|
||||
guild_id: G,
|
||||
) -> Result<Vec<Sound>, sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SoundCtx for Data {
|
||||
async fn search_for_sound<G: Into<u64> + Send, U: Into<u64> + Send>(
|
||||
&self,
|
||||
query: &str,
|
||||
guild_id: G,
|
||||
user_id: U,
|
||||
db_pool: MySqlPool,
|
||||
strict: bool,
|
||||
) -> Result<Vec<Sound>, sqlx::Error> {
|
||||
let guild_id = guild_id.into();
|
||||
let user_id = user_id.into();
|
||||
let db_pool = self.database.clone();
|
||||
|
||||
fn extract_id(s: &str) -> Option<u32> {
|
||||
if s.len() > 3 && s.to_lowercase().starts_with("id:") {
|
||||
@ -134,7 +74,7 @@ impl Sound {
|
||||
|
||||
if let Some(id) = extract_id(&query) {
|
||||
let sound = sqlx::query_as_unchecked!(
|
||||
Self,
|
||||
Sound,
|
||||
"
|
||||
SELECT name, id, public, server_id, uploader_id
|
||||
FROM sounds
|
||||
@ -158,7 +98,7 @@ SELECT name, id, public, server_id, uploader_id
|
||||
|
||||
if strict {
|
||||
sound = sqlx::query_as_unchecked!(
|
||||
Self,
|
||||
Sound,
|
||||
"
|
||||
SELECT name, id, public, server_id, uploader_id
|
||||
FROM sounds
|
||||
@ -179,7 +119,7 @@ SELECT name, id, public, server_id, uploader_id
|
||||
.await?;
|
||||
} else {
|
||||
sound = sqlx::query_as_unchecked!(
|
||||
Self,
|
||||
Sound,
|
||||
"
|
||||
SELECT name, id, public, server_id, uploader_id
|
||||
FROM sounds
|
||||
@ -204,6 +144,70 @@ SELECT name, id, public, server_id, uploader_id
|
||||
}
|
||||
}
|
||||
|
||||
async fn autocomplete_user_sounds<U: Into<u64> + Send, G: Into<u64> + Send>(
|
||||
&self,
|
||||
query: &str,
|
||||
user_id: U,
|
||||
guild_id: G,
|
||||
) -> Result<Vec<Sound>, Error> {
|
||||
let db_pool = self.database.clone();
|
||||
|
||||
sqlx::query_as_unchecked!(
|
||||
Sound,
|
||||
"
|
||||
SELECT name, id, public, server_id, uploader_id
|
||||
FROM sounds
|
||||
WHERE name LIKE CONCAT(?, '%') AND (uploader_id = ? OR server_id = ?)
|
||||
LIMIT 25
|
||||
",
|
||||
query,
|
||||
user_id.into(),
|
||||
guild_id.into(),
|
||||
)
|
||||
.fetch_all(&db_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn user_sounds<U: Into<u64> + Send>(
|
||||
&self,
|
||||
user_id: U,
|
||||
) -> Result<Vec<Sound>, sqlx::Error> {
|
||||
let sounds = sqlx::query_as_unchecked!(
|
||||
Sound,
|
||||
"
|
||||
SELECT name, id, public, server_id, uploader_id
|
||||
FROM sounds
|
||||
WHERE uploader_id = ?
|
||||
",
|
||||
user_id.into()
|
||||
)
|
||||
.fetch_all(&self.database)
|
||||
.await?;
|
||||
|
||||
Ok(sounds)
|
||||
}
|
||||
|
||||
async fn guild_sounds<G: Into<u64> + Send>(
|
||||
&self,
|
||||
guild_id: G,
|
||||
) -> Result<Vec<Sound>, sqlx::Error> {
|
||||
let sounds = sqlx::query_as_unchecked!(
|
||||
Sound,
|
||||
"
|
||||
SELECT name, id, public, server_id, uploader_id
|
||||
FROM sounds
|
||||
WHERE server_id = ?
|
||||
",
|
||||
guild_id.into()
|
||||
)
|
||||
.fetch_all(&self.database)
|
||||
.await?;
|
||||
|
||||
Ok(sounds)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sound {
|
||||
async fn src(&self, db_pool: MySqlPool) -> Vec<u8> {
|
||||
struct Src {
|
||||
src: Vec<u8>,
|
||||
@ -229,7 +233,7 @@ SELECT src
|
||||
pub async fn store_sound_source(
|
||||
&self,
|
||||
db_pool: MySqlPool,
|
||||
) -> Result<Restartable, Box<dyn std::error::Error + Send + Sync>> {
|
||||
) -> 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);
|
||||
@ -241,6 +245,15 @@ SELECT src
|
||||
file.write_all(&self.src(db_pool).await).await?;
|
||||
}
|
||||
|
||||
Ok(path_name)
|
||||
}
|
||||
|
||||
pub async fn playable(
|
||||
&self,
|
||||
db_pool: MySqlPool,
|
||||
) -> 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!"))
|
||||
@ -397,42 +410,4 @@ INSERT INTO sounds (name, server_id, uploader_id, public, src)
|
||||
None => Err(Box::new(ErrorTypes::InvalidFile)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn user_sounds<U: Into<u64>>(
|
||||
user_id: U,
|
||||
db_pool: MySqlPool,
|
||||
) -> Result<Vec<Sound>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let sounds = sqlx::query_as_unchecked!(
|
||||
Sound,
|
||||
"
|
||||
SELECT name, id, public, server_id, uploader_id
|
||||
FROM sounds
|
||||
WHERE uploader_id = ?
|
||||
",
|
||||
user_id.into()
|
||||
)
|
||||
.fetch_all(&db_pool)
|
||||
.await?;
|
||||
|
||||
Ok(sounds)
|
||||
}
|
||||
|
||||
pub async fn guild_sounds<G: Into<u64>>(
|
||||
guild_id: G,
|
||||
db_pool: MySqlPool,
|
||||
) -> Result<Vec<Sound>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let sounds = sqlx::query_as_unchecked!(
|
||||
Sound,
|
||||
"
|
||||
SELECT name, id, public, server_id, uploader_id
|
||||
FROM sounds
|
||||
WHERE server_id = ?
|
||||
",
|
||||
guild_id.into()
|
||||
)
|
||||
.fetch_all(&db_pool)
|
||||
.await?;
|
||||
|
||||
Ok(sounds)
|
||||
}
|
||||
}
|
141
src/utils.rs
Normal file
141
src/utils.rs
Normal file
@ -0,0 +1,141 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use poise::serenity::model::{
|
||||
channel::Channel,
|
||||
guild::Guild,
|
||||
id::{ChannelId, UserId},
|
||||
};
|
||||
use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call};
|
||||
use sqlx::MySqlPool;
|
||||
use tokio::sync::{Mutex, MutexGuard};
|
||||
|
||||
use crate::{
|
||||
models::{
|
||||
guild_data::CtxGuildData,
|
||||
sound::{Sound, SoundCtx},
|
||||
},
|
||||
Data,
|
||||
};
|
||||
|
||||
pub async fn play_audio(
|
||||
sound: &mut Sound,
|
||||
volume: u8,
|
||||
call_handler: &mut MutexGuard<'_, Call>,
|
||||
mysql_pool: MySqlPool,
|
||||
loop_: bool,
|
||||
) -> Result<TrackHandle, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let (track, track_handler) = create_player(sound.playable(mysql_pool.clone()).await?.into());
|
||||
|
||||
let _ = track_handler.set_volume(volume as f32 / 100.0);
|
||||
|
||||
if loop_ {
|
||||
let _ = track_handler.enable_loop();
|
||||
} else {
|
||||
let _ = track_handler.disable_loop();
|
||||
}
|
||||
|
||||
call_handler.play(track);
|
||||
|
||||
Ok(track_handler)
|
||||
}
|
||||
|
||||
pub async fn join_channel(
|
||||
ctx: &poise::serenity_prelude::Context,
|
||||
guild: Guild,
|
||||
channel_id: ChannelId,
|
||||
) -> (Arc<Mutex<Call>>, JoinResult<()>) {
|
||||
let songbird = songbird::get(ctx).await.unwrap();
|
||||
let current_user = ctx.cache.current_user_id();
|
||||
|
||||
let current_voice_state = guild
|
||||
.voice_states
|
||||
.get(¤t_user)
|
||||
.and_then(|voice_state| voice_state.channel_id);
|
||||
|
||||
let (call, res) = if current_voice_state == Some(channel_id) {
|
||||
let call_opt = songbird.get(guild.id);
|
||||
|
||||
if let Some(call) = call_opt {
|
||||
(call, Ok(()))
|
||||
} else {
|
||||
let (call, res) = songbird.join(guild.id, channel_id).await;
|
||||
|
||||
(call, res)
|
||||
}
|
||||
} else {
|
||||
let (call, res) = songbird.join(guild.id, channel_id).await;
|
||||
|
||||
(call, res)
|
||||
};
|
||||
|
||||
{
|
||||
// set call to deafen
|
||||
let _ = call.lock().await.deafen(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;
|
||||
}
|
||||
|
||||
(call, res)
|
||||
}
|
||||
|
||||
pub async fn play_from_query(
|
||||
ctx: &poise::serenity_prelude::Context,
|
||||
data: &Data,
|
||||
guild: Guild,
|
||||
user_id: UserId,
|
||||
query: &str,
|
||||
loop_: bool,
|
||||
) -> String {
|
||||
let guild_id = guild.id;
|
||||
|
||||
let channel_to_join = guild
|
||||
.voice_states
|
||||
.get(&user_id)
|
||||
.and_then(|voice_state| voice_state.channel_id);
|
||||
|
||||
match channel_to_join {
|
||||
Some(user_channel) => {
|
||||
let pool = data.database.clone();
|
||||
|
||||
let mut sound_vec = data
|
||||
.search_for_sound(query, guild_id, user_id, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let sound_res = sound_vec.first_mut();
|
||||
|
||||
match sound_res {
|
||||
Some(sound) => {
|
||||
{
|
||||
let (call_handler, _) =
|
||||
join_channel(ctx, guild.clone(), user_channel).await;
|
||||
|
||||
let guild_data = data.guild_data(guild_id).await.unwrap();
|
||||
|
||||
let mut lock = call_handler.lock().await;
|
||||
|
||||
play_audio(
|
||||
sound,
|
||||
guild_data.read().await.volume,
|
||||
&mut lock,
|
||||
pool,
|
||||
loop_,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
format!("Playing sound {} with ID {}", sound.name, sound.id)
|
||||
}
|
||||
|
||||
None => "Couldn't find sound by term provided".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
None => "You are not in a voice chat!".to_string(),
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user