Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
9cfcb0d09c | |||
cc55b3e1d1 | |||
a9a08e656f | |||
30fda2b0ee | |||
5fc7584100 | |||
de0584e2f4 | |||
f7b0150688 | |||
31ee6b4540 | |||
4edcee2567 | |||
208440a7ff | |||
b8b17a504d | |||
6307de331d | |||
64e7eb4a53 | |||
5ce9ca3923 | |||
52327b3695 | |||
365d1df4ce | |||
189cb195a4 | |||
a05d6f77db | |||
f5acab7440 | |||
31b6f7a0ab | |||
5ade3f83a8 | |||
f3c6db036e | |||
651ad9dffe | |||
405fa08c2f | |||
50365c3215 | |||
9d588e7e03 | |||
7a1a1c637f | |||
fe85f82a09 | |||
27f678b978 | |||
8dbf11bb68 | |||
3a70f65ec2 | |||
3fef235839 | |||
a6956d9344 |
@ -1,2 +0,0 @@
|
|||||||
[build]
|
|
||||||
target-dir = "/home/jude/.rust_build/soundfx-rs"
|
|
2
.idea/dataSources.local.xml
generated
2
.idea/dataSources.local.xml
generated
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="dataSourceStorageLocal" created-in="CL-221.5080.224">
|
<component name="dataSourceStorageLocal" created-in="CL-223.8836.42">
|
||||||
<data-source name="MySQL for 5.1 - soundfx@localhost" uuid="1067c1d0-1386-4a39-b3f5-6d48d6f279eb">
|
<data-source name="MySQL for 5.1 - soundfx@localhost" uuid="1067c1d0-1386-4a39-b3f5-6d48d6f279eb">
|
||||||
<database-info product="" version="" jdbc-version="" driver-name="" driver-version="" dbms="MYSQL" exact-version="0" />
|
<database-info product="" version="" jdbc-version="" driver-name="" driver-version="" dbms="MYSQL" exact-version="0" />
|
||||||
<secret-storage>master_key</secret-storage>
|
<secret-storage>master_key</secret-storage>
|
||||||
|
1
.idea/sqldialects.xml
generated
1
.idea/sqldialects.xml
generated
@ -1,7 +1,6 @@
|
|||||||
<?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$/migrations/create.sql" dialect="GenericSQL" />
|
|
||||||
<file url="PROJECT" dialect="MySQL" />
|
<file url="PROJECT" dialect="MySQL" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
1146
Cargo.lock
generated
1146
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
32
Cargo.toml
32
Cargo.toml
@ -1,22 +1,38 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "soundfx-rs"
|
name = "soundfx-rs"
|
||||||
version = "1.5.0"
|
description = "Discord bot for custom sound effects and soundboards"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
version = "1.5.8"
|
||||||
authors = ["jellywx <judesouthworth@pm.me>"]
|
authors = ["jellywx <judesouthworth@pm.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
songbird = { git = "https://github.com/serenity-rs/songbird", branch = "next", features = ["builtin-queue"] }
|
songbird = { version = "0.3", features = ["builtin-queue"] }
|
||||||
poise = { git = "https://github.com/jellywx/poise", branch = "jellywx-pv2" }
|
poise = "0.3"
|
||||||
sqlx = { version = "0.5", default-features = false, features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal"] }
|
sqlx = { version = "0.5", default-features = false, features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "migrate"] }
|
||||||
dotenv = "0.15"
|
|
||||||
tokio = { version = "1", features = ["fs", "process", "io-util"] }
|
tokio = { version = "1", features = ["fs", "process", "io-util"] }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
reqwest = "0.11"
|
reqwest = "0.11"
|
||||||
env_logger = "0.8"
|
env_logger = "0.10"
|
||||||
regex = "1.4"
|
regex = "1.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
dashmap = "4.0"
|
dashmap = "5.3"
|
||||||
|
serde = "1.0"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
|
||||||
[patch."https://github.com/serenity-rs/serenity"]
|
[patch."https://github.com/serenity-rs/serenity"]
|
||||||
serenity = { git = "https://github.com//serenity-rs/serenity", branch = "current" }
|
serenity = { version = "0.11.5" }
|
||||||
|
|
||||||
|
[package.metadata.deb]
|
||||||
|
depends = "$auto, ffmpeg"
|
||||||
|
suggests = "mysql-server-8.0"
|
||||||
|
maintainer-scripts = "debian"
|
||||||
|
assets = [
|
||||||
|
["target/release/soundfx-rs", "usr/bin/soundfx-rs", "755"],
|
||||||
|
["conf/default.env", "etc/soundfx-rs/default.env", "600"]
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.deb.systemd-units]
|
||||||
|
unit-scripts = "systemd"
|
||||||
|
start = false
|
||||||
|
33
README.md
33
README.md
@ -1,26 +1,33 @@
|
|||||||
# SoundFX 2
|
# SoundFX
|
||||||
## The complete (second) Rust rewrite of SoundFX
|
|
||||||
|
|
||||||
SoundFX 2 is the Rust rewrite of SoundFX. SoundFX 2 attempts to retain all functionality of the original bot, in a more
|
A bot for managing sound effects in Discord.
|
||||||
efficient and robust package. SoundFX 2 is as asynchronous as it can get, and runs on the Tokio runtime.
|
|
||||||
|
|
||||||
### Building
|
## Installing
|
||||||
|
|
||||||
Run the migrations in the `migrations` directory to set up the database.
|
Download a .deb file from the releases and install with `sudo apt install ./soundfx_rs-a.b.c_arm64.deb`. You will also need a database set up. Install MySQL 8.
|
||||||
|
|
||||||
Use Cargo to build the executable.
|
## Running & config
|
||||||
|
|
||||||
### Running & Config
|
The bot is installed as a systemd service `soundfx-rs`. Use `systemctl start soundfx-rs` and `systemctl stop soundfx-rs` to respectively start and stop the bot.
|
||||||
|
|
||||||
The bot connects to the MySQL server URL defined in the environment.
|
Config options are provided in a file `/etc/soundfx-rs/default.env`
|
||||||
|
|
||||||
Environment variables read:
|
Options:
|
||||||
* `DISCORD_TOKEN`- your token (required)
|
* `DISCORD_TOKEN`- your token (required)
|
||||||
* `DATABASE_URL`- your database URL (required)
|
* `DATABASE_URL`- your database URL (required)
|
||||||
* `UPLOAD_MAX_SIZE`- specifies the maximum file size to allow in bytes (defaults to 2097152 (2MB))
|
* `MAX_SOUNDS`- specifies how many sounds a user should be allowed without having the `PATREON_ROLE` specified below
|
||||||
* `MAX_SOUNDS`- specifies how many sounds a user should be allowed without Patreon
|
|
||||||
* `PATREON_GUILD`- specifies the ID of the guild being used for Patreon benefits
|
* `PATREON_GUILD`- specifies the ID of the guild being used for Patreon benefits
|
||||||
* `PATREON_ROLE`- specifies the role being checked for Patreon benefits
|
* `PATREON_ROLE`- specifies the role being checked for Patreon benefits
|
||||||
* `CACHING_LOCATION`- specifies the location in which to cache the audio files (defaults to `/tmp/`)
|
* `CACHING_LOCATION`- specifies the location in which to cache the audio files (defaults to `/tmp/`)
|
||||||
|
* `UPLOAD_MAX_SIZE`- specifies the maximum upload size to permit in bytes. Defaults to 2MB
|
||||||
|
|
||||||
The bot will also consider variables in a `.env` file in the working directory.
|
## Building from source
|
||||||
|
|
||||||
|
1. Install build dependencies: `sudo apt install gcc gcc-multilib cmake ffmpeg libopus-dev`
|
||||||
|
2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `soundfx`
|
||||||
|
3. Install Cargo and Rust from https://rustup.rs
|
||||||
|
4. Install SQLx CLI: `cargo install sqlx-cli`
|
||||||
|
5. From the source code directory, execute `sqlx migrate run`
|
||||||
|
6. Build with cargo: `cargo build --release`
|
||||||
|
|
||||||
|
When running from source, the config options above can be configured simply as environment variables.
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"heavy rain": "243627__lebaston100__heavy-rain.wav",
|
|
||||||
"rain on window": "rain-on-windows-cropped.wav",
|
|
||||||
"rain on tent": "531947__straget__the-rain-falls-against-the-parasol.wav",
|
|
||||||
"waves": "400632__inspectorj__ambience-seaside-waves-close-a.wav",
|
|
||||||
"river": "459407__pfannkuchn__small-river-1-fast-close.wav"
|
|
||||||
}
|
|
Binary file not shown.
3
build.rs
Normal file
3
build.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=migrations");
|
||||||
|
}
|
7
conf/default.env
Normal file
7
conf/default.env
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
DISCORD_TOKEN=
|
||||||
|
DATABASE_URL=mysql://localhost/soundfx
|
||||||
|
UPLOAD_MAX_SIZE=2097152
|
||||||
|
MAX_SOUNDS=8
|
||||||
|
CACHING_LOCATION=/tmp
|
||||||
|
PATREON_GUILD=
|
||||||
|
PATREON_ROLE=
|
13
debian/postinst
vendored
Executable file
13
debian/postinst
vendored
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
id -u soundfx &>/dev/null || useradd -r -M soundfx
|
||||||
|
|
||||||
|
if [ ! -f /etc/soundfx-rs/config.env ]; then
|
||||||
|
cp /etc/soundfx-rs/default.env /etc/soundfx-rs/config.env
|
||||||
|
fi
|
||||||
|
|
||||||
|
chown soundfx /etc/soundfx-rs/config.env
|
||||||
|
|
||||||
|
#DEBHELPER#
|
11
debian/postrm
vendored
Normal file
11
debian/postrm
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
id -u soundfx &>/dev/null || userdel soundfx
|
||||||
|
|
||||||
|
if [ -f /etc/soundfx-rs/config.env ]; then
|
||||||
|
rm /etc/soundfx/config.env
|
||||||
|
fi
|
||||||
|
|
||||||
|
#DEBHELPER#
|
10
migrations/20220730000000_expand_join_sounds.sql
Normal file
10
migrations/20220730000000_expand_join_sounds.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE join_sounds (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`user` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`join_sound_id` INT UNSIGNED NOT NULL,
|
||||||
|
`guild` BIGINT UNSIGNED,
|
||||||
|
FOREIGN KEY (`join_sound_id`) REFERENCES sounds(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO join_sounds (`user`, `join_sound_id`) SELECT `user`, `join_sound_id` FROM `users` WHERE `join_sound_id` is not null;
|
1
migrations/20230323110559_disable_user_greets.sql
Normal file
1
migrations/20230323110559_disable_user_greets.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE servers MODIFY COLUMN allow_greets INT NOT NULL DEFAULT 1;
|
@ -4,7 +4,7 @@ use crate::{consts::THEME_COLOR, Context, Error};
|
|||||||
#[poise::command(slash_command)]
|
#[poise::command(slash_command)]
|
||||||
pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
ctx.send(|m| {
|
ctx.send(|m| {
|
||||||
m.embed(|e| {
|
m.ephemeral(true).embed(|e| {
|
||||||
e.title("Help")
|
e.title("Help")
|
||||||
.color(THEME_COLOR)
|
.color(THEME_COLOR)
|
||||||
.footer(|f| {
|
.footer(|f| {
|
||||||
@ -21,6 +21,7 @@ pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
|
|
||||||
__Play Commands__
|
__Play Commands__
|
||||||
`/play` - Play a sound by name or ID
|
`/play` - Play a sound by name or ID
|
||||||
|
`/queue` - Play sounds on queue instead of instantly
|
||||||
`/loop` - Play a sound on loop
|
`/loop` - Play a sound on loop
|
||||||
`/disconnect` - Disconnect the bot
|
`/disconnect` - Disconnect the bot
|
||||||
`/stop` - Stop playback
|
`/stop` - Stop playback
|
||||||
@ -38,7 +39,8 @@ __Search Commands__
|
|||||||
`/random` - View random public sounds
|
`/random` - View random public sounds
|
||||||
|
|
||||||
__Setting Commands__
|
__Setting Commands__
|
||||||
`/greet set/unset` - Set or unset a join sound
|
`/greet server set/unset` - Set or unset a join sound for just this server
|
||||||
|
`/greet user set/unset` - Set or unset a join sound across all servers
|
||||||
`/greet enable/disable` - Enable or disable join sounds on this server
|
`/greet enable/disable` - Enable or disable join sounds on this server
|
||||||
`/volume` - Change the volume
|
`/volume` - Change the volume
|
||||||
|
|
||||||
@ -57,7 +59,7 @@ __Advanced Commands__
|
|||||||
pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let current_user = ctx.discord().cache.current_user();
|
let current_user = ctx.discord().cache.current_user();
|
||||||
|
|
||||||
ctx.send(|m| m
|
ctx.send(|m| m.ephemeral(true)
|
||||||
.embed(|e| e
|
.embed(|e| e
|
||||||
.title("Info")
|
.title("Info")
|
||||||
.color(THEME_COLOR)
|
.color(THEME_COLOR)
|
||||||
|
@ -13,13 +13,16 @@ use crate::{
|
|||||||
slash_command,
|
slash_command,
|
||||||
rename = "upload",
|
rename = "upload",
|
||||||
category = "Manage",
|
category = "Manage",
|
||||||
required_permissions = "MANAGE_GUILD"
|
default_member_permissions = "MANAGE_GUILD",
|
||||||
|
guild_only = true
|
||||||
)]
|
)]
|
||||||
pub async fn upload_new_sound(
|
pub async fn upload_new_sound(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Name to upload sound to"] name: String,
|
#[description = "Name to upload sound to"] name: String,
|
||||||
#[description = "Sound file (max. 2MB)"] file: Attachment,
|
#[description = "Sound file (max. 2MB)"] file: Attachment,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
ctx.defer().await?;
|
||||||
|
|
||||||
fn is_numeric(s: &String) -> bool {
|
fn is_numeric(s: &String) -> bool {
|
||||||
for char in s.chars() {
|
for char in s.chars() {
|
||||||
if char.is_digit(10) {
|
if char.is_digit(10) {
|
||||||
@ -98,7 +101,7 @@ pub async fn upload_new_sound(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a sound you have uploaded
|
/// Delete a sound you have uploaded
|
||||||
#[poise::command(slash_command, rename = "delete", category = "Manage")]
|
#[poise::command(slash_command, rename = "delete", guild_only = true)]
|
||||||
pub async fn delete_sound(
|
pub async fn delete_sound(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Name or ID of sound to delete"]
|
#[description = "Name or ID of sound to delete"]
|
||||||
@ -151,7 +154,7 @@ pub async fn delete_sound(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Change a sound between public and private
|
/// Change a sound between public and private
|
||||||
#[poise::command(slash_command, rename = "public", category = "Manage")]
|
#[poise::command(slash_command, rename = "public", guild_only = true)]
|
||||||
pub async fn change_public(
|
pub async fn change_public(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Name or ID of sound to change privacy setting of"]
|
#[description = "Name or ID of sound to change privacy setting of"]
|
||||||
@ -194,7 +197,7 @@ pub async fn change_public(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Download a sound file from the bot
|
/// Download a sound file from the bot
|
||||||
#[poise::command(slash_command, rename = "download", category = "Manage")]
|
#[poise::command(slash_command, rename = "download", guild_only = true)]
|
||||||
pub async fn download_file(
|
pub async fn download_file(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Name or ID of sound to download"]
|
#[description = "Name or ID of sound to download"]
|
||||||
|
@ -9,7 +9,7 @@ pub mod stop;
|
|||||||
|
|
||||||
pub async fn autocomplete_sound(
|
pub async fn autocomplete_sound(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
partial: String,
|
partial: &str,
|
||||||
) -> Vec<poise::AutocompleteChoice<String>> {
|
) -> Vec<poise::AutocompleteChoice<String>> {
|
||||||
ctx.data()
|
ctx.data()
|
||||||
.autocomplete_user_sounds(&partial, ctx.author().id, ctx.guild_id().unwrap())
|
.autocomplete_user_sounds(&partial, ctx.author().id, ctx.guild_id().unwrap())
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use poise::serenity::{
|
use poise::serenity_prelude::{
|
||||||
builder::CreateActionRow, model::interactions::message_component::ButtonStyle,
|
builder::CreateActionRow, model::application::component::ButtonStyle, GuildChannel,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -10,33 +10,49 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Play a sound in your current voice channel
|
/// Play a sound in your current voice channel
|
||||||
#[poise::command(slash_command, required_permissions = "SPEAK")]
|
#[poise::command(slash_command, default_member_permissions = "SPEAK", guild_only = true)]
|
||||||
pub async fn play(
|
pub async fn play(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Name or ID of sound to play"]
|
#[description = "Name or ID of sound to play"]
|
||||||
#[autocomplete = "autocomplete_sound"]
|
#[autocomplete = "autocomplete_sound"]
|
||||||
name: String,
|
name: String,
|
||||||
|
#[description = "Channel to play in (default: your current voice channel)"]
|
||||||
|
#[channel_types("Voice")]
|
||||||
|
channel: Option<GuildChannel>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
ctx.defer().await?;
|
||||||
|
|
||||||
let guild = ctx.guild().unwrap();
|
let guild = ctx.guild().unwrap();
|
||||||
|
|
||||||
|
if channel.as_ref().map_or(false, |c| c.is_text_based()) {
|
||||||
|
ctx.say("The channel specified is not a voice channel.")
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
ctx.say(
|
ctx.say(
|
||||||
play_from_query(
|
play_from_query(
|
||||||
&ctx.discord(),
|
&ctx.discord(),
|
||||||
&ctx.data(),
|
&ctx.data(),
|
||||||
guild,
|
guild,
|
||||||
ctx.author().id,
|
ctx.author().id,
|
||||||
|
channel.map(|c| c.id),
|
||||||
&name,
|
&name,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await,
|
.await,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Play up to 25 sounds on queue
|
/// Play up to 25 sounds on queue
|
||||||
#[poise::command(slash_command, rename = "queue", required_permissions = "SPEAK")]
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "queue",
|
||||||
|
default_member_permissions = "SPEAK",
|
||||||
|
guild_only = true
|
||||||
|
)]
|
||||||
pub async fn queue_play(
|
pub async fn queue_play(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Name or ID for queue position 1"]
|
#[description = "Name or ID for queue position 1"]
|
||||||
@ -115,7 +131,7 @@ pub async fn queue_play(
|
|||||||
#[autocomplete = "autocomplete_sound"]
|
#[autocomplete = "autocomplete_sound"]
|
||||||
sound_25: Option<String>,
|
sound_25: Option<String>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let _ = ctx.defer().await;
|
ctx.defer().await?;
|
||||||
|
|
||||||
let guild = ctx.guild().unwrap();
|
let guild = ctx.guild().unwrap();
|
||||||
|
|
||||||
@ -197,13 +213,20 @@ pub async fn queue_play(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Loop a sound in your current voice channel
|
/// Loop a sound in your current voice channel
|
||||||
#[poise::command(slash_command, rename = "loop", required_permissions = "SPEAK")]
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "loop",
|
||||||
|
default_member_permissions = "SPEAK",
|
||||||
|
guild_only = true
|
||||||
|
)]
|
||||||
pub async fn loop_play(
|
pub async fn loop_play(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Name or ID of sound to loop"]
|
#[description = "Name or ID of sound to loop"]
|
||||||
#[autocomplete = "autocomplete_sound"]
|
#[autocomplete = "autocomplete_sound"]
|
||||||
name: String,
|
name: String,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
ctx.defer().await?;
|
||||||
|
|
||||||
let guild = ctx.guild().unwrap();
|
let guild = ctx.guild().unwrap();
|
||||||
|
|
||||||
ctx.say(
|
ctx.say(
|
||||||
@ -212,6 +235,7 @@ pub async fn loop_play(
|
|||||||
&ctx.data(),
|
&ctx.data(),
|
||||||
guild,
|
guild,
|
||||||
ctx.author().id,
|
ctx.author().id,
|
||||||
|
None,
|
||||||
&name,
|
&name,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
@ -227,7 +251,8 @@ pub async fn loop_play(
|
|||||||
slash_command,
|
slash_command,
|
||||||
rename = "soundboard",
|
rename = "soundboard",
|
||||||
category = "Play",
|
category = "Play",
|
||||||
required_permissions = "SPEAK"
|
default_member_permissions = "SPEAK",
|
||||||
|
guild_only = true
|
||||||
)]
|
)]
|
||||||
pub async fn soundboard(
|
pub async fn soundboard(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
|
@ -1,8 +1,19 @@
|
|||||||
use poise::{serenity::constants::MESSAGE_CODE_LIMIT, CreateReply};
|
use poise::{
|
||||||
|
serenity_prelude,
|
||||||
|
serenity_prelude::{
|
||||||
|
application::component::ButtonStyle,
|
||||||
|
constants::MESSAGE_CODE_LIMIT,
|
||||||
|
interaction::{message_component::MessageComponentInteraction, InteractionResponseType},
|
||||||
|
CreateActionRow, CreateEmbed, GuildId, UserId,
|
||||||
|
},
|
||||||
|
CreateReply,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
consts::THEME_COLOR,
|
||||||
models::sound::{Sound, SoundCtx},
|
models::sound::{Sound, SoundCtx},
|
||||||
Context, Error,
|
Context, Data, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> {
|
fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> {
|
||||||
@ -27,83 +38,224 @@ fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Show uploaded sounds
|
/// Show uploaded sounds
|
||||||
#[poise::command(slash_command, rename = "list")]
|
#[poise::command(slash_command, rename = "list", guild_only = true)]
|
||||||
pub async fn list_sounds(_ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn list_sounds(_ctx: Context<'_>) -> Result<(), Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||||
|
enum ListContext {
|
||||||
|
User = 0,
|
||||||
|
Guild = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListContext {
|
||||||
|
pub fn title(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ListContext::User => "Your sounds",
|
||||||
|
ListContext::Guild => "Server sounds",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Show the sounds uploaded to this server
|
/// Show the sounds uploaded to this server
|
||||||
#[poise::command(slash_command, rename = "server")]
|
#[poise::command(slash_command, rename = "server", guild_only = true)]
|
||||||
pub async fn list_guild_sounds(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn list_guild_sounds(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let sounds;
|
let pager = SoundPager {
|
||||||
let mut message_buffer;
|
nonce: 0,
|
||||||
|
page: 0,
|
||||||
|
context: ListContext::Guild,
|
||||||
|
};
|
||||||
|
|
||||||
sounds = ctx.data().guild_sounds(ctx.guild_id().unwrap()).await?;
|
pager.reply(ctx).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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show all sounds you have uploaded
|
/// Show all sounds you have uploaded
|
||||||
#[poise::command(slash_command, rename = "user")]
|
#[poise::command(slash_command, rename = "user", guild_only = true)]
|
||||||
pub async fn list_user_sounds(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn list_user_sounds(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let sounds;
|
let pager = SoundPager {
|
||||||
let mut message_buffer;
|
nonce: 0,
|
||||||
|
page: 0,
|
||||||
|
context: ListContext::User,
|
||||||
|
};
|
||||||
|
|
||||||
sounds = ctx.data().user_sounds(ctx.author().id).await?;
|
pager.reply(ctx).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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct SoundPager {
|
||||||
|
nonce: u64,
|
||||||
|
page: u64,
|
||||||
|
context: ListContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoundPager {
|
||||||
|
async fn get_page(
|
||||||
|
&self,
|
||||||
|
data: &Data,
|
||||||
|
user_id: UserId,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> Result<Vec<Sound>, sqlx::Error> {
|
||||||
|
match self.context {
|
||||||
|
ListContext::User => data.user_sounds(user_id, Some(self.page)).await,
|
||||||
|
ListContext::Guild => data.guild_sounds(guild_id, Some(self.page)).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_action_row(&self, max_page: u64) -> CreateActionRow {
|
||||||
|
let mut row = CreateActionRow::default();
|
||||||
|
|
||||||
|
row.create_button(|b| {
|
||||||
|
b.custom_id(
|
||||||
|
serde_json::to_string(&SoundPager {
|
||||||
|
nonce: 0,
|
||||||
|
page: 0,
|
||||||
|
context: self.context,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.style(ButtonStyle::Primary)
|
||||||
|
.label("⏪")
|
||||||
|
.disabled(self.page == 0)
|
||||||
|
})
|
||||||
|
.create_button(|b| {
|
||||||
|
b.custom_id(
|
||||||
|
serde_json::to_string(&SoundPager {
|
||||||
|
nonce: 1,
|
||||||
|
page: self.page.saturating_sub(1),
|
||||||
|
context: self.context,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.style(ButtonStyle::Secondary)
|
||||||
|
.label("◀️")
|
||||||
|
.disabled(self.page == 0)
|
||||||
|
})
|
||||||
|
.create_button(|b| {
|
||||||
|
b.custom_id("pid")
|
||||||
|
.style(ButtonStyle::Success)
|
||||||
|
.label(format!("Page {}", self.page + 1))
|
||||||
|
.disabled(true)
|
||||||
|
})
|
||||||
|
.create_button(|b| {
|
||||||
|
b.custom_id(
|
||||||
|
serde_json::to_string(&SoundPager {
|
||||||
|
nonce: 2,
|
||||||
|
page: self.page.saturating_add(1),
|
||||||
|
context: self.context,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.style(ButtonStyle::Secondary)
|
||||||
|
.label("▶️")
|
||||||
|
.disabled(self.page == max_page)
|
||||||
|
})
|
||||||
|
.create_button(|b| {
|
||||||
|
b.custom_id(
|
||||||
|
serde_json::to_string(&SoundPager {
|
||||||
|
nonce: 3,
|
||||||
|
page: max_page,
|
||||||
|
context: self.context,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.style(ButtonStyle::Primary)
|
||||||
|
.label("⏩")
|
||||||
|
.disabled(self.page == max_page)
|
||||||
|
});
|
||||||
|
|
||||||
|
row
|
||||||
|
}
|
||||||
|
|
||||||
|
fn embed(&self, sounds: &[Sound], count: u64) -> CreateEmbed {
|
||||||
|
let mut embed = CreateEmbed::default();
|
||||||
|
|
||||||
|
embed
|
||||||
|
.color(THEME_COLOR)
|
||||||
|
.title(self.context.title())
|
||||||
|
.description(format!("**{}** sounds:", count))
|
||||||
|
.fields(sounds.iter().map(|s| {
|
||||||
|
(
|
||||||
|
s.name.as_str(),
|
||||||
|
format!(
|
||||||
|
"ID: `{}`\n{}",
|
||||||
|
s.id,
|
||||||
|
if s.public { "*Public*" } else { "*Private*" }
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
embed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_interaction(
|
||||||
|
ctx: &serenity_prelude::Context,
|
||||||
|
data: &Data,
|
||||||
|
interaction: &MessageComponentInteraction,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let user_id = interaction.user.id;
|
||||||
|
let guild_id = interaction.guild_id.unwrap();
|
||||||
|
|
||||||
|
let pager = serde_json::from_str::<Self>(&interaction.data.custom_id)?;
|
||||||
|
let sounds = pager.get_page(data, user_id, guild_id).await?;
|
||||||
|
let count = match pager.context {
|
||||||
|
ListContext::User => data.count_user_sounds(user_id).await?,
|
||||||
|
ListContext::Guild => data.count_guild_sounds(guild_id).await?,
|
||||||
|
};
|
||||||
|
|
||||||
|
interaction
|
||||||
|
.create_interaction_response(&ctx, |r| {
|
||||||
|
r.kind(InteractionResponseType::UpdateMessage)
|
||||||
|
.interaction_response_data(|d| {
|
||||||
|
d.ephemeral(true)
|
||||||
|
.add_embed(pager.embed(&sounds, count))
|
||||||
|
.components(|c| c.add_action_row(pager.create_action_row(count / 25)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reply(&self, ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let sounds = self
|
||||||
|
.get_page(ctx.data(), ctx.author().id, ctx.guild_id().unwrap())
|
||||||
|
.await?;
|
||||||
|
let count = match self.context {
|
||||||
|
ListContext::User => ctx.data().count_user_sounds(ctx.author().id).await?,
|
||||||
|
ListContext::Guild => {
|
||||||
|
ctx.data()
|
||||||
|
.count_guild_sounds(ctx.guild_id().unwrap())
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Search for sounds
|
/// Search for sounds
|
||||||
#[poise::command(slash_command, rename = "search", category = "Search")]
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "search",
|
||||||
|
category = "Search",
|
||||||
|
guild_only = true
|
||||||
|
)]
|
||||||
pub async fn search_sounds(
|
pub async fn search_sounds(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Sound name to search for"] query: String,
|
#[description = "Sound name to search for"] query: String,
|
||||||
@ -123,7 +275,7 @@ pub async fn search_sounds(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Show a page of random sounds
|
/// Show a page of random sounds
|
||||||
#[poise::command(slash_command, rename = "random")]
|
#[poise::command(slash_command, rename = "random", guild_only = true)]
|
||||||
pub async fn show_random_sounds(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn show_random_sounds(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let search_results = sqlx::query_as_unchecked!(
|
let search_results = sqlx::query_as_unchecked!(
|
||||||
Sound,
|
Sound,
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
|
use poise::serenity_prelude::{GuildId, User};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{guild_data::CtxGuildData, join_sound::JoinSoundCtx, sound::SoundCtx},
|
cmds::autocomplete_sound,
|
||||||
|
models::{
|
||||||
|
guild_data::{AllowGreet, CtxGuildData},
|
||||||
|
join_sound::JoinSoundCtx,
|
||||||
|
sound::SoundCtx,
|
||||||
|
},
|
||||||
Context, Error,
|
Context, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Change the bot's volume in this server
|
/// Change the bot's volume in this server
|
||||||
#[poise::command(slash_command, rename = "volume")]
|
#[poise::command(slash_command, rename = "volume", guild_only = true)]
|
||||||
pub async fn change_volume(
|
pub async fn change_volume(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "New volume as a percentage"] volume: Option<usize>,
|
#[description = "New volume as a percentage"] volume: Option<usize>,
|
||||||
@ -31,18 +38,44 @@ pub async fn change_volume(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manage greet sounds on this server
|
/// Manage greet sounds
|
||||||
#[poise::command(slash_command, rename = "greet")]
|
#[poise::command(slash_command, rename = "greet", guild_only = true)]
|
||||||
pub async fn greet_sound(_ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn greet_sound(_ctx: Context<'_>) -> Result<(), Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a join sound
|
/// Manage greet sounds in this server
|
||||||
|
#[poise::command(slash_command, rename = "server")]
|
||||||
|
pub async fn guild_greet_sound(_ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a user's server-specific join sound
|
||||||
#[poise::command(slash_command, rename = "set")]
|
#[poise::command(slash_command, rename = "set")]
|
||||||
pub async fn set_greet_sound(
|
pub async fn set_guild_greet_sound(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Name or ID of sound to set as your join sound"] name: String,
|
#[description = "Name or ID of sound to set as join sound"]
|
||||||
|
#[autocomplete = "autocomplete_sound"]
|
||||||
|
name: String,
|
||||||
|
#[description = "User to set join sound for"] user: User,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
if user.id != ctx.author().id {
|
||||||
|
let guild = ctx.guild().unwrap();
|
||||||
|
let permissions = guild
|
||||||
|
.member_permissions(&ctx.discord(), ctx.author().id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if permissions.map_or(true, |p| !p.manage_guild()) {
|
||||||
|
ctx.send(|b| {
|
||||||
|
b.ephemeral(true)
|
||||||
|
.content("Only admins can change other user's greet sounds.")
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let sound_vec = ctx
|
let sound_vec = ctx
|
||||||
.data()
|
.data()
|
||||||
.search_for_sound(&name, ctx.guild_id().unwrap(), ctx.author().id, true)
|
.search_for_sound(&name, ctx.guild_id().unwrap(), ctx.author().id, true)
|
||||||
@ -51,8 +84,8 @@ pub async fn set_greet_sound(
|
|||||||
match sound_vec.first() {
|
match sound_vec.first() {
|
||||||
Some(sound) => {
|
Some(sound) => {
|
||||||
ctx.data()
|
ctx.data()
|
||||||
.update_join_sound(ctx.author().id, Some(sound.id))
|
.update_join_sound(user.id, ctx.guild_id(), Some(sound.id))
|
||||||
.await;
|
.await?;
|
||||||
|
|
||||||
ctx.say(format!(
|
ctx.say(format!(
|
||||||
"Greet sound has been set to {} (ID {})",
|
"Greet sound has been set to {} (ID {})",
|
||||||
@ -69,23 +102,109 @@ pub async fn set_greet_sound(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a join sound
|
/// Unset a user's server-specific join sound
|
||||||
#[poise::command(slash_command, rename = "unset")]
|
#[poise::command(slash_command, rename = "unset", guild_only = true)]
|
||||||
pub async fn unset_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn unset_guild_greet_sound(
|
||||||
ctx.data().update_join_sound(ctx.author().id, None).await;
|
ctx: Context<'_>,
|
||||||
|
#[description = "User to set join sound for"] user: User,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if user.id != ctx.author().id {
|
||||||
|
let guild = ctx.guild().unwrap();
|
||||||
|
let permissions = guild
|
||||||
|
.member_permissions(&ctx.discord(), ctx.author().id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if permissions.map_or(true, |p| !p.manage_guild()) {
|
||||||
|
ctx.send(|b| {
|
||||||
|
b.ephemeral(true)
|
||||||
|
.content("Only admins can change other user's greet sounds.")
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.data()
|
||||||
|
.update_join_sound(user.id, ctx.guild_id(), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
ctx.say("Greet sound has been unset").await?;
|
ctx.say("Greet sound has been unset").await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disable greet sounds on this server
|
/// Manage your own greet sound
|
||||||
#[poise::command(slash_command, rename = "disable")]
|
#[poise::command(slash_command, rename = "user")]
|
||||||
|
pub async fn user_greet_sound(_ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set your global join sound
|
||||||
|
#[poise::command(slash_command, rename = "set")]
|
||||||
|
pub async fn set_user_greet_sound(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Name or ID of sound to set as your join sound"]
|
||||||
|
#[autocomplete = "autocomplete_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.data()
|
||||||
|
.update_join_sound(ctx.author().id, None::<GuildId>, Some(sound.id))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
ctx.send(|b| {
|
||||||
|
b.ephemeral(true).content(format!(
|
||||||
|
"Greet sound has been set to {} (ID {})",
|
||||||
|
sound.name, sound.id
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
ctx.send(|b| {
|
||||||
|
b.ephemeral(true)
|
||||||
|
.content("Could not find a sound by that name.")
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unset your global join sound
|
||||||
|
#[poise::command(slash_command, rename = "unset", guild_only = true)]
|
||||||
|
pub async fn unset_user_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
ctx.data()
|
||||||
|
.update_join_sound(ctx.author().id, None::<GuildId>, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
ctx.send(|b| b.ephemeral(true).content("Greet sound has been unset"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disable all greet sounds on this server
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "disable",
|
||||||
|
guild_only = true,
|
||||||
|
required_permissions = "MANAGE_GUILD"
|
||||||
|
)]
|
||||||
pub async fn disable_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn disable_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let guild_data_opt = ctx.guild_data(ctx.guild_id().unwrap()).await;
|
let guild_data_opt = ctx.guild_data(ctx.guild_id().unwrap()).await;
|
||||||
|
|
||||||
if let Ok(guild_data) = guild_data_opt {
|
if let Ok(guild_data) = guild_data_opt {
|
||||||
guild_data.write().await.allow_greets = false;
|
guild_data.write().await.allow_greets = AllowGreet::Disabled;
|
||||||
|
|
||||||
guild_data.read().await.commit(&ctx.data().database).await?;
|
guild_data.read().await.commit(&ctx.data().database).await?;
|
||||||
}
|
}
|
||||||
@ -96,13 +215,40 @@ pub async fn disable_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable greet sounds on this server
|
/// Enable only server greet sounds on this server
|
||||||
#[poise::command(slash_command, rename = "enable")]
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "enable",
|
||||||
|
guild_only = true,
|
||||||
|
required_permissions = "MANAGE_GUILD"
|
||||||
|
)]
|
||||||
|
pub async fn enable_guild_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 = AllowGreet::GuildOnly;
|
||||||
|
|
||||||
|
guild_data.read().await.commit(&ctx.data().database).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.say("Greet sounds have been partially enable in this server. Use \"/greet server set\" to configure server greet sounds.")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable all greet sounds on this server
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "enable",
|
||||||
|
guild_only = true,
|
||||||
|
required_permissions = "MANAGE_GUILD"
|
||||||
|
)]
|
||||||
pub async fn enable_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn enable_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let guild_data_opt = ctx.guild_data(ctx.guild_id().unwrap()).await;
|
let guild_data_opt = ctx.guild_data(ctx.guild_id().unwrap()).await;
|
||||||
|
|
||||||
if let Ok(guild_data) = guild_data_opt {
|
if let Ok(guild_data) = guild_data_opt {
|
||||||
guild_data.write().await.allow_greets = true;
|
guild_data.write().await.allow_greets = AllowGreet::Enabled;
|
||||||
|
|
||||||
guild_data.read().await.commit(&ctx.data().database).await?;
|
guild_data.read().await.commit(&ctx.data().database).await?;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,12 @@ use songbird;
|
|||||||
use crate::{Context, Error};
|
use crate::{Context, Error};
|
||||||
|
|
||||||
/// Stop the bot from playing and clear the play queue
|
/// Stop the bot from playing and clear the play queue
|
||||||
#[poise::command(slash_command, rename = "stop", required_permissions = "MANAGE_GUILD")]
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "stop",
|
||||||
|
default_member_permissions = "SPEAK",
|
||||||
|
guild_only = true
|
||||||
|
)]
|
||||||
pub async fn stop_playing(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn stop_playing(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let songbird = songbird::get(ctx.discord()).await.unwrap();
|
let songbird = songbird::get(ctx.discord()).await.unwrap();
|
||||||
let call_opt = songbird.get(ctx.guild_id().unwrap());
|
let call_opt = songbird.get(ctx.guild_id().unwrap());
|
||||||
@ -20,7 +25,7 @@ pub async fn stop_playing(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Disconnect the bot
|
/// Disconnect the bot
|
||||||
#[poise::command(slash_command, required_permissions = "SPEAK")]
|
#[poise::command(slash_command, default_member_permissions = "SPEAK", guild_only = true)]
|
||||||
pub async fn disconnect(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn disconnect(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let songbird = songbird::get(ctx.discord()).await.unwrap();
|
let songbird = songbird::get(ctx.discord()).await.unwrap();
|
||||||
let _ = songbird.leave(ctx.guild_id().unwrap()).await;
|
let _ = songbird.leave(ctx.guild_id().unwrap()).await;
|
||||||
|
@ -7,7 +7,10 @@ lazy_static! {
|
|||||||
.unwrap_or_else(|_| "2097152".to_string())
|
.unwrap_or_else(|_| "2097152".to_string())
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
pub static ref MAX_SOUNDS: u32 = env::var("MAX_SOUNDS").unwrap().parse::<u32>().unwrap();
|
pub static ref MAX_SOUNDS: u32 = env::var("MAX_SOUNDS")
|
||||||
|
.unwrap_or_else(|_| "8".to_string())
|
||||||
|
.parse::<u32>()
|
||||||
|
.unwrap();
|
||||||
pub static ref PATREON_GUILD: u64 = env::var("PATREON_GUILD").unwrap().parse::<u64>().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();
|
pub static ref PATREON_ROLE: u64 = env::var("PATREON_ROLE").unwrap().parse::<u64>().unwrap();
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,30 @@
|
|||||||
use std::{collections::HashMap, env};
|
use std::{collections::HashMap, env};
|
||||||
|
|
||||||
use poise::serenity::{
|
use poise::serenity_prelude::{
|
||||||
model::{
|
model::{
|
||||||
|
application::interaction::{Interaction, InteractionResponseType},
|
||||||
channel::Channel,
|
channel::Channel,
|
||||||
interactions::{Interaction, InteractionResponseType},
|
|
||||||
},
|
},
|
||||||
prelude::Context,
|
|
||||||
utils::shard_id,
|
utils::shard_id,
|
||||||
|
Activity, Context,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{guild_data::CtxGuildData, join_sound::JoinSoundCtx, sound::Sound},
|
cmds::search::SoundPager,
|
||||||
|
models::{
|
||||||
|
guild_data::{AllowGreet, CtxGuildData},
|
||||||
|
join_sound::JoinSoundCtx,
|
||||||
|
sound::Sound,
|
||||||
|
},
|
||||||
utils::{join_channel, play_audio, play_from_query},
|
utils::{join_channel, play_audio, play_from_query},
|
||||||
Data, Error,
|
Data, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> {
|
pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> {
|
||||||
match event {
|
match event {
|
||||||
|
poise::Event::Ready { .. } => {
|
||||||
|
ctx.set_activity(Activity::watching("for /play")).await;
|
||||||
|
}
|
||||||
poise::Event::GuildCreate { guild, is_new, .. } => {
|
poise::Event::GuildCreate { guild, is_new, .. } => {
|
||||||
if *is_new {
|
if *is_new {
|
||||||
if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
|
if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
|
||||||
@ -85,8 +93,15 @@ pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> R
|
|||||||
allowed_greets = read.allow_greets;
|
allowed_greets = read.allow_greets;
|
||||||
}
|
}
|
||||||
|
|
||||||
if allowed_greets {
|
if allowed_greets != AllowGreet::Disabled {
|
||||||
if let Some(join_id) = data.join_sound(new.user_id).await {
|
if let Some(join_id) = data
|
||||||
|
.join_sound(
|
||||||
|
new.user_id,
|
||||||
|
new.guild_id,
|
||||||
|
allowed_greets == AllowGreet::GuildOnly,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
let mut sound = sqlx::query_as_unchecked!(
|
let mut sound = sqlx::query_as_unchecked!(
|
||||||
Sound,
|
Sound,
|
||||||
"
|
"
|
||||||
@ -119,23 +134,27 @@ SELECT name, id, public, server_id, uploader_id
|
|||||||
}
|
}
|
||||||
poise::Event::InteractionCreate { interaction } => match interaction {
|
poise::Event::InteractionCreate { interaction } => match interaction {
|
||||||
Interaction::MessageComponent(component) => {
|
Interaction::MessageComponent(component) => {
|
||||||
if component.guild_id.is_some() {
|
if let Some(guild_id) = component.guild_id {
|
||||||
play_from_query(
|
if let Ok(()) = SoundPager::handle_interaction(ctx, &data, component).await {
|
||||||
&ctx,
|
} else {
|
||||||
&data,
|
|
||||||
component.guild_id.unwrap().to_guild_cached(&ctx).unwrap(),
|
|
||||||
component.user.id,
|
|
||||||
&component.data.custom_id,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
component
|
component
|
||||||
.create_interaction_response(ctx, |r| {
|
.create_interaction_response(ctx, |r| {
|
||||||
r.kind(InteractionResponseType::DeferredUpdateMessage)
|
r.kind(InteractionResponseType::DeferredUpdateMessage)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
play_from_query(
|
||||||
|
&ctx,
|
||||||
|
&data,
|
||||||
|
guild_id.to_guild_cached(&ctx).unwrap(),
|
||||||
|
component.user.id,
|
||||||
|
None,
|
||||||
|
&component.data.custom_id,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
54
src/main.rs
54
src/main.rs
@ -8,14 +8,13 @@ mod event_handlers;
|
|||||||
mod models;
|
mod models;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use std::{env, sync::Arc};
|
use std::{env, path::Path, sync::Arc};
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use dotenv::dotenv;
|
use poise::serenity_prelude::{
|
||||||
use poise::serenity::{
|
|
||||||
builder::CreateApplicationCommands,
|
builder::CreateApplicationCommands,
|
||||||
model::{
|
model::{
|
||||||
gateway::{Activity, GatewayIntents},
|
gateway::GatewayIntents,
|
||||||
id::{GuildId, UserId},
|
id::{GuildId, UserId},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,24 +24,23 @@ use tokio::sync::RwLock;
|
|||||||
|
|
||||||
use crate::{event_handlers::listener, models::guild_data::GuildData};
|
use crate::{event_handlers::listener, models::guild_data::GuildData};
|
||||||
|
|
||||||
// Which database driver are we using?
|
|
||||||
type Database = MySql;
|
type Database = MySql;
|
||||||
|
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
database: Pool<Database>,
|
database: Pool<Database>,
|
||||||
http: reqwest::Client,
|
http: reqwest::Client,
|
||||||
guild_data_cache: DashMap<GuildId, Arc<RwLock<GuildData>>>,
|
guild_data_cache: DashMap<GuildId, Arc<RwLock<GuildData>>>,
|
||||||
join_sound_cache: DashMap<UserId, Option<u32>>,
|
join_sound_cache: DashMap<UserId, DashMap<Option<GuildId>, Option<u32>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
type Context<'a> = poise::Context<'a, Data, Error>;
|
type Context<'a> = poise::Context<'a, Data, Error>;
|
||||||
|
|
||||||
pub async fn register_application_commands(
|
pub async fn register_application_commands(
|
||||||
ctx: &poise::serenity::client::Context,
|
ctx: &poise::serenity_prelude::Context,
|
||||||
framework: &poise::Framework<Data, Error>,
|
framework: &poise::Framework<Data, Error>,
|
||||||
guild_id: Option<GuildId>,
|
guild_id: Option<GuildId>,
|
||||||
) -> Result<(), poise::serenity::Error> {
|
) -> Result<(), poise::serenity_prelude::Error> {
|
||||||
let mut commands_builder = CreateApplicationCommands::default();
|
let mut commands_builder = CreateApplicationCommands::default();
|
||||||
let commands = &framework.options().commands;
|
let commands = &framework.options().commands;
|
||||||
for command in commands {
|
for command in commands {
|
||||||
@ -53,7 +51,7 @@ pub async fn register_application_commands(
|
|||||||
commands_builder.add_application_command(context_menu_command);
|
commands_builder.add_application_command(context_menu_command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let commands_builder = poise::serenity::json::Value::Array(commands_builder.0);
|
let commands_builder = poise::serenity_prelude::json::Value::Array(commands_builder.0);
|
||||||
|
|
||||||
if let Some(guild_id) = guild_id {
|
if let Some(guild_id) = guild_id {
|
||||||
ctx.http
|
ctx.http
|
||||||
@ -68,12 +66,13 @@ pub async fn register_application_commands(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// entry point
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
env_logger::init();
|
if Path::new("/etc/soundfx-rs/config.env").exists() {
|
||||||
|
dotenv::from_path("/etc/soundfx-rs/config.env").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
dotenv()?;
|
env_logger::init();
|
||||||
|
|
||||||
let discord_token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment");
|
let discord_token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment");
|
||||||
|
|
||||||
@ -103,10 +102,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
cmds::settings::change_volume(),
|
cmds::settings::change_volume(),
|
||||||
poise::Command {
|
poise::Command {
|
||||||
subcommands: vec![
|
subcommands: vec![
|
||||||
|
poise::Command {
|
||||||
|
subcommands: vec![
|
||||||
|
cmds::settings::set_guild_greet_sound(),
|
||||||
|
cmds::settings::unset_guild_greet_sound(),
|
||||||
|
cmds::settings::enable_guild_greet_sound(),
|
||||||
|
],
|
||||||
|
..cmds::settings::guild_greet_sound()
|
||||||
|
},
|
||||||
|
poise::Command {
|
||||||
|
subcommands: vec![
|
||||||
|
cmds::settings::set_user_greet_sound(),
|
||||||
|
cmds::settings::unset_user_greet_sound(),
|
||||||
|
],
|
||||||
|
..cmds::settings::user_greet_sound()
|
||||||
|
},
|
||||||
cmds::settings::disable_greet_sound(),
|
cmds::settings::disable_greet_sound(),
|
||||||
cmds::settings::enable_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()
|
||||||
},
|
},
|
||||||
@ -120,19 +132,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
poise::Framework::build()
|
sqlx::migrate!().run(&database).await?;
|
||||||
|
|
||||||
|
poise::Framework::builder()
|
||||||
.token(discord_token)
|
.token(discord_token)
|
||||||
.user_data_setup(move |ctx, _bot, framework| {
|
.user_data_setup(move |ctx, _bot, framework| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
ctx.set_activity(Activity::watching("for /play")).await;
|
register_application_commands(ctx, framework, None)
|
||||||
|
|
||||||
register_application_commands(
|
|
||||||
ctx,
|
|
||||||
framework,
|
|
||||||
env::var("DEBUG_GUILD")
|
|
||||||
.map(|inner| GuildId(inner.parse().expect("DEBUG_GUILD not valid")))
|
|
||||||
.ok(),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use poise::serenity::{async_trait, model::id::GuildId};
|
use poise::serenity_prelude::{async_trait, model::id::GuildId};
|
||||||
use sqlx::Executor;
|
use sqlx::{Executor, Type};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::{Context, Data, Database};
|
use crate::{Context, Data, Database};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Type, PartialEq)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum AllowGreet {
|
||||||
|
Enabled = 1,
|
||||||
|
GuildOnly = 0,
|
||||||
|
Disabled = -1,
|
||||||
|
}
|
||||||
|
|
||||||
#[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: AllowGreet,
|
||||||
pub allowed_role: Option<u64>,
|
pub allowed_role: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +117,7 @@ INSERT INTO servers (id)
|
|||||||
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: AllowGreet::Enabled,
|
||||||
allowed_role: None,
|
allowed_role: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,45 +1,90 @@
|
|||||||
use poise::serenity::{async_trait, model::id::UserId};
|
use poise::serenity_prelude::{async_trait, model::id::UserId, GuildId};
|
||||||
|
|
||||||
use crate::Data;
|
use crate::Data;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait JoinSoundCtx {
|
pub trait JoinSoundCtx {
|
||||||
async fn join_sound<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Option<u32>;
|
async fn join_sound<U: Into<UserId> + Send + Sync, G: Into<GuildId> + Send + Sync>(
|
||||||
async fn update_join_sound<U: Into<UserId> + Send + Sync>(
|
|
||||||
&self,
|
&self,
|
||||||
user_id: U,
|
user_id: U,
|
||||||
|
guild_id: Option<G>,
|
||||||
|
guild_only: bool,
|
||||||
|
) -> Option<u32>;
|
||||||
|
async fn update_join_sound<U: Into<UserId> + Send + Sync, G: Into<GuildId> + Send + Sync>(
|
||||||
|
&self,
|
||||||
|
user_id: U,
|
||||||
|
guild_id: Option<G>,
|
||||||
join_id: Option<u32>,
|
join_id: Option<u32>,
|
||||||
);
|
) -> Result<(), sqlx::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct JoinSound {
|
||||||
|
join_sound_id: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl JoinSoundCtx for Data {
|
impl JoinSoundCtx for Data {
|
||||||
async fn join_sound<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Option<u32> {
|
async fn join_sound<U: Into<UserId> + Send + Sync, G: Into<GuildId> + Send + Sync>(
|
||||||
|
&self,
|
||||||
|
user_id: U,
|
||||||
|
guild_id: Option<G>,
|
||||||
|
guild_only: bool,
|
||||||
|
) -> Option<u32> {
|
||||||
let user_id = user_id.into();
|
let user_id = user_id.into();
|
||||||
|
let guild_id = guild_id.map(|g| g.into());
|
||||||
|
|
||||||
let x = if let Some(join_sound_id) = self.join_sound_cache.get(&user_id) {
|
let cached_join_id = self
|
||||||
join_sound_id.value().clone()
|
.join_sound_cache
|
||||||
|
.get(&user_id)
|
||||||
|
.map(|d| d.get(&guild_id).map(|i| i.value().clone()))
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
let x = if let Some(join_sound_id) = cached_join_id {
|
||||||
|
join_sound_id
|
||||||
} else {
|
} else {
|
||||||
let join_sound_id = {
|
let join_sound_id = {
|
||||||
let join_id_res = sqlx::query!(
|
let join_id_res = if guild_only {
|
||||||
|
sqlx::query_as!(
|
||||||
|
JoinSound,
|
||||||
"
|
"
|
||||||
SELECT join_sound_id
|
SELECT join_sound_id
|
||||||
FROM users
|
FROM join_sounds
|
||||||
WHERE user = ?
|
WHERE user = ?
|
||||||
|
AND guild = ?
|
||||||
|
ORDER BY guild IS NULL
|
||||||
",
|
",
|
||||||
user_id.as_u64()
|
user_id.as_u64(),
|
||||||
|
guild_id.map(|g| g.0)
|
||||||
)
|
)
|
||||||
.fetch_one(&self.database)
|
.fetch_one(&self.database)
|
||||||
.await;
|
.await
|
||||||
|
} else {
|
||||||
|
sqlx::query_as!(
|
||||||
|
JoinSound,
|
||||||
|
"
|
||||||
|
SELECT join_sound_id
|
||||||
|
FROM join_sounds
|
||||||
|
WHERE user = ?
|
||||||
|
AND (guild IS NULL OR guild = ?)
|
||||||
|
ORDER BY guild IS NULL
|
||||||
|
",
|
||||||
|
user_id.as_u64(),
|
||||||
|
guild_id.map(|g| g.0)
|
||||||
|
)
|
||||||
|
.fetch_one(&self.database)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
if let Ok(row) = join_id_res {
|
if let Ok(row) = join_id_res {
|
||||||
row.join_sound_id
|
Some(row.join_sound_id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.join_sound_cache.insert(user_id, join_sound_id);
|
self.join_sound_cache.entry(user_id).and_modify(|d| {
|
||||||
|
d.insert(guild_id, join_sound_id);
|
||||||
|
});
|
||||||
|
|
||||||
join_sound_id
|
join_sound_id
|
||||||
};
|
};
|
||||||
@ -47,39 +92,54 @@ SELECT join_sound_id
|
|||||||
x
|
x
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_join_sound<U: Into<UserId> + Send + Sync>(
|
async fn update_join_sound<U: Into<UserId> + Send + Sync, G: Into<GuildId> + Send + Sync>(
|
||||||
&self,
|
&self,
|
||||||
user_id: U,
|
user_id: U,
|
||||||
|
guild_id: Option<G>,
|
||||||
join_id: Option<u32>,
|
join_id: Option<u32>,
|
||||||
) {
|
) -> Result<(), sqlx::Error> {
|
||||||
let user_id = user_id.into();
|
let user_id = user_id.into();
|
||||||
|
let guild_id = guild_id.map(|g| g.into());
|
||||||
|
|
||||||
self.join_sound_cache.insert(user_id, join_id);
|
self.join_sound_cache.entry(user_id).and_modify(|d| {
|
||||||
|
d.insert(guild_id, join_id);
|
||||||
|
});
|
||||||
|
|
||||||
let pool = self.database.clone();
|
let mut transaction = self.database.begin().await?;
|
||||||
|
|
||||||
let _ = sqlx::query!(
|
match join_id {
|
||||||
"
|
Some(join_id) => {
|
||||||
INSERT IGNORE INTO users (user)
|
sqlx::query!(
|
||||||
VALUES (?)
|
"DELETE FROM join_sounds WHERE user = ? AND guild <=> ?",
|
||||||
",
|
user_id.0,
|
||||||
user_id.as_u64()
|
guild_id.map(|g| g.0)
|
||||||
)
|
)
|
||||||
.execute(&pool)
|
.execute(&mut transaction)
|
||||||
.await;
|
.await?;
|
||||||
|
|
||||||
let _ = sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"INSERT INTO join_sounds (user, join_sound_id, guild) VALUES (?, ?, ?)",
|
||||||
UPDATE users
|
user_id.0,
|
||||||
SET
|
|
||||||
join_sound_id = ?
|
|
||||||
WHERE
|
|
||||||
user = ?
|
|
||||||
",
|
|
||||||
join_id,
|
join_id,
|
||||||
user_id.as_u64()
|
guild_id.map(|g| g.0)
|
||||||
)
|
)
|
||||||
.execute(&pool)
|
.execute(&mut transaction)
|
||||||
.await;
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
sqlx::query!(
|
||||||
|
"DELETE FROM join_sounds WHERE user = ? AND guild <=> ?",
|
||||||
|
user_id.0,
|
||||||
|
guild_id.map(|g| g.0)
|
||||||
|
)
|
||||||
|
.execute(&mut transaction)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use std::{env, path::Path};
|
use std::{env, path::Path};
|
||||||
|
|
||||||
use poise::serenity::async_trait;
|
use poise::serenity_prelude::async_trait;
|
||||||
use songbird::input::restartable::Restartable;
|
use songbird::input::restartable::Restartable;
|
||||||
use sqlx::{Error, Executor};
|
use sqlx::Executor;
|
||||||
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
|
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
|
||||||
|
|
||||||
use crate::{consts::UPLOAD_MAX_SIZE, error::ErrorTypes, Data, Database};
|
use crate::{consts::UPLOAD_MAX_SIZE, error::ErrorTypes, Data, Database};
|
||||||
@ -37,12 +37,21 @@ pub trait SoundCtx {
|
|||||||
user_id: U,
|
user_id: U,
|
||||||
guild_id: G,
|
guild_id: G,
|
||||||
) -> Result<Vec<Sound>, sqlx::Error>;
|
) -> Result<Vec<Sound>, sqlx::Error>;
|
||||||
async fn user_sounds<U: Into<u64> + Send>(&self, user_id: U)
|
async fn user_sounds<U: Into<u64> + Send>(
|
||||||
-> Result<Vec<Sound>, sqlx::Error>;
|
&self,
|
||||||
|
user_id: U,
|
||||||
|
page: Option<u64>,
|
||||||
|
) -> Result<Vec<Sound>, sqlx::Error>;
|
||||||
async fn guild_sounds<G: Into<u64> + Send>(
|
async fn guild_sounds<G: Into<u64> + Send>(
|
||||||
&self,
|
&self,
|
||||||
guild_id: G,
|
guild_id: G,
|
||||||
|
page: Option<u64>,
|
||||||
) -> Result<Vec<Sound>, sqlx::Error>;
|
) -> Result<Vec<Sound>, sqlx::Error>;
|
||||||
|
async fn count_user_sounds<U: Into<u64> + Send>(&self, user_id: U) -> Result<u64, sqlx::Error>;
|
||||||
|
async fn count_guild_sounds<G: Into<u64> + Send>(
|
||||||
|
&self,
|
||||||
|
guild_id: G,
|
||||||
|
) -> Result<u64, sqlx::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -149,7 +158,7 @@ SELECT name, id, public, server_id, uploader_id
|
|||||||
query: &str,
|
query: &str,
|
||||||
user_id: U,
|
user_id: U,
|
||||||
guild_id: G,
|
guild_id: G,
|
||||||
) -> Result<Vec<Sound>, Error> {
|
) -> Result<Vec<Sound>, sqlx::Error> {
|
||||||
let db_pool = self.database.clone();
|
let db_pool = self.database.clone();
|
||||||
|
|
||||||
sqlx::query_as_unchecked!(
|
sqlx::query_as_unchecked!(
|
||||||
@ -171,18 +180,41 @@ LIMIT 25
|
|||||||
async fn user_sounds<U: Into<u64> + Send>(
|
async fn user_sounds<U: Into<u64> + Send>(
|
||||||
&self,
|
&self,
|
||||||
user_id: U,
|
user_id: U,
|
||||||
|
page: Option<u64>,
|
||||||
) -> Result<Vec<Sound>, sqlx::Error> {
|
) -> Result<Vec<Sound>, sqlx::Error> {
|
||||||
let sounds = sqlx::query_as_unchecked!(
|
let sounds = match page {
|
||||||
|
Some(page) => {
|
||||||
|
sqlx::query_as_unchecked!(
|
||||||
Sound,
|
Sound,
|
||||||
"
|
"
|
||||||
SELECT name, id, public, server_id, uploader_id
|
SELECT name, id, public, server_id, uploader_id
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE uploader_id = ?
|
WHERE uploader_id = ?
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT ?, ?
|
||||||
|
",
|
||||||
|
user_id.into(),
|
||||||
|
page * 25,
|
||||||
|
(page + 1) * 25
|
||||||
|
)
|
||||||
|
.fetch_all(&self.database)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
sqlx::query_as_unchecked!(
|
||||||
|
Sound,
|
||||||
|
"
|
||||||
|
SELECT name, id, public, server_id, uploader_id
|
||||||
|
FROM sounds
|
||||||
|
WHERE uploader_id = ?
|
||||||
|
ORDER BY id DESC
|
||||||
",
|
",
|
||||||
user_id.into()
|
user_id.into()
|
||||||
)
|
)
|
||||||
.fetch_all(&self.database)
|
.fetch_all(&self.database)
|
||||||
.await?;
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(sounds)
|
Ok(sounds)
|
||||||
}
|
}
|
||||||
@ -190,21 +222,68 @@ SELECT name, id, public, server_id, uploader_id
|
|||||||
async fn guild_sounds<G: Into<u64> + Send>(
|
async fn guild_sounds<G: Into<u64> + Send>(
|
||||||
&self,
|
&self,
|
||||||
guild_id: G,
|
guild_id: G,
|
||||||
|
page: Option<u64>,
|
||||||
) -> Result<Vec<Sound>, sqlx::Error> {
|
) -> Result<Vec<Sound>, sqlx::Error> {
|
||||||
let sounds = sqlx::query_as_unchecked!(
|
let sounds = match page {
|
||||||
|
Some(page) => {
|
||||||
|
sqlx::query_as_unchecked!(
|
||||||
Sound,
|
Sound,
|
||||||
"
|
"
|
||||||
SELECT name, id, public, server_id, uploader_id
|
SELECT name, id, public, server_id, uploader_id
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE server_id = ?
|
WHERE server_id = ?
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT ?, ?
|
||||||
|
",
|
||||||
|
guild_id.into(),
|
||||||
|
page * 25,
|
||||||
|
(page + 1) * 25
|
||||||
|
)
|
||||||
|
.fetch_all(&self.database)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
sqlx::query_as_unchecked!(
|
||||||
|
Sound,
|
||||||
|
"
|
||||||
|
SELECT name, id, public, server_id, uploader_id
|
||||||
|
FROM sounds
|
||||||
|
WHERE server_id = ?
|
||||||
|
ORDER BY id DESC
|
||||||
",
|
",
|
||||||
guild_id.into()
|
guild_id.into()
|
||||||
)
|
)
|
||||||
.fetch_all(&self.database)
|
.fetch_all(&self.database)
|
||||||
.await?;
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(sounds)
|
Ok(sounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn count_user_sounds<U: Into<u64> + Send>(&self, user_id: U) -> Result<u64, sqlx::Error> {
|
||||||
|
Ok(sqlx::query!(
|
||||||
|
"SELECT COUNT(1) as count FROM sounds WHERE uploader_id = ?",
|
||||||
|
user_id.into()
|
||||||
|
)
|
||||||
|
.fetch_one(&self.database)
|
||||||
|
.await?
|
||||||
|
.count as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn count_guild_sounds<G: Into<u64> + Send>(
|
||||||
|
&self,
|
||||||
|
guild_id: G,
|
||||||
|
) -> Result<u64, sqlx::Error> {
|
||||||
|
Ok(sqlx::query!(
|
||||||
|
"SELECT COUNT(1) as count FROM sounds WHERE server_id = ?",
|
||||||
|
guild_id.into()
|
||||||
|
)
|
||||||
|
.fetch_one(&self.database)
|
||||||
|
.await?
|
||||||
|
.count as u64)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sound {
|
impl Sound {
|
||||||
@ -262,7 +341,7 @@ SELECT src
|
|||||||
pub async fn count_user_sounds<U: Into<u64>>(
|
pub async fn count_user_sounds<U: Into<u64>>(
|
||||||
user_id: U,
|
user_id: U,
|
||||||
db_pool: impl Executor<'_, Database = Database>,
|
db_pool: impl Executor<'_, Database = Database>,
|
||||||
) -> Result<u32, sqlx::error::Error> {
|
) -> Result<u32, sqlx::Error> {
|
||||||
let user_id = user_id.into();
|
let user_id = user_id.into();
|
||||||
|
|
||||||
let c = sqlx::query!(
|
let c = sqlx::query!(
|
||||||
@ -284,7 +363,7 @@ SELECT COUNT(1) as count
|
|||||||
user_id: U,
|
user_id: U,
|
||||||
name: &String,
|
name: &String,
|
||||||
db_pool: impl Executor<'_, Database = Database>,
|
db_pool: impl Executor<'_, Database = Database>,
|
||||||
) -> Result<u32, sqlx::error::Error> {
|
) -> Result<u32, sqlx::Error> {
|
||||||
let user_id = user_id.into();
|
let user_id = user_id.into();
|
||||||
|
|
||||||
let c = sqlx::query!(
|
let c = sqlx::query!(
|
||||||
@ -330,14 +409,7 @@ WHERE
|
|||||||
&self,
|
&self,
|
||||||
db_pool: impl Executor<'_, Database = Database>,
|
db_pool: impl Executor<'_, Database = Database>,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
sqlx::query!(
|
sqlx::query!("DELETE FROM sounds WHERE id = ?", self.id)
|
||||||
"
|
|
||||||
DELETE
|
|
||||||
FROM sounds
|
|
||||||
WHERE id = ?
|
|
||||||
",
|
|
||||||
self.id
|
|
||||||
)
|
|
||||||
.execute(db_pool)
|
.execute(db_pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use poise::serenity::model::{
|
use poise::serenity_prelude::model::{
|
||||||
channel::Channel,
|
channel::Channel,
|
||||||
guild::Guild,
|
guild::Guild,
|
||||||
id::{ChannelId, UserId},
|
id::{ChannelId, UserId},
|
||||||
@ -104,15 +104,18 @@ pub async fn play_from_query(
|
|||||||
data: &Data,
|
data: &Data,
|
||||||
guild: Guild,
|
guild: Guild,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
|
channel: Option<ChannelId>,
|
||||||
query: &str,
|
query: &str,
|
||||||
loop_: bool,
|
loop_: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
let guild_id = guild.id;
|
let guild_id = guild.id;
|
||||||
|
|
||||||
let channel_to_join = guild
|
let channel_to_join = channel.or_else(|| {
|
||||||
|
guild
|
||||||
.voice_states
|
.voice_states
|
||||||
.get(&user_id)
|
.get(&user_id)
|
||||||
.and_then(|voice_state| voice_state.channel_id);
|
.and_then(|voice_state| voice_state.channel_id)
|
||||||
|
});
|
||||||
|
|
||||||
match channel_to_join {
|
match channel_to_join {
|
||||||
Some(user_channel) => {
|
Some(user_channel) => {
|
||||||
|
15
systemd/soundfx-rs.service
Normal file
15
systemd/soundfx-rs.service
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Discord bot for custom sound effects and soundboards
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=soundfx
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/soundfx-rs
|
||||||
|
WorkingDirectory=/etc/soundfx-rs
|
||||||
|
Restart=always
|
||||||
|
RestartSec=4
|
||||||
|
# Environment="RUST_LOG=warn,soundfx_rs=info"
|
||||||
|
# Environment="RUST_BACKTRACE=full"
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
Reference in New Issue
Block a user