Compare commits
	
		
			51 Commits
		
	
	
		
			soundboard
			...
			66f45f11f2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 66f45f11f2 | ||
|  | e30a08e019 | ||
|  | 80f45a1f5c | ||
|  | 1a1b1b8144 | ||
| 34d5fddf6c | |||
| ac41dbce0c | |||
| 208d169c76 | |||
|  | 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 | ||
|  | d82cbf2fd6 | ||
|  | 0df5466052 | ||
|  | febeeefb01 | ||
| 821f283969 | |||
| eb5ea3167d | |||
| c364343fe9 | |||
| d59c50e7a9 | |||
| b350007dae | |||
| bb54c0d2c0 | |||
| 53a8bb3127 | |||
| 4d8f1acb57 | 
| @@ -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"?> | ||||
| <project version="4"> | ||||
|   <component name="dataSourceStorageLocal" created-in="CL-211.7442.42"> | ||||
|   <component name="dataSourceStorageLocal" created-in="CL-231.9161.40"> | ||||
|     <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" /> | ||||
|       <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"?> | ||||
| <project version="4"> | ||||
|   <component name="SqlDialectMappings"> | ||||
|     <file url="file://$PROJECT_DIR$/migrations/create.sql" dialect="GenericSQL" /> | ||||
|     <file url="PROJECT" dialect="MySQL" /> | ||||
|   </component> | ||||
| </project> | ||||
							
								
								
									
										1925
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1925
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										37
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -1,22 +1,41 @@ | ||||
| [package] | ||||
| name = "soundfx-rs" | ||||
| version = "1.4.3" | ||||
| description = "Discord bot for custom sound effects and soundboards" | ||||
| license = "AGPL-3.0-only" | ||||
| version = "1.5.9" | ||||
| authors = ["jellywx <judesouthworth@pm.me>"] | ||||
| edition = "2018" | ||||
|  | ||||
| [dependencies] | ||||
| songbird = { git = "https://github.com/serenity-rs/songbird", branch = "next" } | ||||
| serenity = { git = "https://github.com/serenity-rs/serenity", branch = "next", features = ["voice", "collector", "unstable_discord_api"] } | ||||
| sqlx = { version = "0.5", default-features = false, features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal"] } | ||||
| dotenv = "0.15" | ||||
| songbird = { version = "0.3", features = ["builtin-queue"] } | ||||
| poise = "0.3" | ||||
| sqlx = { version = "0.5", default-features = false, features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "migrate"] } | ||||
| tokio = { version = "1", features = ["fs", "process", "io-util"] } | ||||
| lazy_static = "1.4" | ||||
| reqwest = "0.11" | ||||
| env_logger = "0.8" | ||||
| env_logger = "0.10" | ||||
| regex = "1.4" | ||||
| log = "0.4" | ||||
| serde_json = "1.0" | ||||
| dashmap = "4.0" | ||||
| dashmap = "5.3" | ||||
| serde = "1.0" | ||||
| dotenv = "0.15.0" | ||||
|  | ||||
| [dependencies.regex_command_attr] | ||||
| path = "./regex_command_attr" | ||||
| [patch."https://github.com/serenity-rs/serenity"] | ||||
| 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/config.env", "600"] | ||||
| ] | ||||
| conf-files = [ | ||||
|     "/etc/soundfx-rs/config.env", | ||||
| ] | ||||
|  | ||||
| [package.metadata.deb.systemd-units] | ||||
| unit-scripts = "systemd" | ||||
| start = false | ||||
|   | ||||
							
								
								
									
										9
									
								
								Containerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Containerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| FROM ubuntu:20.04 | ||||
|  | ||||
| ENV RUSTUP_HOME=/usr/local/rustup \ | ||||
|     CARGO_HOME=/usr/local/cargo \ | ||||
|     PATH=/usr/local/cargo/bin:$PATH | ||||
|  | ||||
| RUN apt update && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y gcc gcc-multilib cmake ffmpeg libopus-dev pkg-config libssl-dev curl mysql-client-8.0 | ||||
| RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal | ||||
| RUN cargo install cargo-deb | ||||
							
								
								
									
										50
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,24 +1,48 @@ | ||||
| # SoundFX 2 | ||||
| ## The complete (second) Rust rewrite of SoundFX | ||||
| # SoundFX | ||||
|  | ||||
| SoundFX 2 is the Rust rewrite of SoundFX. SoundFX 2 attempts to retain all functionality of the original bot, in a more  | ||||
| efficient and robust package. SoundFX 2 is as asynchronous as it can get, and runs on the Tokio runtime. | ||||
| A bot for managing sound effects in Discord. | ||||
|  | ||||
| ### Building | ||||
| ## Installing | ||||
|  | ||||
| Use the Cargo.toml file to build it. Simple as. Don't need anything like MySQL libs and stuff because SQLx includes its  | ||||
| own pure Rust one. Needs Rust 1.43+ | ||||
| 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. | ||||
|  | ||||
| ### Running & Config | ||||
| ## Running & config | ||||
|  | ||||
| The bot connects to the MySQL server URL defined in a `.env` file in the working directory of the program. | ||||
| 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. | ||||
|  | ||||
| Config options: | ||||
| Config options are provided in a file `/etc/soundfx-rs/default.env` | ||||
|  | ||||
| Options: | ||||
| * `DISCORD_TOKEN`- your token (required) | ||||
| * `DATABASE_URL`- your database URL (required) | ||||
| * `DISCONNECT_CYCLES`- specifies the number of inactivity cycles before the bot should disconnect itself from a voice channel | ||||
| * `DISCONNECT_CYCLE_DELAY`- specifies the delay between cleanup cycles | ||||
| * `MAX_SOUNDS`- specifies how many sounds a user should be allowed without Patreon | ||||
| * `MAX_SOUNDS`- specifies how many sounds a user should be allowed without having the `PATREON_ROLE` specified below | ||||
| * `PATREON_GUILD`- specifies the ID of the guild being used 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/`) | ||||
| * `UPLOAD_MAX_SIZE`- specifies the maximum upload size to permit in bytes. Defaults to 2MB | ||||
|  | ||||
| ## Building from source | ||||
|  | ||||
| When running from source, the config options above can be configured simply as environment variables. | ||||
|  | ||||
| Two options for building are offered. The first is easier. | ||||
|  | ||||
| ### Build for local platform | ||||
|  | ||||
| 1. Install build dependencies: `sudo apt install gcc gcc-multilib cmake ffmpeg libopus-dev pkg-config libssl-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` | ||||
|  | ||||
| ### Build for other platform | ||||
|  | ||||
| By default, this builds targeting Ubuntu 20.04. Modify the Containerfile if you wish to target a different platform. These instructions are written using `podman`, but `docker` should work too. | ||||
|  | ||||
| 1. Install container software: `sudo apt install podman`. | ||||
| 2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `soundfx` | ||||
| 3. Install SQLx CLI: `cargo install sqlx-cli` | ||||
| 4. From the source code directory, execute `sqlx migrate run` | ||||
| 5. Build container image: `podman build -t soundfx .` | ||||
| 6. Build with podman: `podman run --rm --network=host -v "$PWD":/mnt -w /mnt -e "DATABASE_URL=mysql://user@localhost/soundfx" soundfx cargo deb`  | ||||
|   | ||||
										
											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= | ||||
							
								
								
									
										2
									
								
								debian/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								debian/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| * | ||||
| !.gitignore | ||||
							
								
								
									
										9
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| set -e | ||||
|  | ||||
| id -u soundfx &>/dev/null || useradd -r -M soundfx | ||||
|  | ||||
| chown soundfx /etc/soundfx-rs/config.env | ||||
|  | ||||
| #DEBHELPER# | ||||
							
								
								
									
										7
									
								
								debian/postrm
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								debian/postrm
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| set -e | ||||
|  | ||||
| id -u soundfx &>/dev/null || userdel soundfx | ||||
|  | ||||
| #DEBHELPER# | ||||
							
								
								
									
										8
									
								
								dump-query.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										8
									
								
								dump-query.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| mysql -D soundfx -N -e "SELECT name, hex(src) FROM sounds" > out | ||||
| split --additional-suffix=.row -l 1 out | ||||
| for filename in *.row; do | ||||
|     name=`grep -oP '^(.+)(?=\t)' $filename` | ||||
|     col=`awk -F '\t' '{print $2}' "$filename"` | ||||
|     echo $col > "$filename.hex" | ||||
|     xxd -r -p "$filename.hex" "$name.opus" | ||||
| done | ||||
							
								
								
									
										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; | ||||
| @@ -1,15 +0,0 @@ | ||||
| [package] | ||||
| name = "regex_command_attr" | ||||
| version = "0.3.6" | ||||
| authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"] | ||||
| edition = "2018" | ||||
| description = "Procedural macros for command creation for the Serenity library." | ||||
| license = "ISC" | ||||
|  | ||||
| [lib] | ||||
| proc-macro = true | ||||
|  | ||||
| [dependencies] | ||||
| quote = "^1.0" | ||||
| syn = { version = "^1.0", features = ["full", "derive", "extra-traits"] } | ||||
| proc-macro2 = "1.0" | ||||
| @@ -1,408 +0,0 @@ | ||||
| use std::fmt::{self, Write}; | ||||
|  | ||||
| use proc_macro2::Span; | ||||
| use syn::parse::{Error, Result}; | ||||
| use syn::spanned::Spanned; | ||||
| use syn::{Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path}; | ||||
|  | ||||
| use crate::structures::{ApplicationCommandOptionType, Arg, CommandKind, PermissionLevel}; | ||||
| use crate::util::{AsOption, LitExt}; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, PartialEq)] | ||||
| pub enum ValueKind { | ||||
|     // #[<name>] | ||||
|     Name, | ||||
|  | ||||
|     // #[<name> = <value>] | ||||
|     Equals, | ||||
|  | ||||
|     // #[<name>([<value>, <value>, <value>, ...])] | ||||
|     List, | ||||
|  | ||||
|     // #[<name>([<prop> = <value>, <prop> = <value>, ...])] | ||||
|     EqualsList, | ||||
|  | ||||
|     // #[<name>(<value>)] | ||||
|     SingleList, | ||||
| } | ||||
|  | ||||
| impl fmt::Display for ValueKind { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             ValueKind::Name => f.pad("`#[<name>]`"), | ||||
|             ValueKind::Equals => f.pad("`#[<name> = <value>]`"), | ||||
|             ValueKind::List => f.pad("`#[<name>([<value>, <value>, <value>, ...])]`"), | ||||
|             ValueKind::EqualsList => { | ||||
|                 f.pad("`#[<name>([<prop> = <value>, <prop> = <value>, ...])]`") | ||||
|             } | ||||
|             ValueKind::SingleList => f.pad("`#[<name>(<value>)]`"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn to_ident(p: Path) -> Result<Ident> { | ||||
|     if p.segments.is_empty() { | ||||
|         return Err(Error::new( | ||||
|             p.span(), | ||||
|             "cannot convert an empty path to an identifier", | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     if p.segments.len() > 1 { | ||||
|         return Err(Error::new( | ||||
|             p.span(), | ||||
|             "the path must not have more than one segment", | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     if !p.segments[0].arguments.is_empty() { | ||||
|         return Err(Error::new( | ||||
|             p.span(), | ||||
|             "the singular path segment must not have any arguments", | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     Ok(p.segments[0].ident.clone()) | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Values { | ||||
|     pub name: Ident, | ||||
|     pub literals: Vec<(Option<String>, Lit)>, | ||||
|     pub kind: ValueKind, | ||||
|     pub span: Span, | ||||
| } | ||||
|  | ||||
| impl Values { | ||||
|     #[inline] | ||||
|     pub fn new( | ||||
|         name: Ident, | ||||
|         kind: ValueKind, | ||||
|         literals: Vec<(Option<String>, Lit)>, | ||||
|         span: Span, | ||||
|     ) -> Self { | ||||
|         Values { | ||||
|             name, | ||||
|             literals, | ||||
|             kind, | ||||
|             span, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn parse_values(attr: &Attribute) -> Result<Values> { | ||||
|     fn is_list_or_named_list(meta: &NestedMeta) -> ValueKind { | ||||
|         match meta { | ||||
|             // catch if the nested value is a literal value | ||||
|             NestedMeta::Lit(_) => ValueKind::List, | ||||
|             // catch if the nested value is a meta value | ||||
|             NestedMeta::Meta(m) => match m { | ||||
|                 // path => some quoted value | ||||
|                 Meta::Path(_) => ValueKind::List, | ||||
|                 Meta::List(_) | Meta::NameValue(_) => ValueKind::EqualsList, | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let meta = attr.parse_meta()?; | ||||
|  | ||||
|     match meta { | ||||
|         Meta::Path(path) => { | ||||
|             let name = to_ident(path)?; | ||||
|  | ||||
|             Ok(Values::new(name, ValueKind::Name, Vec::new(), attr.span())) | ||||
|         } | ||||
|         Meta::List(meta) => { | ||||
|             let name = to_ident(meta.path)?; | ||||
|             let nested = meta.nested; | ||||
|  | ||||
|             if nested.is_empty() { | ||||
|                 return Err(Error::new(attr.span(), "list cannot be empty")); | ||||
|             } | ||||
|  | ||||
|             if is_list_or_named_list(nested.first().unwrap()) == ValueKind::List { | ||||
|                 let mut lits = Vec::with_capacity(nested.len()); | ||||
|  | ||||
|                 for meta in nested { | ||||
|                     match meta { | ||||
|                         // catch if the nested value is a literal value | ||||
|                         NestedMeta::Lit(l) => lits.push((None, l)), | ||||
|                         // catch if the nested value is a meta value | ||||
|                         NestedMeta::Meta(m) => match m { | ||||
|                             // path => some quoted value | ||||
|                             Meta::Path(path) => { | ||||
|                                 let i = to_ident(path)?; | ||||
|                                 lits.push((None, Lit::Str(LitStr::new(&i.to_string(), i.span())))) | ||||
|                             } | ||||
|                             Meta::List(_) | Meta::NameValue(_) => { | ||||
|                                 return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level")) | ||||
|                             } | ||||
|                         }, | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 let kind = if lits.len() == 1 { | ||||
|                     ValueKind::SingleList | ||||
|                 } else { | ||||
|                     ValueKind::List | ||||
|                 }; | ||||
|  | ||||
|                 Ok(Values::new(name, kind, lits, attr.span())) | ||||
|             } else { | ||||
|                 let mut lits = Vec::with_capacity(nested.len()); | ||||
|  | ||||
|                 for meta in nested { | ||||
|                     match meta { | ||||
|                         // catch if the nested value is a literal value | ||||
|                         NestedMeta::Lit(_) => { | ||||
|                             return Err(Error::new(attr.span(), "key-value pairs expected")) | ||||
|                         } | ||||
|                         // catch if the nested value is a meta value | ||||
|                         NestedMeta::Meta(m) => match m { | ||||
|                             Meta::NameValue(n) => { | ||||
|                                 let name = to_ident(n.path)?.to_string(); | ||||
|                                 let value = n.lit; | ||||
|  | ||||
|                                 lits.push((Some(name), value)); | ||||
|                             } | ||||
|                             Meta::List(_) | Meta::Path(_) => { | ||||
|                                 return Err(Error::new(attr.span(), "key-value pairs expected")) | ||||
|                             } | ||||
|                         }, | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 Ok(Values::new(name, ValueKind::EqualsList, lits, attr.span())) | ||||
|             } | ||||
|         } | ||||
|         Meta::NameValue(meta) => { | ||||
|             let name = to_ident(meta.path)?; | ||||
|             let lit = meta.lit; | ||||
|  | ||||
|             Ok(Values::new( | ||||
|                 name, | ||||
|                 ValueKind::Equals, | ||||
|                 vec![(None, lit)], | ||||
|                 attr.span(), | ||||
|             )) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| struct DisplaySlice<'a, T>(&'a [T]); | ||||
|  | ||||
| impl<'a, T: fmt::Display> fmt::Display for DisplaySlice<'a, T> { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         let mut iter = self.0.iter().enumerate(); | ||||
|  | ||||
|         match iter.next() { | ||||
|             None => f.write_str("nothing")?, | ||||
|             Some((idx, elem)) => { | ||||
|                 write!(f, "{}: {}", idx, elem)?; | ||||
|  | ||||
|                 for (idx, elem) in iter { | ||||
|                     f.write_char('\n')?; | ||||
|                     write!(f, "{}: {}", idx, elem)?; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| fn is_form_acceptable(expect: &[ValueKind], kind: ValueKind) -> bool { | ||||
|     if expect.contains(&ValueKind::List) && kind == ValueKind::SingleList { | ||||
|         true | ||||
|     } else { | ||||
|         expect.contains(&kind) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| fn validate(values: &Values, forms: &[ValueKind]) -> Result<()> { | ||||
|     if !is_form_acceptable(forms, values.kind) { | ||||
|         return Err(Error::new( | ||||
|             values.span, | ||||
|             // Using the `_args` version here to avoid an allocation. | ||||
|             format_args!( | ||||
|                 "the attribute must be in of these forms:\n{}", | ||||
|                 DisplaySlice(forms) | ||||
|             ), | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn parse<T: AttributeOption>(values: Values) -> Result<T> { | ||||
|     T::parse(values) | ||||
| } | ||||
|  | ||||
| pub trait AttributeOption: Sized { | ||||
|     fn parse(values: Values) -> Result<Self>; | ||||
| } | ||||
|  | ||||
| impl AttributeOption for Vec<String> { | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::List])?; | ||||
|  | ||||
|         Ok(values | ||||
|             .literals | ||||
|             .into_iter() | ||||
|             .map(|(_, l)| l.to_str()) | ||||
|             .collect()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for String { | ||||
|     #[inline] | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?; | ||||
|  | ||||
|         Ok(values.literals[0].1.to_str()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for bool { | ||||
|     #[inline] | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::Name, ValueKind::SingleList])?; | ||||
|  | ||||
|         Ok(values.literals.get(0).map_or(true, |(_, l)| l.to_bool())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for Ident { | ||||
|     #[inline] | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::SingleList])?; | ||||
|  | ||||
|         Ok(values.literals[0].1.to_ident()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for Vec<Ident> { | ||||
|     #[inline] | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::List])?; | ||||
|  | ||||
|         Ok(values | ||||
|             .literals | ||||
|             .into_iter() | ||||
|             .map(|(_, l)| l.to_ident()) | ||||
|             .collect()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for Option<String> { | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate( | ||||
|             &values, | ||||
|             &[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList], | ||||
|         )?; | ||||
|  | ||||
|         Ok(values.literals.get(0).map(|(_, l)| l.to_str())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for PermissionLevel { | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::SingleList])?; | ||||
|  | ||||
|         Ok(values | ||||
|             .literals | ||||
|             .get(0) | ||||
|             .map(|(_, l)| PermissionLevel::from_str(&*l.to_str()).unwrap()) | ||||
|             .unwrap()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for CommandKind { | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::SingleList])?; | ||||
|  | ||||
|         Ok(values | ||||
|             .literals | ||||
|             .get(0) | ||||
|             .map(|(_, l)| CommandKind::from_str(&*l.to_str()).unwrap()) | ||||
|             .unwrap()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for Arg { | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::EqualsList])?; | ||||
|  | ||||
|         let mut arg: Arg = Default::default(); | ||||
|  | ||||
|         for (key, value) in &values.literals { | ||||
|             match key { | ||||
|                 Some(s) => match s.as_str() { | ||||
|                     "name" => { | ||||
|                         arg.name = value.to_str(); | ||||
|                     } | ||||
|                     "description" => { | ||||
|                         arg.description = value.to_str(); | ||||
|                     } | ||||
|                     "required" => { | ||||
|                         arg.required = value.to_bool(); | ||||
|                     } | ||||
|                     "kind" => arg.kind = ApplicationCommandOptionType::from_str(value.to_str()), | ||||
|                     _ => { | ||||
|                         return Err(Error::new(key.span(), "unexpected attribute")); | ||||
|                     } | ||||
|                 }, | ||||
|                 _ => { | ||||
|                     return Err(Error::new(key.span(), "unnamed attribute")); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(arg) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<T: AttributeOption> AttributeOption for AsOption<T> { | ||||
|     #[inline] | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         Ok(AsOption(Some(T::parse(values)?))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| macro_rules! attr_option_num { | ||||
|     ($($n:ty),*) => { | ||||
|         $( | ||||
|             impl AttributeOption for $n { | ||||
|                 fn parse(values: Values) -> Result<Self> { | ||||
|                     validate(&values, &[ValueKind::SingleList])?; | ||||
|  | ||||
|                     Ok(match &values.literals[0].1 { | ||||
|                         Lit::Int(l) => l.base10_parse::<$n>()?, | ||||
|                         l => { | ||||
|                             let s = l.to_str(); | ||||
|                             // Use `as_str` to guide the compiler to use `&str`'s parse method. | ||||
|                             // We don't want to use our `parse` method here (`impl AttributeOption for String`). | ||||
|                             match s.as_str().parse::<$n>() { | ||||
|                                 Ok(n) => n, | ||||
|                                 Err(_) => return Err(Error::new(l.span(), "invalid integer")), | ||||
|                             } | ||||
|                         } | ||||
|                     }) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             impl AttributeOption for Option<$n> { | ||||
|                 #[inline] | ||||
|                 fn parse(values: Values) -> Result<Self> { | ||||
|                     <$n as AttributeOption>::parse(values).map(Some) | ||||
|                 } | ||||
|             } | ||||
|         )* | ||||
|     } | ||||
| } | ||||
|  | ||||
| attr_option_num!(u16, u32, usize); | ||||
| @@ -1,6 +0,0 @@ | ||||
| pub mod suffixes { | ||||
|     pub const COMMAND: &str = "COMMAND"; | ||||
|     pub const ARG: &str = "ARG"; | ||||
| } | ||||
|  | ||||
| pub use self::suffixes::*; | ||||
| @@ -1,173 +0,0 @@ | ||||
| #![deny(rust_2018_idioms)] | ||||
| #![deny(broken_intra_doc_links)] | ||||
|  | ||||
| use proc_macro::TokenStream; | ||||
| use proc_macro2::Ident; | ||||
| use quote::quote; | ||||
| use syn::{parse::Error, parse_macro_input, parse_quote, spanned::Spanned, Lit}; | ||||
|  | ||||
| pub(crate) mod attributes; | ||||
| pub(crate) mod consts; | ||||
| pub(crate) mod structures; | ||||
|  | ||||
| #[macro_use] | ||||
| pub(crate) mod util; | ||||
|  | ||||
| use attributes::*; | ||||
| use consts::*; | ||||
| use structures::*; | ||||
| use util::*; | ||||
|  | ||||
| macro_rules! match_options { | ||||
|     ($v:expr, $values:ident, $options:ident, $span:expr => [$($name:ident);*]) => { | ||||
|         match $v { | ||||
|             $( | ||||
|                 stringify!($name) => $options.$name = propagate_err!($crate::attributes::parse($values)), | ||||
|             )* | ||||
|             _ => { | ||||
|                 return Error::new($span, format_args!("invalid attribute: {:?}", $v)) | ||||
|                     .to_compile_error() | ||||
|                     .into(); | ||||
|             }, | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| #[proc_macro_attribute] | ||||
| pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { | ||||
|     let mut fun = parse_macro_input!(input as CommandFun); | ||||
|  | ||||
|     let _name = if !attr.is_empty() { | ||||
|         parse_macro_input!(attr as Lit).to_str() | ||||
|     } else { | ||||
|         fun.name.to_string() | ||||
|     }; | ||||
|  | ||||
|     let mut options = Options::new(); | ||||
|  | ||||
|     for attribute in &fun.attributes { | ||||
|         let span = attribute.span(); | ||||
|         let values = propagate_err!(parse_values(attribute)); | ||||
|  | ||||
|         let name = values.name.to_string(); | ||||
|         let name = &name[..]; | ||||
|  | ||||
|         match name { | ||||
|             "arg" => options | ||||
|                 .cmd_args | ||||
|                 .push(propagate_err!(attributes::parse(values))), | ||||
|             "example" => { | ||||
|                 options | ||||
|                     .examples | ||||
|                     .push(propagate_err!(attributes::parse(values))); | ||||
|             } | ||||
|             "description" => { | ||||
|                 let line: String = propagate_err!(attributes::parse(values)); | ||||
|                 util::append_line(&mut options.description, line); | ||||
|             } | ||||
|             _ => { | ||||
|                 match_options!(name, values, options, span => [ | ||||
|                     aliases; | ||||
|                     group; | ||||
|                     required_permissions; | ||||
|                     kind | ||||
|                 ]); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let Options { | ||||
|         aliases, | ||||
|         description, | ||||
|         group, | ||||
|         examples, | ||||
|         required_permissions, | ||||
|         kind, | ||||
|         mut cmd_args, | ||||
|     } = options; | ||||
|  | ||||
|     propagate_err!(create_declaration_validations(&mut fun)); | ||||
|  | ||||
|     let res = parse_quote!(serenity::framework::standard::CommandResult); | ||||
|     create_return_type_validation(&mut fun, res); | ||||
|  | ||||
|     let visibility = fun.visibility; | ||||
|     let name = fun.name.clone(); | ||||
|     let body = fun.body; | ||||
|     let ret = fun.ret; | ||||
|  | ||||
|     let n = name.with_suffix(COMMAND); | ||||
|  | ||||
|     let cooked = fun.cooked.clone(); | ||||
|  | ||||
|     let command_path = quote!(crate::framework::Command); | ||||
|     let arg_path = quote!(crate::framework::Arg); | ||||
|  | ||||
|     populate_fut_lifetimes_on_refs(&mut fun.args); | ||||
|     let args = fun.args; | ||||
|  | ||||
|     let arg_idents = cmd_args | ||||
|         .iter() | ||||
|         .map(|arg| { | ||||
|             n.with_suffix(arg.name.replace(" ", "_").replace("-", "_").as_str()) | ||||
|                 .with_suffix(ARG) | ||||
|         }) | ||||
|         .collect::<Vec<Ident>>(); | ||||
|  | ||||
|     let mut tokens = cmd_args | ||||
|         .iter_mut() | ||||
|         .map(|arg| { | ||||
|             let Arg { | ||||
|                 name, | ||||
|                 description, | ||||
|                 kind, | ||||
|                 required, | ||||
|             } = arg; | ||||
|  | ||||
|             let an = n.with_suffix(name.as_str()).with_suffix(ARG); | ||||
|  | ||||
|             quote! { | ||||
|                 #(#cooked)* | ||||
|                 #[allow(missing_docs)] | ||||
|                 pub static #an: #arg_path = #arg_path { | ||||
|                     name: #name, | ||||
|                     description: #description, | ||||
|                     kind: #kind, | ||||
|                     required: #required, | ||||
|                 }; | ||||
|             } | ||||
|         }) | ||||
|         .fold(quote! {}, |mut a, b| { | ||||
|             a.extend(b); | ||||
|             a | ||||
|         }); | ||||
|  | ||||
|     tokens.extend(quote! { | ||||
|         #(#cooked)* | ||||
|         #[allow(missing_docs)] | ||||
|         pub static #n: #command_path = #command_path { | ||||
|             fun: #name, | ||||
|             names: &[#_name, #(#aliases),*], | ||||
|             desc: #description, | ||||
|             group: #group, | ||||
|             examples: &[#(#examples),*], | ||||
|             required_permissions: #required_permissions, | ||||
|             kind: #kind, | ||||
|             args: &[#(&#arg_idents),*], | ||||
|         }; | ||||
|  | ||||
|         #(#cooked)* | ||||
|         #[allow(missing_docs)] | ||||
|         #visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, #ret> { | ||||
|             use ::serenity::futures::future::FutureExt; | ||||
|  | ||||
|             async move { | ||||
|                 let _output: #ret = { #(#body)* }; | ||||
|                 #[allow(unreachable_code)] | ||||
|                 _output | ||||
|             }.boxed() | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     tokens.into() | ||||
| } | ||||
| @@ -1,359 +0,0 @@ | ||||
| use proc_macro2::TokenStream as TokenStream2; | ||||
| use quote::{quote, ToTokens}; | ||||
| use syn::{ | ||||
|     braced, | ||||
|     parse::{Error, Parse, ParseStream, Result}, | ||||
|     spanned::Spanned, | ||||
|     Attribute, Block, FnArg, Ident, Pat, ReturnType, Stmt, Token, Type, Visibility, | ||||
| }; | ||||
|  | ||||
| use crate::util::{self, Argument, Parenthesised}; | ||||
|  | ||||
| fn parse_argument(arg: FnArg) -> Result<Argument> { | ||||
|     match arg { | ||||
|         FnArg::Typed(typed) => { | ||||
|             let pat = typed.pat; | ||||
|             let kind = typed.ty; | ||||
|  | ||||
|             match *pat { | ||||
|                 Pat::Ident(id) => { | ||||
|                     let name = id.ident; | ||||
|                     let mutable = id.mutability; | ||||
|  | ||||
|                     Ok(Argument { | ||||
|                         mutable, | ||||
|                         name, | ||||
|                         kind: *kind, | ||||
|                     }) | ||||
|                 } | ||||
|                 Pat::Wild(wild) => { | ||||
|                     let token = wild.underscore_token; | ||||
|  | ||||
|                     let name = Ident::new("_", token.spans[0]); | ||||
|  | ||||
|                     Ok(Argument { | ||||
|                         mutable: None, | ||||
|                         name, | ||||
|                         kind: *kind, | ||||
|                     }) | ||||
|                 } | ||||
|                 _ => Err(Error::new( | ||||
|                     pat.span(), | ||||
|                     format_args!("unsupported pattern: {:?}", pat), | ||||
|                 )), | ||||
|             } | ||||
|         } | ||||
|         FnArg::Receiver(_) => Err(Error::new( | ||||
|             arg.span(), | ||||
|             format_args!("`self` arguments are prohibited: {:?}", arg), | ||||
|         )), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Test if the attribute is cooked. | ||||
| fn is_cooked(attr: &Attribute) -> bool { | ||||
|     const COOKED_ATTRIBUTE_NAMES: &[&str] = &[ | ||||
|         "cfg", "cfg_attr", "derive", "inline", "allow", "warn", "deny", "forbid", | ||||
|     ]; | ||||
|  | ||||
|     COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n)) | ||||
| } | ||||
|  | ||||
| /// Removes cooked attributes from a vector of attributes. Uncooked attributes are left in the vector. | ||||
| /// | ||||
| /// # Return | ||||
| /// | ||||
| /// Returns a vector of cooked attributes that have been removed from the input vector. | ||||
| fn remove_cooked(attrs: &mut Vec<Attribute>) -> Vec<Attribute> { | ||||
|     let mut cooked = Vec::new(); | ||||
|  | ||||
|     // FIXME: Replace with `Vec::drain_filter` once it is stable. | ||||
|     let mut i = 0; | ||||
|     while i < attrs.len() { | ||||
|         if !is_cooked(&attrs[i]) { | ||||
|             i += 1; | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         cooked.push(attrs.remove(i)); | ||||
|     } | ||||
|  | ||||
|     cooked | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct CommandFun { | ||||
|     /// `#[...]`-style attributes. | ||||
|     pub attributes: Vec<Attribute>, | ||||
|     /// Populated cooked attributes. These are attributes outside of the realm of this crate's procedural macros | ||||
|     /// and will appear in generated output. | ||||
|     pub cooked: Vec<Attribute>, | ||||
|     pub visibility: Visibility, | ||||
|     pub name: Ident, | ||||
|     pub args: Vec<Argument>, | ||||
|     pub ret: Type, | ||||
|     pub body: Vec<Stmt>, | ||||
| } | ||||
|  | ||||
| impl Parse for CommandFun { | ||||
|     fn parse(input: ParseStream<'_>) -> Result<Self> { | ||||
|         let mut attributes = input.call(Attribute::parse_outer)?; | ||||
|  | ||||
|         // Rename documentation comment attributes (`#[doc = "..."]`) to `#[description = "..."]`. | ||||
|         util::rename_attributes(&mut attributes, "doc", "description"); | ||||
|  | ||||
|         let cooked = remove_cooked(&mut attributes); | ||||
|  | ||||
|         let visibility = input.parse::<Visibility>()?; | ||||
|  | ||||
|         input.parse::<Token![async]>()?; | ||||
|  | ||||
|         input.parse::<Token![fn]>()?; | ||||
|         let name = input.parse()?; | ||||
|  | ||||
|         // (...) | ||||
|         let Parenthesised(args) = input.parse::<Parenthesised<FnArg>>()?; | ||||
|  | ||||
|         let ret = match input.parse::<ReturnType>()? { | ||||
|             ReturnType::Type(_, t) => (*t).clone(), | ||||
|             ReturnType::Default => { | ||||
|                 return Err(input | ||||
|                     .error("expected a result type of either `CommandResult` or `CheckResult`")) | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         // { ... } | ||||
|         let bcont; | ||||
|         braced!(bcont in input); | ||||
|         let body = bcont.call(Block::parse_within)?; | ||||
|  | ||||
|         let args = args | ||||
|             .into_iter() | ||||
|             .map(parse_argument) | ||||
|             .collect::<Result<Vec<_>>>()?; | ||||
|  | ||||
|         Ok(Self { | ||||
|             attributes, | ||||
|             cooked, | ||||
|             visibility, | ||||
|             name, | ||||
|             args, | ||||
|             ret, | ||||
|             body, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ToTokens for CommandFun { | ||||
|     fn to_tokens(&self, stream: &mut TokenStream2) { | ||||
|         let Self { | ||||
|             attributes: _, | ||||
|             cooked, | ||||
|             visibility, | ||||
|             name, | ||||
|             args, | ||||
|             ret, | ||||
|             body, | ||||
|         } = self; | ||||
|  | ||||
|         stream.extend(quote! { | ||||
|             #(#cooked)* | ||||
|             #visibility async fn #name (#(#args),*) -> #ret { | ||||
|                 #(#body)* | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub enum PermissionLevel { | ||||
|     Unrestricted, | ||||
|     Managed, | ||||
|     Restricted, | ||||
| } | ||||
|  | ||||
| impl Default for PermissionLevel { | ||||
|     fn default() -> Self { | ||||
|         Self::Unrestricted | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl PermissionLevel { | ||||
|     pub fn from_str(s: &str) -> Option<Self> { | ||||
|         Some(match s.to_uppercase().as_str() { | ||||
|             "UNRESTRICTED" => Self::Unrestricted, | ||||
|             "MANAGED" => Self::Managed, | ||||
|             "RESTRICTED" => Self::Restricted, | ||||
|             _ => return None, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ToTokens for PermissionLevel { | ||||
|     fn to_tokens(&self, stream: &mut TokenStream2) { | ||||
|         let path = quote!(crate::framework::PermissionLevel); | ||||
|         let variant; | ||||
|  | ||||
|         match self { | ||||
|             Self::Unrestricted => { | ||||
|                 variant = quote!(Unrestricted); | ||||
|             } | ||||
|  | ||||
|             Self::Managed => { | ||||
|                 variant = quote!(Managed); | ||||
|             } | ||||
|  | ||||
|             Self::Restricted => { | ||||
|                 variant = quote!(Restricted); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         stream.extend(quote! { | ||||
|             #path::#variant | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub enum CommandKind { | ||||
|     Slash, | ||||
|     Both, | ||||
|     Text, | ||||
| } | ||||
|  | ||||
| impl Default for CommandKind { | ||||
|     fn default() -> Self { | ||||
|         Self::Both | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl CommandKind { | ||||
|     pub fn from_str(s: &str) -> Option<Self> { | ||||
|         Some(match s.to_uppercase().as_str() { | ||||
|             "SLASH" => Self::Slash, | ||||
|             "BOTH" => Self::Both, | ||||
|             "TEXT" => Self::Text, | ||||
|             _ => return None, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ToTokens for CommandKind { | ||||
|     fn to_tokens(&self, stream: &mut TokenStream2) { | ||||
|         let path = quote!(crate::framework::CommandKind); | ||||
|         let variant; | ||||
|  | ||||
|         match self { | ||||
|             Self::Slash => { | ||||
|                 variant = quote!(Slash); | ||||
|             } | ||||
|  | ||||
|             Self::Both => { | ||||
|                 variant = quote!(Both); | ||||
|             } | ||||
|  | ||||
|             Self::Text => { | ||||
|                 variant = quote!(Text); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         stream.extend(quote! { | ||||
|             #path::#variant | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub(crate) enum ApplicationCommandOptionType { | ||||
|     SubCommand, | ||||
|     SubCommandGroup, | ||||
|     String, | ||||
|     Integer, | ||||
|     Boolean, | ||||
|     User, | ||||
|     Channel, | ||||
|     Role, | ||||
|     Mentionable, | ||||
|     Unknown, | ||||
| } | ||||
|  | ||||
| impl ApplicationCommandOptionType { | ||||
|     pub fn from_str(s: String) -> Self { | ||||
|         match s.as_str() { | ||||
|             "SubCommand" => Self::SubCommand, | ||||
|             "SubCommandGroup" => Self::SubCommandGroup, | ||||
|             "String" => Self::String, | ||||
|             "Integer" => Self::Integer, | ||||
|             "Boolean" => Self::Boolean, | ||||
|             "User" => Self::User, | ||||
|             "Channel" => Self::Channel, | ||||
|             "Role" => Self::Role, | ||||
|             "Mentionable" => Self::Mentionable, | ||||
|             _ => Self::Unknown, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ToTokens for ApplicationCommandOptionType { | ||||
|     fn to_tokens(&self, stream: &mut TokenStream2) { | ||||
|         let path = quote!( | ||||
|             serenity::model::interactions::application_command::ApplicationCommandOptionType | ||||
|         ); | ||||
|         let variant = match self { | ||||
|             ApplicationCommandOptionType::SubCommand => quote!(SubCommand), | ||||
|             ApplicationCommandOptionType::SubCommandGroup => quote!(SubCommandGroup), | ||||
|             ApplicationCommandOptionType::String => quote!(String), | ||||
|             ApplicationCommandOptionType::Integer => quote!(Integer), | ||||
|             ApplicationCommandOptionType::Boolean => quote!(Boolean), | ||||
|             ApplicationCommandOptionType::User => quote!(User), | ||||
|             ApplicationCommandOptionType::Channel => quote!(Channel), | ||||
|             ApplicationCommandOptionType::Role => quote!(Role), | ||||
|             ApplicationCommandOptionType::Mentionable => quote!(Mentionable), | ||||
|             ApplicationCommandOptionType::Unknown => quote!(Unknown), | ||||
|         }; | ||||
|  | ||||
|         stream.extend(quote! { | ||||
|             #path::#variant | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct Arg { | ||||
|     pub name: String, | ||||
|     pub description: String, | ||||
|     pub kind: ApplicationCommandOptionType, | ||||
|     pub required: bool, | ||||
| } | ||||
|  | ||||
| impl Default for Arg { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             name: String::new(), | ||||
|             description: String::new(), | ||||
|             kind: ApplicationCommandOptionType::String, | ||||
|             required: false, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| pub(crate) struct Options { | ||||
|     pub aliases: Vec<String>, | ||||
|     pub description: String, | ||||
|     pub group: String, | ||||
|     pub examples: Vec<String>, | ||||
|     pub required_permissions: PermissionLevel, | ||||
|     pub kind: CommandKind, | ||||
|     pub cmd_args: Vec<Arg>, | ||||
| } | ||||
|  | ||||
| impl Options { | ||||
|     #[inline] | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             group: "Other".to_string(), | ||||
|             ..Default::default() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,239 +0,0 @@ | ||||
| use proc_macro::TokenStream; | ||||
| use proc_macro2::Span; | ||||
| use proc_macro2::TokenStream as TokenStream2; | ||||
| use quote::{format_ident, quote, ToTokens}; | ||||
| use syn::{ | ||||
|     braced, bracketed, parenthesized, | ||||
|     parse::{Error, Parse, ParseStream, Result as SynResult}, | ||||
|     parse_quote, | ||||
|     punctuated::Punctuated, | ||||
|     spanned::Spanned, | ||||
|     token::{Comma, Mut}, | ||||
|     Attribute, Ident, Lifetime, Lit, Path, PathSegment, Type, | ||||
| }; | ||||
|  | ||||
| use crate::structures::CommandFun; | ||||
|  | ||||
| pub trait LitExt { | ||||
|     fn to_str(&self) -> String; | ||||
|     fn to_bool(&self) -> bool; | ||||
|     fn to_ident(&self) -> Ident; | ||||
| } | ||||
|  | ||||
| impl LitExt for Lit { | ||||
|     fn to_str(&self) -> String { | ||||
|         match self { | ||||
|             Lit::Str(s) => s.value(), | ||||
|             Lit::ByteStr(s) => unsafe { String::from_utf8_unchecked(s.value()) }, | ||||
|             Lit::Char(c) => c.value().to_string(), | ||||
|             Lit::Byte(b) => (b.value() as char).to_string(), | ||||
|             _ => panic!("values must be a (byte)string or a char"), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn to_bool(&self) -> bool { | ||||
|         if let Lit::Bool(b) = self { | ||||
|             b.value | ||||
|         } else { | ||||
|             self.to_str() | ||||
|                 .parse() | ||||
|                 .unwrap_or_else(|_| panic!("expected bool from {:?}", self)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn to_ident(&self) -> Ident { | ||||
|         Ident::new(&self.to_str(), self.span()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub trait IdentExt2: Sized { | ||||
|     fn to_uppercase(&self) -> Self; | ||||
|     fn with_suffix(&self, suf: &str) -> Ident; | ||||
| } | ||||
|  | ||||
| impl IdentExt2 for Ident { | ||||
|     #[inline] | ||||
|     fn to_uppercase(&self) -> Self { | ||||
|         format_ident!("{}", self.to_string().to_uppercase()) | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn with_suffix(&self, suffix: &str) -> Ident { | ||||
|         format_ident!("{}_{}", self.to_string().to_uppercase(), suffix) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn into_stream(e: Error) -> TokenStream { | ||||
|     e.to_compile_error().into() | ||||
| } | ||||
|  | ||||
| macro_rules! propagate_err { | ||||
|     ($res:expr) => {{ | ||||
|         match $res { | ||||
|             Ok(v) => v, | ||||
|             Err(e) => return $crate::util::into_stream(e), | ||||
|         } | ||||
|     }}; | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Bracketed<T>(pub Punctuated<T, Comma>); | ||||
|  | ||||
| impl<T: Parse> Parse for Bracketed<T> { | ||||
|     fn parse(input: ParseStream<'_>) -> SynResult<Self> { | ||||
|         let content; | ||||
|         bracketed!(content in input); | ||||
|  | ||||
|         Ok(Bracketed(content.parse_terminated(T::parse)?)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Braced<T>(pub Punctuated<T, Comma>); | ||||
|  | ||||
| impl<T: Parse> Parse for Braced<T> { | ||||
|     fn parse(input: ParseStream<'_>) -> SynResult<Self> { | ||||
|         let content; | ||||
|         braced!(content in input); | ||||
|  | ||||
|         Ok(Braced(content.parse_terminated(T::parse)?)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Parenthesised<T>(pub Punctuated<T, Comma>); | ||||
|  | ||||
| impl<T: Parse> Parse for Parenthesised<T> { | ||||
|     fn parse(input: ParseStream<'_>) -> SynResult<Self> { | ||||
|         let content; | ||||
|         parenthesized!(content in input); | ||||
|  | ||||
|         Ok(Parenthesised(content.parse_terminated(T::parse)?)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct AsOption<T>(pub Option<T>); | ||||
|  | ||||
| impl<T: ToTokens> ToTokens for AsOption<T> { | ||||
|     fn to_tokens(&self, stream: &mut TokenStream2) { | ||||
|         match &self.0 { | ||||
|             Some(o) => stream.extend(quote!(Some(#o))), | ||||
|             None => stream.extend(quote!(None)), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<T> Default for AsOption<T> { | ||||
|     #[inline] | ||||
|     fn default() -> Self { | ||||
|         AsOption(None) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Argument { | ||||
|     pub mutable: Option<Mut>, | ||||
|     pub name: Ident, | ||||
|     pub kind: Type, | ||||
| } | ||||
|  | ||||
| impl ToTokens for Argument { | ||||
|     fn to_tokens(&self, stream: &mut TokenStream2) { | ||||
|         let Argument { | ||||
|             mutable, | ||||
|             name, | ||||
|             kind, | ||||
|         } = self; | ||||
|  | ||||
|         stream.extend(quote! { | ||||
|             #mutable #name: #kind | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn generate_type_validation(have: Type, expect: Type) -> syn::Stmt { | ||||
|     parse_quote! { | ||||
|         serenity::static_assertions::assert_type_eq_all!(#have, #expect); | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn create_declaration_validations(fun: &mut CommandFun) -> SynResult<()> { | ||||
|     if fun.args.len() > 3 { | ||||
|         return Err(Error::new( | ||||
|             fun.args.last().unwrap().span(), | ||||
|             format_args!("function's arity exceeds more than 3 arguments"), | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     let context: Type = parse_quote!(&serenity::client::Context); | ||||
|     let message: Type = parse_quote!(&(dyn crate::framework::CommandInvoke + Sync + Send)); | ||||
|     let args: Type = parse_quote!(crate::framework::Args); | ||||
|  | ||||
|     let mut index = 0; | ||||
|  | ||||
|     let mut spoof_or_check = |kind: Type, name: &str| { | ||||
|         match fun.args.get(index) { | ||||
|             Some(x) => fun | ||||
|                 .body | ||||
|                 .insert(0, generate_type_validation(x.kind.clone(), kind)), | ||||
|             None => fun.args.push(Argument { | ||||
|                 mutable: None, | ||||
|                 name: Ident::new(name, Span::call_site()), | ||||
|                 kind, | ||||
|             }), | ||||
|         } | ||||
|  | ||||
|         index += 1; | ||||
|     }; | ||||
|  | ||||
|     spoof_or_check(context, "_ctx"); | ||||
|     spoof_or_check(message, "_msg"); | ||||
|     spoof_or_check(args, "_args"); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn create_return_type_validation(r#fn: &mut CommandFun, expect: Type) { | ||||
|     let stmt = generate_type_validation(r#fn.ret.clone(), expect); | ||||
|     r#fn.body.insert(0, stmt); | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn populate_fut_lifetimes_on_refs(args: &mut Vec<Argument>) { | ||||
|     for arg in args { | ||||
|         if let Type::Reference(reference) = &mut arg.kind { | ||||
|             reference.lifetime = Some(Lifetime::new("'fut", Span::call_site())); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Renames all attributes that have a specific `name` to the `target`. | ||||
| pub fn rename_attributes(attributes: &mut Vec<Attribute>, name: &str, target: &str) { | ||||
|     for attr in attributes { | ||||
|         if attr.path.is_ident(name) { | ||||
|             attr.path = Path::from(PathSegment::from(Ident::new(target, Span::call_site()))); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn append_line(desc: &mut String, mut line: String) { | ||||
|     if line.starts_with(' ') { | ||||
|         line.remove(0); | ||||
|     } | ||||
|  | ||||
|     match line.rfind("\\$") { | ||||
|         Some(i) => { | ||||
|             desc.push_str(line[..i].trim_end()); | ||||
|             desc.push(' '); | ||||
|         } | ||||
|         None => { | ||||
|             desc.push_str(&line); | ||||
|             desc.push('\n'); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										254
									
								
								src/cmds/info.rs
									
									
									
									
									
								
							
							
						
						
									
										254
									
								
								src/cmds/info.rs
									
									
									
									
									
								
							| @@ -1,225 +1,79 @@ | ||||
| use std::{collections::HashMap, sync::Arc}; | ||||
| use crate::{consts::THEME_COLOR, Context, Error}; | ||||
|  | ||||
| use regex_command_attr::command; | ||||
| use serenity::{client::Context, framework::standard::CommandResult}; | ||||
|  | ||||
| use crate::{ | ||||
|     framework::{Args, CommandInvoke, CommandKind, CreateGenericResponse, RegexFramework}, | ||||
|     THEME_COLOR, | ||||
| }; | ||||
|  | ||||
| #[command] | ||||
| #[group("Information")] | ||||
| #[description("Get information on the commands of the bot")] | ||||
| #[arg( | ||||
|     name = "command", | ||||
|     description = "Get help for a specific command", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[example("`/help` - see all commands")] | ||||
| #[example("`/help play` - get help about the `play` command")] | ||||
| pub async fn help( | ||||
|     ctx: &Context, | ||||
|     invoke: &(dyn CommandInvoke + Sync + Send), | ||||
|     args: Args, | ||||
| ) -> CommandResult { | ||||
|     fn get_groups(framework: Arc<RegexFramework>) -> HashMap<&'static str, Vec<&'static str>> { | ||||
|         let mut groups = HashMap::new(); | ||||
|  | ||||
|         for command in &framework.commands_ { | ||||
|             let entry = groups.entry(command.group).or_insert(vec![]); | ||||
|  | ||||
|             entry.push(command.names[0]); | ||||
|         } | ||||
|  | ||||
|         groups | ||||
|     } | ||||
|  | ||||
|     let framework = ctx | ||||
|         .data | ||||
|         .read() | ||||
|         .await | ||||
|         .get::<RegexFramework>() | ||||
|         .cloned() | ||||
|         .unwrap(); | ||||
|  | ||||
|     if let Some(command_name) = args.named("command") { | ||||
|         if let Some(command) = framework.commands.get(command_name) { | ||||
|             let examples = if command.examples.is_empty() { | ||||
|                 "".to_string() | ||||
|             } else { | ||||
|                 format!( | ||||
|                     "**Examples** | ||||
| {}", | ||||
|                     command | ||||
|                         .examples | ||||
|                         .iter() | ||||
|                         .map(|e| format!(" • {}", e)) | ||||
|                         .collect::<Vec<String>>() | ||||
|                         .join("\n") | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             let args = if command.args.is_empty() { | ||||
|                 "**Arguments** | ||||
|  • *This command has no arguments*" | ||||
|                     .to_string() | ||||
|             } else { | ||||
|                 format!( | ||||
|                     "**Arguments** | ||||
| {}", | ||||
|                     command | ||||
|                         .args | ||||
|                         .iter() | ||||
|                         .map(|a| format!( | ||||
|                             " • `{}` {} - {}", | ||||
|                             a.name, | ||||
|                             if a.required { "" } else { "[optional]" }, | ||||
|                             a.description | ||||
|                         )) | ||||
|                         .collect::<Vec<String>>() | ||||
|                         .join("\n") | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             invoke | ||||
|                 .respond( | ||||
|                     ctx.http.clone(), | ||||
|                     CreateGenericResponse::new().embed(|e| { | ||||
|                         e.title(format!("{} Help", command_name)) | ||||
|                             .color(THEME_COLOR) | ||||
|                             .description(format!( | ||||
|                                 "**Available In** | ||||
| `Slash Commands` {} | ||||
| ` Text Commands` {} | ||||
|  | ||||
| **Aliases** | ||||
| {} | ||||
|  | ||||
| **Overview** | ||||
|  • {} | ||||
| {} | ||||
|  | ||||
| {}", | ||||
|                                 if command.kind != CommandKind::Text { | ||||
|                                     "✅" | ||||
|                                 } else { | ||||
|                                     "❎" | ||||
|                                 }, | ||||
|                                 if command.kind != CommandKind::Slash { | ||||
|                                     "✅" | ||||
|                                 } else { | ||||
|                                     "❎" | ||||
|                                 }, | ||||
|                                 command | ||||
|                                     .names | ||||
|                                     .iter() | ||||
|                                     .map(|n| format!("`{}`", n)) | ||||
|                                     .collect::<Vec<String>>() | ||||
|                                     .join(" "), | ||||
|                                 command.desc, | ||||
|                                 args, | ||||
|                                 examples | ||||
|                             )) | ||||
|                     }), | ||||
|                 ) | ||||
|                 .await?; | ||||
|         } else { | ||||
|             let groups = get_groups(framework); | ||||
|             let groups_iter = groups.iter().map(|(name, commands)| { | ||||
|                 ( | ||||
|                     name, | ||||
|                     commands | ||||
|                         .iter() | ||||
|                         .map(|c| format!("`{}`", c)) | ||||
|                         .collect::<Vec<String>>() | ||||
|                         .join(" "), | ||||
|                     true, | ||||
|                 ) | ||||
|             }); | ||||
|  | ||||
|             invoke | ||||
|                 .respond( | ||||
|                     ctx.http.clone(), | ||||
|                     CreateGenericResponse::new().embed(|e| { | ||||
|                         e.title("Invalid Command") | ||||
|                             .color(THEME_COLOR) | ||||
|                             .description("Type `/help command` to view more about a command below:") | ||||
|                             .fields(groups_iter) | ||||
|                     }), | ||||
|                 ) | ||||
|                 .await?; | ||||
|         } | ||||
|     } else { | ||||
|         let groups = get_groups(framework); | ||||
|         let groups_iter = groups.iter().map(|(name, commands)| { | ||||
|             ( | ||||
|                 name, | ||||
|                 commands | ||||
|                     .iter() | ||||
|                     .map(|c| format!("`{}`", c)) | ||||
|                     .collect::<Vec<String>>() | ||||
|                     .join(" "), | ||||
|                 true, | ||||
|             ) | ||||
|         }); | ||||
|  | ||||
|         invoke | ||||
|             .respond( | ||||
|                 ctx.http.clone(), | ||||
|                 CreateGenericResponse::new().embed(|e| { | ||||
| /// View bot commands | ||||
| #[poise::command(slash_command)] | ||||
| pub async fn help(ctx: Context<'_>) -> Result<(), Error> { | ||||
|     ctx.send(|m| { | ||||
|         m.ephemeral(true).embed(|e| { | ||||
|             e.title("Help") | ||||
|                 .color(THEME_COLOR) | ||||
|                         .description("**Welcome to SoundFX!** | ||||
| To get started, upload a sound with `/upload`, or use `/search` and `/play` to look at some of the public sounds | ||||
|                 .footer(|f| { | ||||
|                     f.text(concat!( | ||||
|                         env!("CARGO_PKG_NAME"), | ||||
|                         " ver ", | ||||
|                         env!("CARGO_PKG_VERSION") | ||||
|                     )) | ||||
|                 }) | ||||
|                 .description( | ||||
|                     "__Info Commands__ | ||||
| `/help` `/info` | ||||
| *run these commands with no options* | ||||
|  | ||||
| Type `/help command` to view help about a command below:") | ||||
|                         .fields(groups_iter) | ||||
|                 }), | ||||
| __Play Commands__ | ||||
| `/play` - Play a sound by name or ID | ||||
| `/queue` - Play sounds on queue instead of instantly | ||||
| `/loop` - Play a sound on loop | ||||
| `/disconnect` - Disconnect the bot | ||||
| `/stop` - Stop playback | ||||
|  | ||||
| __Library Commands__ | ||||
| `/upload` - Upload a sound file | ||||
| `/delete` - Delete a sound file | ||||
| `/download` - Download a sound file | ||||
| `/public` - Set a sound as public/private | ||||
| `/list server` - List sounds on this server | ||||
| `/list user` - List your sounds | ||||
|  | ||||
| __Search Commands__ | ||||
| `/search` - Search for public sounds by name | ||||
| `/random` - View random public sounds | ||||
|  | ||||
| __Setting Commands__ | ||||
| `/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 | ||||
| `/volume` - Change the volume | ||||
|  | ||||
| __Advanced Commands__ | ||||
| `/soundboard` - Create a soundboard", | ||||
|                 ) | ||||
|         }) | ||||
|     }) | ||||
|     .await?; | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[command] | ||||
| #[group("Information")] | ||||
| #[aliases("invite")] | ||||
| #[description("Get additional information on the bot")] | ||||
| async fn info( | ||||
|     ctx: &Context, | ||||
|     invoke: &(dyn CommandInvoke + Sync + Send), | ||||
|     _args: Args, | ||||
| ) -> CommandResult { | ||||
|     let current_user = ctx.cache.current_user(); | ||||
| /// Get additional information about the bot | ||||
| #[poise::command(slash_command)] | ||||
| pub async fn info(ctx: Context<'_>) -> Result<(), Error> { | ||||
|     let current_user = ctx.discord().cache.current_user(); | ||||
|  | ||||
|     invoke.respond(ctx.http.clone(), CreateGenericResponse::new() | ||||
|     ctx.send(|m| m.ephemeral(true) | ||||
|         .embed(|e| e | ||||
|             .title("Info") | ||||
|             .color(THEME_COLOR) | ||||
|             .footer(|f| f | ||||
|                 .text(concat!(env!("CARGO_PKG_NAME"), " ver ", env!("CARGO_PKG_VERSION")))) | ||||
|             .description(format!("Default prefix: `?` | ||||
|  | ||||
| Reset prefix: `@{0} prefix ?` | ||||
|  | ||||
| Invite me: https://discord.com/api/oauth2/authorize?client_id={1}&permissions=3165184&scope=applications.commands%20bot | ||||
|             .description(format!("Invite me: https://discord.com/api/oauth2/authorize?client_id={}&permissions=3165184&scope=applications.commands%20bot | ||||
|  | ||||
| **Welcome to SoundFX!** | ||||
| Developer: <@203532103185465344> | ||||
| Find me on https://discord.jellywx.com/ and on https://github.com/JellyWX :) | ||||
|  | ||||
| **Sound Credits** | ||||
| \"The rain falls against the parasol\" https://freesound.org/people/straget/ | ||||
| \"Heavy Rain\" https://freesound.org/people/lebaston100/ | ||||
| \"Rain on Windows, Interior, A\" https://freesound.org/people/InspectorJ/ | ||||
| \"Seaside Waves, Close, A\" https://freesound.org/people/InspectorJ/ | ||||
| \"Small River 1 - Fast - Close\" https://freesound.org/people/Pfannkuchn/ | ||||
|  | ||||
| **An online dashboard is available!** Visit https://soundfx.jellywx.com/dashboard | ||||
| There is a maximum sound limit per user. This can be removed by subscribing at **https://patreon.com/jellywx**", current_user.name, current_user.id.as_u64())))).await?; | ||||
| There is a maximum sound limit per user. This can be removed by subscribing at **https://patreon.com/jellywx**", | ||||
|                                  current_user.id.as_u64())))).await?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|   | ||||
| @@ -1,32 +1,28 @@ | ||||
| use std::time::Duration; | ||||
|  | ||||
| use regex_command_attr::command; | ||||
| use serenity::{ | ||||
|     client::Context, | ||||
|     framework::standard::CommandResult, | ||||
|     model::id::{GuildId, RoleId}, | ||||
| }; | ||||
| use poise::serenity_prelude::{Attachment, GuildId, RoleId}; | ||||
| use tokio::fs::File; | ||||
|  | ||||
| use crate::{ | ||||
|     framework::{Args, CommandInvoke, CreateGenericResponse}, | ||||
|     sound::Sound, | ||||
|     MySQL, MAX_SOUNDS, PATREON_GUILD, PATREON_ROLE, | ||||
|     cmds::autocomplete_sound, | ||||
|     consts::{MAX_SOUNDS, PATREON_GUILD, PATREON_ROLE}, | ||||
|     models::sound::{Sound, SoundCtx}, | ||||
|     Context, Error, | ||||
| }; | ||||
|  | ||||
| #[command("upload")] | ||||
| #[group("Manage")] | ||||
| #[description("Upload a new sound to the bot")] | ||||
| #[arg( | ||||
|     name = "name", | ||||
|     description = "Name to upload sound to", | ||||
|     kind = "String", | ||||
|     required = true | ||||
| /// Upload a new sound to the bot | ||||
| #[poise::command( | ||||
|     slash_command, | ||||
|     rename = "upload", | ||||
|     category = "Manage", | ||||
|     default_member_permissions = "MANAGE_GUILD", | ||||
|     guild_only = true | ||||
| )] | ||||
| pub async fn upload_new_sound( | ||||
|     ctx: &Context, | ||||
|     invoke: &(dyn CommandInvoke + Sync + Send), | ||||
|     args: Args, | ||||
| ) -> CommandResult { | ||||
|     ctx: Context<'_>, | ||||
|     #[description = "Name to upload sound to"] name: String, | ||||
|     #[description = "Sound file (max. 2MB)"] file: Attachment, | ||||
| ) -> Result<(), Error> { | ||||
|     ctx.defer().await?; | ||||
|  | ||||
|     fn is_numeric(s: &String) -> bool { | ||||
|         for char in s.chars() { | ||||
|             if char.is_digit(10) { | ||||
| @@ -38,36 +34,26 @@ pub async fn upload_new_sound( | ||||
|         true | ||||
|     } | ||||
|  | ||||
|     let new_name = args | ||||
|         .named("name") | ||||
|         .map(|n| n.to_string()) | ||||
|         .unwrap_or(String::new()); | ||||
|  | ||||
|     if !new_name.is_empty() && new_name.len() <= 20 { | ||||
|         if !is_numeric(&new_name) { | ||||
|             let pool = ctx | ||||
|                 .data | ||||
|                 .read() | ||||
|                 .await | ||||
|                 .get::<MySQL>() | ||||
|                 .cloned() | ||||
|                 .expect("Could not get SQLPool from data"); | ||||
|  | ||||
|     if !name.is_empty() && name.len() <= 20 { | ||||
|         if !is_numeric(&name) { | ||||
|             // need to check the name is not currently in use by the user | ||||
|             let count_name = | ||||
|                 Sound::count_named_user_sounds(invoke.author_id().0, &new_name, pool.clone()) | ||||
|                 Sound::count_named_user_sounds(ctx.author().id, &name, &ctx.data().database) | ||||
|                     .await?; | ||||
|             if count_name > 0 { | ||||
|                 invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("You are already using that name. Please choose a unique name for your upload.")).await?; | ||||
|                 ctx.say( | ||||
|                     "You are already using that name. Please choose a unique name for your upload.", | ||||
|                 ) | ||||
|                 .await?; | ||||
|             } else { | ||||
|                 // need to check how many sounds user currently has | ||||
|                 let count = Sound::count_user_sounds(invoke.author_id().0, pool.clone()).await?; | ||||
|                 let count = Sound::count_user_sounds(ctx.author().id, &ctx.data().database).await?; | ||||
|                 let mut permit_upload = true; | ||||
|  | ||||
|                 // need to check if user is patreon or nah | ||||
|                 if count >= *MAX_SOUNDS { | ||||
|                     let patreon_guild_member = GuildId(*PATREON_GUILD) | ||||
|                         .member(ctx, invoke.author_id()) | ||||
|                         .member(ctx.discord(), ctx.author().id) | ||||
|                         .await; | ||||
|  | ||||
|                     if let Ok(member) = patreon_guild_member { | ||||
| @@ -78,153 +64,67 @@ pub async fn upload_new_sound( | ||||
|                 } | ||||
|  | ||||
|                 if permit_upload { | ||||
|                     let attachment = if let Some(attachment) = invoke | ||||
|                         .msg() | ||||
|                         .map(|m| m.attachments.get(0).map(|a| a.url.clone())) | ||||
|                         .flatten() | ||||
|                     { | ||||
|                         Some(attachment) | ||||
|                     } else { | ||||
|                         invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("Please now upload an audio file under 1MB in size (larger files will be automatically trimmed):")).await?; | ||||
|  | ||||
|                         let reply = invoke | ||||
|                             .channel_id() | ||||
|                             .await_reply(&ctx) | ||||
|                             .author_id(invoke.author_id()) | ||||
|                             .timeout(Duration::from_secs(120)) | ||||
|                             .await; | ||||
|  | ||||
|                         match reply { | ||||
|                             Some(reply_msg) => { | ||||
|                                 if let Some(attachment) = reply_msg.attachments.get(0) { | ||||
|                                     Some(attachment.url.clone()) | ||||
|                                 } else { | ||||
|                                     invoke.followup(ctx.http.clone(), CreateGenericResponse::new().content("Please upload 1 attachment following your upload command. Aborted")).await?; | ||||
|  | ||||
|                                     None | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             None => { | ||||
|                                 invoke | ||||
|                                     .followup( | ||||
|                                         ctx.http.clone(), | ||||
|                                         CreateGenericResponse::new() | ||||
|                                             .content("Upload timed out. Please redo the command"), | ||||
|                                     ) | ||||
|                                     .await?; | ||||
|  | ||||
|                                 None | ||||
|                             } | ||||
|                         } | ||||
|                     }; | ||||
|  | ||||
|                     if let Some(url) = attachment { | ||||
|                     match Sound::create_anon( | ||||
|                             &new_name, | ||||
|                             url.as_str(), | ||||
|                             invoke.guild_id().unwrap().0, | ||||
|                             invoke.author_id().0, | ||||
|                             pool, | ||||
|                         &name, | ||||
|                         file.url.as_str(), | ||||
|                         ctx.guild_id().unwrap(), | ||||
|                         ctx.author().id, | ||||
|                         &ctx.data().database, | ||||
|                     ) | ||||
|                     .await | ||||
|                     { | ||||
|                         Ok(_) => { | ||||
|                                 invoke | ||||
|                                     .followup( | ||||
|                                         ctx.http.clone(), | ||||
|                                         CreateGenericResponse::new() | ||||
|                                             .content("Sound has been uploaded"), | ||||
|                                     ) | ||||
|                                     .await?; | ||||
|                             ctx.say("Sound has been uploaded").await?; | ||||
|                         } | ||||
|  | ||||
|                         Err(e) => { | ||||
|                             println!("Error occurred during upload: {:?}", e); | ||||
|                                 invoke | ||||
|                                     .followup( | ||||
|                                         ctx.http.clone(), | ||||
|                                         CreateGenericResponse::new() | ||||
|                                             .content("Sound failed to upload."), | ||||
|                                     ) | ||||
|                                     .await?; | ||||
|                             } | ||||
|                             ctx.say("Sound failed to upload.").await?; | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     invoke.respond( | ||||
|                         ctx.http.clone(), | ||||
|                         CreateGenericResponse::new().content(format!( | ||||
|                     ctx.say(format!( | ||||
|                             "You have reached the maximum number of sounds ({}). Either delete some with `/delete` or join our Patreon for unlimited uploads at **https://patreon.com/jellywx**", | ||||
|                             *MAX_SOUNDS, | ||||
|                         ))).await?; | ||||
|                         )).await?; | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             invoke | ||||
|                 .respond( | ||||
|                     ctx.http.clone(), | ||||
|                     CreateGenericResponse::new() | ||||
|                         .content("Please ensure the sound name contains a non-numerical character"), | ||||
|                 ) | ||||
|             ctx.say("Please ensure the sound name contains a non-numerical character") | ||||
|                 .await?; | ||||
|         } | ||||
|     } else { | ||||
|         invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("Usage: `/upload <name>`. Please ensure the name provided is less than 20 characters in length")).await?; | ||||
|         ctx.say("Usage: `/upload <name>`. Please ensure the name provided is less than 20 characters in length").await?; | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[command("delete")] | ||||
| #[group("Manage")] | ||||
| #[description("Delete a sound you have uploaded")] | ||||
| #[arg( | ||||
|     name = "query", | ||||
|     description = "Delete sound with the specified name or ID", | ||||
|     kind = "String", | ||||
|     required = true | ||||
| )] | ||||
| #[example("`/delete beep` - delete the sound with the name \"beep\"")] | ||||
| /// Delete a sound you have uploaded | ||||
| #[poise::command(slash_command, rename = "delete", guild_only = true)] | ||||
| pub async fn delete_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"); | ||||
|     ctx: Context<'_>, | ||||
|     #[description = "Name or ID of sound to delete"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     name: String, | ||||
| ) -> Result<(), Error> { | ||||
|     let pool = ctx.data().database.clone(); | ||||
|  | ||||
|     let uid = invoke.author_id().0; | ||||
|     let gid = invoke.guild_id().unwrap().0; | ||||
|     let uid = ctx.author().id.0; | ||||
|     let gid = ctx.guild_id().unwrap().0; | ||||
|  | ||||
|     let name = args | ||||
|         .named("query") | ||||
|         .map(|s| s.to_owned()) | ||||
|         .unwrap_or(String::new()); | ||||
|  | ||||
|     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 { | ||||
|         Some(sound) => { | ||||
|             if sound.uploader_id != Some(uid) && sound.server_id != gid { | ||||
|                 invoke | ||||
|                     .respond( | ||||
|                         ctx.http.clone(), | ||||
|                         CreateGenericResponse::new().content( | ||||
|                             "You can only delete sounds from this guild or that you have uploaded.", | ||||
|                         ), | ||||
|                     ) | ||||
|                 ctx.say("You can only delete sounds from this guild or that you have uploaded.") | ||||
|                     .await?; | ||||
|             } else { | ||||
|                 let has_perms = { | ||||
|                     if let Ok(member) = invoke.member(&ctx).await { | ||||
|                         if let Ok(perms) = member.permissions(&ctx) { | ||||
|                     if let Ok(member) = ctx.guild_id().unwrap().member(&ctx.discord(), uid).await { | ||||
|                         if let Ok(perms) = member.permissions(&ctx.discord()) { | ||||
|                             perms.manage_guild() | ||||
|                         } else { | ||||
|                             false | ||||
| @@ -235,109 +135,95 @@ pub async fn delete_sound( | ||||
|                 }; | ||||
|  | ||||
|                 if sound.uploader_id == Some(uid) || has_perms { | ||||
|                     sound.delete(pool).await?; | ||||
|                     sound.delete(&pool).await?; | ||||
|  | ||||
|                     invoke | ||||
|                         .respond( | ||||
|                             ctx.http.clone(), | ||||
|                             CreateGenericResponse::new().content("Sound has been deleted"), | ||||
|                         ) | ||||
|                         .await?; | ||||
|                     ctx.say("Sound has been deleted").await?; | ||||
|                 } else { | ||||
|                     invoke | ||||
|                         .respond( | ||||
|                             ctx.http.clone(), | ||||
|                             CreateGenericResponse::new().content( | ||||
|                                 "Only server admins can delete sounds uploaded by other users.", | ||||
|                             ), | ||||
|                         ) | ||||
|                     ctx.say("Only server admins can delete sounds uploaded by other users.") | ||||
|                         .await?; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         None => { | ||||
|             invoke | ||||
|                 .respond( | ||||
|                     ctx.http.clone(), | ||||
|                     CreateGenericResponse::new().content("Sound could not be found by that name."), | ||||
|                 ) | ||||
|                 .await?; | ||||
|             ctx.say("Sound could not be found by that name.").await?; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[command("public")] | ||||
| #[group("Manage")] | ||||
| #[description("Change a sound between public and private")] | ||||
| #[arg( | ||||
|     name = "query", | ||||
|     kind = "String", | ||||
|     description = "Sound name or ID to change the privacy setting of", | ||||
|     required = true | ||||
| )] | ||||
| #[example("`/public 12` - change sound with ID 12 to private")] | ||||
| #[example("`/public 12` - change sound with ID 12 back to public")] | ||||
| /// Change a sound between public and private | ||||
| #[poise::command(slash_command, rename = "public", guild_only = true)] | ||||
| pub async fn change_public( | ||||
|     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"); | ||||
|     ctx: Context<'_>, | ||||
|     #[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 = invoke.author_id().as_u64().to_owned(); | ||||
|     let uid = ctx.author().id.0; | ||||
|     let gid = ctx.guild_id().unwrap().0; | ||||
|  | ||||
|     let name = args.named("query").unwrap(); | ||||
|     let gid = *invoke.guild_id().unwrap().as_u64(); | ||||
|  | ||||
|     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 { | ||||
|         Some(sound) => { | ||||
|             if sound.uploader_id != Some(uid) { | ||||
|                 invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("You can only change the visibility of sounds you have uploaded. Use `?list me` to view your sounds")).await?; | ||||
|                 ctx.say("You can only change the visibility of sounds you have uploaded. Use `/list` to view your sounds").await?; | ||||
|             } else { | ||||
|                 if sound.public { | ||||
|                     sound.public = false; | ||||
|  | ||||
|                     invoke | ||||
|                         .respond( | ||||
|                             ctx.http.clone(), | ||||
|                             CreateGenericResponse::new() | ||||
|                                 .content("Sound has been set to private 🔒"), | ||||
|                         ) | ||||
|                         .await?; | ||||
|                     ctx.say("Sound has been set to private 🔒").await?; | ||||
|                 } else { | ||||
|                     sound.public = true; | ||||
|  | ||||
|                     invoke | ||||
|                         .respond( | ||||
|                             ctx.http.clone(), | ||||
|                             CreateGenericResponse::new().content("Sound has been set to public 🔓"), | ||||
|                         ) | ||||
|                         .await?; | ||||
|                     ctx.say("Sound has been set to public 🔓").await?; | ||||
|                 } | ||||
|  | ||||
|                 sound.commit(pool).await? | ||||
|                 sound.commit(&pool).await? | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         None => { | ||||
|             invoke | ||||
|                 .respond( | ||||
|                     ctx.http.clone(), | ||||
|                     CreateGenericResponse::new().content("Sound could not be found by that name."), | ||||
|                 ) | ||||
|                 .await?; | ||||
|             ctx.say("Sound could not be found by that name.").await?; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Download a sound file from the bot | ||||
| #[poise::command(slash_command, rename = "download", guild_only = true)] | ||||
| 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).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?; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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 async fn autocomplete_sound( | ||||
|     ctx: Context<'_>, | ||||
|     partial: &str, | ||||
| ) -> Vec<poise::AutocompleteChoice<String>> { | ||||
|     ctx.data() | ||||
|         .autocomplete_user_sounds(&partial, ctx.author().id, ctx.guild_id().unwrap()) | ||||
|         .await | ||||
|         .unwrap_or(vec![]) | ||||
|         .iter() | ||||
|         .map(|s| poise::AutocompleteChoice { | ||||
|             name: s.name.clone(), | ||||
|             value: s.id.to_string(), | ||||
|         }) | ||||
|         .collect() | ||||
| } | ||||
|   | ||||
							
								
								
									
										690
									
								
								src/cmds/play.rs
									
									
									
									
									
								
							
							
						
						
									
										690
									
								
								src/cmds/play.rs
									
									
									
									
									
								
							| @@ -1,392 +1,354 @@ | ||||
| use std::{convert::TryFrom, time::Duration}; | ||||
|  | ||||
| use regex_command_attr::command; | ||||
| use serenity::{ | ||||
|     builder::CreateActionRow, | ||||
|     client::Context, | ||||
|     framework::standard::CommandResult, | ||||
|     model::interactions::{message_component::ButtonStyle, InteractionResponseType}, | ||||
| }; | ||||
| use songbird::{ | ||||
|     create_player, ffmpeg, | ||||
|     input::{cached::Memory, Input}, | ||||
|     Event, | ||||
| use poise::serenity_prelude::{ | ||||
|     builder::CreateActionRow, model::application::component::ButtonStyle, GuildChannel, | ||||
|     ReactionType, | ||||
| }; | ||||
|  | ||||
| use crate::{ | ||||
|     event_handlers::RestartTrack, | ||||
|     framework::{Args, CommandInvoke, CreateGenericResponse}, | ||||
|     guild_data::CtxGuildData, | ||||
|     join_channel, play_from_query, | ||||
|     sound::Sound, | ||||
|     AudioIndex, MySQL, | ||||
|     cmds::autocomplete_sound, | ||||
|     models::{guild_data::CtxGuildData, sound::SoundCtx}, | ||||
|     utils::{join_channel, play_from_query, queue_audio}, | ||||
|     Context, Error, | ||||
| }; | ||||
|  | ||||
| #[command] | ||||
| #[aliases("p")] | ||||
| #[required_permissions(Managed)] | ||||
| #[group("Play")] | ||||
| #[description("Play a sound in your current voice channel")] | ||||
| #[arg( | ||||
|     name = "query", | ||||
|     description = "Play sound with the specified name or ID", | ||||
|     kind = "String", | ||||
|     required = true | ||||
| )] | ||||
| #[example("`/play ubercharge` - play sound with name \"ubercharge\" ")] | ||||
| #[example("`/play 13002` - play sound with ID 13002")] | ||||
| /// Play a sound in your current voice channel | ||||
| #[poise::command(slash_command, default_member_permissions = "SPEAK", guild_only = true)] | ||||
| pub async fn play( | ||||
|     ctx: &Context, | ||||
|     invoke: &(dyn CommandInvoke + Sync + Send), | ||||
|     args: Args, | ||||
| ) -> CommandResult { | ||||
|     let guild = invoke.guild(ctx.cache.clone()).unwrap(); | ||||
|     ctx: Context<'_>, | ||||
|     #[description = "Name or ID of sound to play"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     name: String, | ||||
|     #[description = "Channel to play in (default: your current voice channel)"] | ||||
|     #[channel_types("Voice")] | ||||
|     channel: Option<GuildChannel>, | ||||
| ) -> Result<(), Error> { | ||||
|     ctx.defer().await?; | ||||
|  | ||||
|     invoke | ||||
|         .respond( | ||||
|             ctx.http.clone(), | ||||
|             CreateGenericResponse::new() | ||||
|                 .content(play_from_query(ctx, guild, invoke.author_id(), args, false).await), | ||||
|     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( | ||||
|             play_from_query( | ||||
|                 &ctx.discord(), | ||||
|                 &ctx.data(), | ||||
|                 guild, | ||||
|                 ctx.author().id, | ||||
|                 channel.map(|c| c.id), | ||||
|                 &name, | ||||
|                 false, | ||||
|             ) | ||||
|             .await, | ||||
|         ) | ||||
|         .await?; | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[command("loop")] | ||||
| #[required_permissions(Managed)] | ||||
| #[group("Play")] | ||||
| #[description("Play a sound on loop in your current voice channel")] | ||||
| #[arg( | ||||
|     name = "query", | ||||
|     description = "Play sound with the specified name or ID", | ||||
|     kind = "String", | ||||
|     required = true | ||||
| /// Play up to 25 sounds on queue | ||||
| #[poise::command( | ||||
|     slash_command, | ||||
|     rename = "queue", | ||||
|     default_member_permissions = "SPEAK", | ||||
|     guild_only = true | ||||
| )] | ||||
| #[example("`/loop rain` - loop sound with name \"rain\" ")] | ||||
| #[example("`/loop 13002` - play sound with ID 13002")] | ||||
| pub async fn loop_play( | ||||
|     ctx: &Context, | ||||
|     invoke: &(dyn CommandInvoke + Sync + Send), | ||||
|     args: Args, | ||||
| ) -> CommandResult { | ||||
|     let guild = invoke.guild(ctx.cache.clone()).unwrap(); | ||||
| pub async fn queue_play( | ||||
|     ctx: Context<'_>, | ||||
|     #[description = "Name or ID for queue position 1"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_1: String, | ||||
|     #[description = "Name or ID for queue position 2"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_2: String, | ||||
|     #[description = "Name or ID for queue position 3"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_3: Option<String>, | ||||
|     #[description = "Name or ID for queue position 4"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_4: Option<String>, | ||||
|     #[description = "Name or ID for queue position 5"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_5: Option<String>, | ||||
|     #[description = "Name or ID for queue position 6"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_6: Option<String>, | ||||
|     #[description = "Name or ID for queue position 7"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_7: Option<String>, | ||||
|     #[description = "Name or ID for queue position 8"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_8: Option<String>, | ||||
|     #[description = "Name or ID for queue position 9"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_9: Option<String>, | ||||
|     #[description = "Name or ID for queue position 10"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_10: Option<String>, | ||||
|     #[description = "Name or ID for queue position 11"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_11: Option<String>, | ||||
|     #[description = "Name or ID for queue position 12"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_12: Option<String>, | ||||
|     #[description = "Name or ID for queue position 13"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_13: Option<String>, | ||||
|     #[description = "Name or ID for queue position 14"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_14: Option<String>, | ||||
|     #[description = "Name or ID for queue position 15"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_15: Option<String>, | ||||
|     #[description = "Name or ID for queue position 16"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_16: Option<String>, | ||||
|     #[description = "Name or ID for queue position 17"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_17: Option<String>, | ||||
|     #[description = "Name or ID for queue position 18"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_18: Option<String>, | ||||
|     #[description = "Name or ID for queue position 19"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_19: Option<String>, | ||||
|     #[description = "Name or ID for queue position 20"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_20: Option<String>, | ||||
|     #[description = "Name or ID for queue position 21"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_21: Option<String>, | ||||
|     #[description = "Name or ID for queue position 22"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_22: Option<String>, | ||||
|     #[description = "Name or ID for queue position 23"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_23: Option<String>, | ||||
|     #[description = "Name or ID for queue position 24"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_24: Option<String>, | ||||
|     #[description = "Name or ID for queue position 25"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     sound_25: Option<String>, | ||||
| ) -> Result<(), Error> { | ||||
|     ctx.defer().await?; | ||||
|  | ||||
|     invoke | ||||
|         .respond( | ||||
|             ctx.http.clone(), | ||||
|             CreateGenericResponse::new() | ||||
|                 .content(play_from_query(ctx, guild, invoke.author_id(), args, true).await), | ||||
|         ) | ||||
|         .await?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[command("ambience")] | ||||
| #[required_permissions(Managed)] | ||||
| #[group("Play")] | ||||
| #[description("Play ambient sound in your current voice channel")] | ||||
| #[arg( | ||||
|     name = "name", | ||||
|     description = "Play sound with the specified name", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[example("`/ambience rain on tent` - play the ambient sound \"rain on tent\" ")] | ||||
| pub async fn play_ambience( | ||||
|     ctx: &Context, | ||||
|     invoke: &(dyn CommandInvoke + Sync + Send), | ||||
|     args: Args, | ||||
| ) -> CommandResult { | ||||
|     let guild = invoke.guild(ctx.cache.clone()).unwrap(); | ||||
|     let guild = ctx.guild().unwrap(); | ||||
|  | ||||
|     let channel_to_join = guild | ||||
|         .voice_states | ||||
|         .get(&invoke.author_id()) | ||||
|         .get(&ctx.author().id) | ||||
|         .and_then(|voice_state| voice_state.channel_id); | ||||
|  | ||||
|     match channel_to_join { | ||||
|         Some(user_channel) => { | ||||
|             let audio_index = ctx.data.read().await.get::<AudioIndex>().cloned().unwrap(); | ||||
|             let (call_handler, _) = join_channel(ctx.discord(), guild.clone(), user_channel).await; | ||||
|  | ||||
|             if let Some(search_name) = args.named("name") { | ||||
|                 if let Some(filename) = audio_index.get(search_name) { | ||||
|                     let (track, track_handler) = create_player( | ||||
|                         Input::try_from( | ||||
|                             Memory::new(ffmpeg(format!("audio/{}", filename)).await.unwrap()) | ||||
|                                 .unwrap(), | ||||
|                         ) | ||||
|                         .unwrap(), | ||||
|                     ); | ||||
|             let guild_data = ctx | ||||
|                 .data() | ||||
|                 .guild_data(ctx.guild_id().unwrap()) | ||||
|                 .await | ||||
|                 .unwrap(); | ||||
|  | ||||
|                     let (call_handler, _) = join_channel(ctx, guild.clone(), user_channel).await; | ||||
|                     let guild_data = ctx.guild_data(guild).await.unwrap(); | ||||
|  | ||||
|                     { | ||||
|             let mut lock = call_handler.lock().await; | ||||
|  | ||||
|                         lock.play(track); | ||||
|                     } | ||||
|             let query_terms = [ | ||||
|                 Some(sound_1), | ||||
|                 Some(sound_2), | ||||
|                 sound_3, | ||||
|                 sound_4, | ||||
|                 sound_5, | ||||
|                 sound_6, | ||||
|                 sound_7, | ||||
|                 sound_8, | ||||
|                 sound_9, | ||||
|                 sound_10, | ||||
|                 sound_11, | ||||
|                 sound_12, | ||||
|                 sound_13, | ||||
|                 sound_14, | ||||
|                 sound_15, | ||||
|                 sound_16, | ||||
|                 sound_17, | ||||
|                 sound_18, | ||||
|                 sound_19, | ||||
|                 sound_20, | ||||
|                 sound_21, | ||||
|                 sound_22, | ||||
|                 sound_23, | ||||
|                 sound_24, | ||||
|                 sound_25, | ||||
|             ]; | ||||
|  | ||||
|                     let _ = track_handler.set_volume(guild_data.read().await.volume as f32 / 100.0); | ||||
|                     let _ = track_handler.add_event( | ||||
|                         Event::Periodic( | ||||
|                             track_handler.metadata().duration.unwrap() - Duration::from_millis(200), | ||||
|                             None, | ||||
|                         ), | ||||
|                         RestartTrack {}, | ||||
|                     ); | ||||
|             let mut sounds = vec![]; | ||||
|  | ||||
|                     invoke | ||||
|                         .respond( | ||||
|                             ctx.http.clone(), | ||||
|                             CreateGenericResponse::new() | ||||
|                                 .content(format!("Playing ambience **{}**", search_name)), | ||||
|                         ) | ||||
|             for sound in query_terms.iter().flatten() { | ||||
|                 let search = ctx | ||||
|                     .data() | ||||
|                     .search_for_sound(&sound, ctx.guild_id().unwrap(), ctx.author().id, true) | ||||
|                     .await?; | ||||
|                 } else { | ||||
|                     invoke | ||||
|                         .respond( | ||||
|                             ctx.http.clone(), | ||||
|                             CreateGenericResponse::new().embed(|e| { | ||||
|                                 e.title("Not Found").description(format!( | ||||
|                                     "Could not find ambience sound by name **{}** | ||||
|  | ||||
| __Available ambience sounds:__ | ||||
| {}", | ||||
|                                     search_name, | ||||
|                                     audio_index | ||||
|                                         .keys() | ||||
|                                         .into_iter() | ||||
|                                         .map(|i| i.as_str()) | ||||
|                                         .collect::<Vec<&str>>() | ||||
|                                         .join("\n") | ||||
|                                 )) | ||||
|                             }), | ||||
|                         ) | ||||
|                         .await?; | ||||
|                 } | ||||
|             } else { | ||||
|                 invoke | ||||
|                     .respond( | ||||
|                         ctx.http.clone(), | ||||
|                         CreateGenericResponse::new().embed(|e| { | ||||
|                             e.title("Available Sounds").description( | ||||
|                                 audio_index | ||||
|                                     .keys() | ||||
|                                     .into_iter() | ||||
|                                     .map(|i| i.as_str()) | ||||
|                                     .collect::<Vec<&str>>() | ||||
|                                     .join("\n"), | ||||
|                             ) | ||||
|                         }), | ||||
|                     ) | ||||
|                     .await?; | ||||
|                 if let Some(sound) = search.first() { | ||||
|                     sounds.push(sound.clone()); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             queue_audio( | ||||
|                 &sounds, | ||||
|                 guild_data.read().await.volume, | ||||
|                 &mut lock, | ||||
|                 &ctx.data().database, | ||||
|             ) | ||||
|             .await | ||||
|             .unwrap(); | ||||
|  | ||||
|             ctx.say(format!("Queued {} sounds!", sounds.len())).await?; | ||||
|         } | ||||
|         None => { | ||||
|             invoke | ||||
|                 .respond( | ||||
|                     ctx.http.clone(), | ||||
|                     CreateGenericResponse::new().content("You are not in a voice chat!"), | ||||
|                 ) | ||||
|                 .await?; | ||||
|             ctx.say("You are not in a voice chat!").await?; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[command("soundboard")] | ||||
| #[required_permissions(Managed)] | ||||
| #[group("Play")] | ||||
| #[kind(Slash)] | ||||
| #[description("Get a menu of sounds with buttons to play them")] | ||||
| #[arg( | ||||
|     name = "1", | ||||
|     description = "Query for sound button 1", | ||||
|     kind = "String", | ||||
|     required = true | ||||
| /// Loop a sound in your current voice channel | ||||
| #[poise::command( | ||||
|     slash_command, | ||||
|     rename = "loop", | ||||
|     default_member_permissions = "SPEAK", | ||||
|     guild_only = true | ||||
| )] | ||||
| #[arg( | ||||
|     name = "2", | ||||
|     description = "Query for sound button 2", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "3", | ||||
|     description = "Query for sound button 3", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "4", | ||||
|     description = "Query for sound button 4", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "5", | ||||
|     description = "Query for sound button 5", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "6", | ||||
|     description = "Query for sound button 6", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "7", | ||||
|     description = "Query for sound button 7", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "8", | ||||
|     description = "Query for sound button 8", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "9", | ||||
|     description = "Query for sound button 9", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "10", | ||||
|     description = "Query for sound button 10", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "11", | ||||
|     description = "Query for sound button 11", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "12", | ||||
|     description = "Query for sound button 12", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "13", | ||||
|     description = "Query for sound button 13", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "14", | ||||
|     description = "Query for sound button 14", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "15", | ||||
|     description = "Query for sound button 15", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "16", | ||||
|     description = "Query for sound button 16", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "17", | ||||
|     description = "Query for sound button 17", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "18", | ||||
|     description = "Query for sound button 18", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "19", | ||||
|     description = "Query for sound button 19", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "20", | ||||
|     description = "Query for sound button 20", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "21", | ||||
|     description = "Query for sound button 21", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "22", | ||||
|     description = "Query for sound button 22", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "23", | ||||
|     description = "Query for sound button 23", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "24", | ||||
|     description = "Query for sound button 24", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[arg( | ||||
|     name = "25", | ||||
|     description = "Query for sound button 25", | ||||
|     kind = "String", | ||||
|     required = false | ||||
| )] | ||||
| #[example("`/soundboard ubercharge` - create a soundboard with a button for the \"ubercharge\" sound effect")] | ||||
| #[example("`/soundboard 57000 24119 2 1002 13202` - create a soundboard with 5 buttons, for sounds with the IDs presented")] | ||||
| pub async fn soundboard( | ||||
|     ctx: &Context, | ||||
|     invoke: &(dyn CommandInvoke + Sync + Send), | ||||
|     args: Args, | ||||
| ) -> CommandResult { | ||||
|     if let Some(interaction) = invoke.interaction() { | ||||
|         let _ = interaction | ||||
|             .create_interaction_response(&ctx, |r| { | ||||
|                 r.kind(InteractionResponseType::DeferredChannelMessageWithSource) | ||||
|             }) | ||||
|             .await; | ||||
|     } | ||||
| pub async fn loop_play( | ||||
|     ctx: Context<'_>, | ||||
|     #[description = "Name or ID of sound to loop"] | ||||
|     #[autocomplete = "autocomplete_sound"] | ||||
|     name: String, | ||||
| ) -> Result<(), Error> { | ||||
|     ctx.defer().await?; | ||||
|  | ||||
|     let pool = ctx | ||||
|         .data | ||||
|         .read() | ||||
|         .await | ||||
|         .get::<MySQL>() | ||||
|         .cloned() | ||||
|         .expect("Could not get SQLPool from data"); | ||||
|     let guild = ctx.guild().unwrap(); | ||||
|  | ||||
|     ctx.say( | ||||
|         play_from_query( | ||||
|             &ctx.discord(), | ||||
|             &ctx.data(), | ||||
|             guild, | ||||
|             ctx.author().id, | ||||
|             None, | ||||
|             &name, | ||||
|             true, | ||||
|         ) | ||||
|         .await, | ||||
|     ) | ||||
|     .await?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Get a menu of sounds with buttons to play them | ||||
| #[poise::command( | ||||
|     slash_command, | ||||
|     rename = "soundboard", | ||||
|     category = "Play", | ||||
|     default_member_permissions = "SPEAK", | ||||
|     guild_only = true | ||||
| )] | ||||
| pub async fn soundboard( | ||||
|     ctx: Context<'_>, | ||||
|     #[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>, | ||||
| ) -> Result<(), Error> { | ||||
|     ctx.defer().await?; | ||||
|  | ||||
|     let query_terms = [ | ||||
|         Some(sound_1), | ||||
|         sound_2, | ||||
|         sound_3, | ||||
|         sound_4, | ||||
|         sound_5, | ||||
|         sound_6, | ||||
|         sound_7, | ||||
|         sound_8, | ||||
|         sound_9, | ||||
|         sound_10, | ||||
|         sound_11, | ||||
|         sound_12, | ||||
|         sound_13, | ||||
|         sound_14, | ||||
|         sound_15, | ||||
|         sound_16, | ||||
|         sound_17, | ||||
|         sound_18, | ||||
|         sound_19, | ||||
|         sound_20, | ||||
|     ]; | ||||
|  | ||||
|     let mut sounds = vec![]; | ||||
|  | ||||
|     for n in 1..25 { | ||||
|         let search = Sound::search_for_sound( | ||||
|             args.named(&n.to_string()).unwrap_or(&"".to_string()), | ||||
|             invoke.guild_id().unwrap(), | ||||
|             invoke.author_id(), | ||||
|             pool.clone(), | ||||
|             true, | ||||
|         ) | ||||
|     for sound in query_terms.iter().flatten() { | ||||
|         let search = ctx | ||||
|             .data() | ||||
|             .search_for_sound(&sound, ctx.guild_id().unwrap(), ctx.author().id, true) | ||||
|             .await?; | ||||
|  | ||||
|         if let Some(sound) = search.first() { | ||||
| @@ -396,12 +358,8 @@ pub async fn soundboard( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     invoke | ||||
|         .followup( | ||||
|             ctx.http.clone(), | ||||
|             CreateGenericResponse::new() | ||||
|                 .content("**Play a sound:**") | ||||
|                 .components(|c| { | ||||
|     ctx.send(|m| { | ||||
|         m.content("**Play a sound:**").components(|c| { | ||||
|             for row in sounds.as_slice().chunks(5) { | ||||
|                 let mut action_row: CreateActionRow = Default::default(); | ||||
|                 for sound in row { | ||||
| @@ -415,9 +373,35 @@ pub async fn soundboard( | ||||
|                 c.add_action_row(action_row); | ||||
|             } | ||||
|  | ||||
|                     c | ||||
|                 }), | ||||
|         ) | ||||
|             c.create_action_row(|r| { | ||||
|                 r.create_button(|b| { | ||||
|                     b.label("Stop") | ||||
|                         .emoji(ReactionType::Unicode("⏹".to_string())) | ||||
|                         .style(ButtonStyle::Danger) | ||||
|                         .custom_id("#stop") | ||||
|                 }) | ||||
|                 .create_button(|b| { | ||||
|                     b.label("Mode:") | ||||
|                         .style(ButtonStyle::Secondary) | ||||
|                         .disabled(true) | ||||
|                         .custom_id("#mode") | ||||
|                 }) | ||||
|                 .create_button(|b| { | ||||
|                     b.label("Instant") | ||||
|                         .emoji(ReactionType::Unicode("▶".to_string())) | ||||
|                         .style(ButtonStyle::Secondary) | ||||
|                         .disabled(true) | ||||
|                         .custom_id("#instant") | ||||
|                 }) | ||||
|                 .create_button(|b| { | ||||
|                     b.label("Loop") | ||||
|                         .emoji(ReactionType::Unicode("🔁".to_string())) | ||||
|                         .style(ButtonStyle::Secondary) | ||||
|                         .custom_id("#loop") | ||||
|                 }) | ||||
|             }) | ||||
|         }) | ||||
|     }) | ||||
|     .await?; | ||||
|  | ||||
|     Ok(()) | ||||
|   | ||||
| @@ -1,13 +1,24 @@ | ||||
| use regex_command_attr::command; | ||||
| use serenity::{client::Context, framework::standard::CommandResult}; | ||||
| 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::{ | ||||
|     framework::{Args, CommandInvoke, CreateGenericResponse}, | ||||
|     sound::Sound, | ||||
|     MySQL, | ||||
|     consts::THEME_COLOR, | ||||
|     models::sound::{Sound, SoundCtx}, | ||||
|     Context, Data, Error, | ||||
| }; | ||||
|  | ||||
| fn format_search_results(search_results: Vec<Sound>) -> CreateGenericResponse { | ||||
| fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> { | ||||
|     let mut builder = CreateReply::default(); | ||||
|  | ||||
|     let mut current_character_count = 0; | ||||
|     let title = "Public sounds matching filter:"; | ||||
|  | ||||
| @@ -18,139 +29,254 @@ 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 uploaded sounds | ||||
| #[poise::command(slash_command, rename = "list", guild_only = true)] | ||||
| pub async fn list_sounds(_ctx: Context<'_>) -> Result<(), Error> { | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
|     let sounds; | ||||
|     let mut message_buffer; | ||||
| #[derive(Serialize, Deserialize, Clone, Copy)] | ||||
| enum ListContext { | ||||
|     User = 0, | ||||
|     Guild = 1, | ||||
| } | ||||
|  | ||||
|     if args.named("me").map(|i| i.to_owned()) == Some("me".to_string()) { | ||||
|         sounds = Sound::user_sounds(invoke.author_id(), pool).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(); | ||||
|     } | ||||
|  | ||||
|     for sound in sounds { | ||||
|         message_buffer.push_str( | ||||
|             format!( | ||||
|                 "**{}** ({}), ", | ||||
|                 sound.name, | ||||
|                 if sound.public { "🔓" } else { "🔒" } | ||||
|             ) | ||||
|             .as_str(), | ||||
|         ); | ||||
|  | ||||
|         if message_buffer.len() > 2000 { | ||||
|             invoke | ||||
|                 .respond( | ||||
|                     ctx.http.clone(), | ||||
|                     CreateGenericResponse::new().content(message_buffer), | ||||
|                 ) | ||||
|                 .await?; | ||||
|  | ||||
|             message_buffer = "".to_string(); | ||||
| impl ListContext { | ||||
|     pub fn title(&self) -> &'static str { | ||||
|         match self { | ||||
|             ListContext::User => "Your sounds", | ||||
|             ListContext::Guild => "Server sounds", | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|     if message_buffer.len() > 0 { | ||||
|         invoke | ||||
|             .respond( | ||||
|                 ctx.http.clone(), | ||||
|                 CreateGenericResponse::new().content(message_buffer), | ||||
|             ) | ||||
|             .await?; | ||||
|     } | ||||
| /// Show the sounds uploaded to this server | ||||
| #[poise::command(slash_command, rename = "server", guild_only = true)] | ||||
| pub async fn list_guild_sounds(ctx: Context<'_>) -> Result<(), Error> { | ||||
|     let pager = SoundPager { | ||||
|         nonce: 0, | ||||
|         page: 0, | ||||
|         context: ListContext::Guild, | ||||
|     }; | ||||
|  | ||||
|     pager.reply(ctx).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 = "user", guild_only = true)] | ||||
| pub async fn list_user_sounds(ctx: Context<'_>) -> Result<(), Error> { | ||||
|     let pager = SoundPager { | ||||
|         nonce: 0, | ||||
|         page: 0, | ||||
|         context: ListContext::User, | ||||
|     }; | ||||
|  | ||||
|     pager.reply(ctx).await?; | ||||
|  | ||||
|     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 | ||||
| #[poise::command( | ||||
|     slash_command, | ||||
|     rename = "search", | ||||
|     category = "Search", | ||||
|     guild_only = true | ||||
| )] | ||||
| 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", guild_only = true)] | ||||
| pub async fn show_random_sounds(ctx: Context<'_>) -> Result<(), Error> { | ||||
|     let search_results = sqlx::query_as_unchecked!( | ||||
|         Sound, | ||||
|         " | ||||
| @@ -161,12 +287,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,260 @@ | ||||
| use regex_command_attr::command; | ||||
| use serenity::{client::Context, framework::standard::CommandResult}; | ||||
| use poise::serenity_prelude::{GuildId, User}; | ||||
|  | ||||
| use crate::{ | ||||
|     framework::{Args, CommandInvoke, CreateGenericResponse}, | ||||
|     guild_data::CtxGuildData, | ||||
|     sound::{JoinSoundCtx, Sound}, | ||||
|     MySQL, | ||||
|     cmds::autocomplete_sound, | ||||
|     models::{ | ||||
|         guild_data::{AllowGreet, 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", guild_only = true)] | ||||
| 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?; | ||||
|         guild_data.read().await.commit(&ctx.data().database).await?; | ||||
|  | ||||
|         invoke | ||||
|             .respond( | ||||
|                 ctx.http.clone(), | ||||
|                 CreateGenericResponse::new().content(format!("Volume changed to {}%", volume)), | ||||
|             ) | ||||
|             .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 | ||||
| #[poise::command(slash_command, rename = "greet", guild_only = true)] | ||||
| 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?; | ||||
|  | ||||
| /// Manage greet sounds in this server | ||||
| #[poise::command(slash_command, rename = "server")] | ||||
| pub async fn guild_greet_sound(_ctx: Context<'_>) -> Result<(), Error> { | ||||
|     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")] | ||||
| 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"); | ||||
| /// Set a user's server-specific join sound | ||||
| #[poise::command(slash_command, rename = "set")] | ||||
| pub async fn set_guild_greet_sound( | ||||
|     ctx: Context<'_>, | ||||
|     #[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> { | ||||
|     if user.id != ctx.author().id { | ||||
|         let guild = ctx.guild().unwrap(); | ||||
|         let permissions = guild | ||||
|             .member_permissions(&ctx.discord(), ctx.author().id) | ||||
|             .await; | ||||
|  | ||||
|     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."), | ||||
|             ) | ||||
|         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?; | ||||
|     } else { | ||||
|         let sound_vec = Sound::search_for_sound( | ||||
|             &query, | ||||
|             invoke.guild_id().unwrap(), | ||||
|             user_id, | ||||
|             pool.clone(), | ||||
|             true, | ||||
|         ) | ||||
|  | ||||
|             return Ok(()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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(user.id, ctx.guild_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"); | ||||
| /// Unset a user's server-specific join sound | ||||
| #[poise::command(slash_command, rename = "unset", guild_only = true)] | ||||
| pub async fn unset_guild_greet_sound( | ||||
|     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; | ||||
|  | ||||
|     let guild_data_opt = ctx.guild_data(invoke.guild_id().unwrap()).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?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Manage your own greet sound | ||||
| #[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> { | ||||
|     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 = AllowGreet::Disabled; | ||||
|  | ||||
|         { | ||||
|             guild_data.write().await.allow_greets = !current; | ||||
|         guild_data.read().await.commit(&ctx.data().database).await?; | ||||
|     } | ||||
|  | ||||
|         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" } | ||||
|                 )), | ||||
|             ) | ||||
|     ctx.say("Greet sounds have been disabled in this server") | ||||
|         .await?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Enable only server greet sounds on this server | ||||
| #[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> { | ||||
|     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::Enabled; | ||||
|  | ||||
|         guild_data.read().await.commit(&ctx.data().database).await?; | ||||
|     } | ||||
|  | ||||
|     ctx.say("Greet sounds have been enable in this server") | ||||
|         .await?; | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|   | ||||
| @@ -1,22 +1,17 @@ | ||||
| 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 and clear the play queue | ||||
| #[poise::command( | ||||
|     slash_command, | ||||
|     rename = "stop", | ||||
|     default_member_permissions = "SPEAK", | ||||
|     guild_only = true | ||||
| )] | ||||
| 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 +19,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, default_member_permissions = "SPEAK", guild_only = true)] | ||||
| 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(()) | ||||
| } | ||||
|   | ||||
							
								
								
									
										16
									
								
								src/consts.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/consts.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| use std::env; | ||||
|  | ||||
| pub const THEME_COLOR: u32 = 0x00e0f3; | ||||
|  | ||||
| lazy_static! { | ||||
|     pub static ref UPLOAD_MAX_SIZE: u64 = env::var("UPLOAD_MAX_SIZE") | ||||
|         .unwrap_or_else(|_| "2097152".to_string()) | ||||
|         .parse::<u64>() | ||||
|         .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_ROLE: u64 = env::var("PATREON_ROLE").unwrap().parse::<u64>().unwrap(); | ||||
| } | ||||
| @@ -1,51 +1,32 @@ | ||||
| use std::{collections::HashMap, env}; | ||||
|  | ||||
| use serenity::{ | ||||
|     async_trait, | ||||
|     client::{Context, EventHandler}, | ||||
| use poise::serenity_prelude::{ | ||||
|     model::{ | ||||
|         application::interaction::{Interaction, InteractionResponseType}, | ||||
|         channel::Channel, | ||||
|         gateway::{Activity, Ready}, | ||||
|         guild::Guild, | ||||
|         id::GuildId, | ||||
|         interactions::{Interaction, InteractionResponseType}, | ||||
|         voice::VoiceState, | ||||
|     }, | ||||
|     utils::shard_id, | ||||
|     ActionRowComponent, Activity, Context, CreateActionRow, CreateComponents, | ||||
| }; | ||||
| use songbird::{Event, EventContext, EventHandler as SongbirdEventHandler}; | ||||
|  | ||||
| use crate::{ | ||||
|     framework::{Args, RegexFramework}, | ||||
|     guild_data::CtxGuildData, | ||||
|     join_channel, play_audio, play_from_query, | ||||
|     sound::{JoinSoundCtx, Sound}, | ||||
|     MySQL, ReqwestClient, | ||||
|     cmds::search::SoundPager, | ||||
|     models::{ | ||||
|         guild_data::{AllowGreet, CtxGuildData}, | ||||
|         join_sound::JoinSoundCtx, | ||||
|         sound::Sound, | ||||
|     }, | ||||
|     utils::{join_channel, play_audio, play_from_query}, | ||||
|     Data, Error, | ||||
| }; | ||||
|  | ||||
| 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 | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct Handler; | ||||
|  | ||||
| #[serenity::async_trait] | ||||
| impl EventHandler for Handler { | ||||
|     async fn ready(&self, ctx: Context, _: Ready) { | ||||
| pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> { | ||||
|     match event { | ||||
|         poise::Event::Ready { .. } => { | ||||
|             ctx.set_activity(Activity::watching("for /play")).await; | ||||
|         } | ||||
|  | ||||
|     async fn guild_create(&self, ctx: Context, guild: Guild, is_new: bool) { | ||||
|         if is_new { | ||||
|         poise::Event::GuildCreate { guild, is_new, .. } => { | ||||
|             if *is_new { | ||||
|                 if let Ok(token) = env::var("DISCORDBOTS_TOKEN") { | ||||
|                     let shard_count = ctx.cache.shard_count(); | ||||
|                     let current_shard_id = shard_id(guild.id.as_u64().to_owned(), shard_count); | ||||
| @@ -54,7 +35,9 @@ impl EventHandler for Handler { | ||||
|                         .cache | ||||
|                         .guilds() | ||||
|                         .iter() | ||||
|                     .filter(|g| shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id) | ||||
|                         .filter(|g| { | ||||
|                             shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id | ||||
|                         }) | ||||
|                         .count() as u64; | ||||
|  | ||||
|                     let mut hm = HashMap::new(); | ||||
| @@ -62,15 +45,8 @@ impl EventHandler for Handler { | ||||
|                     hm.insert("shard_id", current_shard_id); | ||||
|                     hm.insert("shard_count", shard_count); | ||||
|  | ||||
|                 let client = ctx | ||||
|                     .data | ||||
|                     .read() | ||||
|                     .await | ||||
|                     .get::<ReqwestClient>() | ||||
|                     .cloned() | ||||
|                     .expect("Could not get ReqwestClient from data"); | ||||
|  | ||||
|                 let response = client | ||||
|                     let response = data | ||||
|                         .http | ||||
|                         .post( | ||||
|                             format!( | ||||
|                                 "https://top.gg/api/bots/{}/stats", | ||||
| @@ -89,37 +65,22 @@ impl EventHandler for Handler { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     async fn voice_state_update( | ||||
|         &self, | ||||
|         ctx: Context, | ||||
|         guild_id_opt: Option<GuildId>, | ||||
|         old: Option<VoiceState>, | ||||
|         new: VoiceState, | ||||
|     ) { | ||||
|         poise::Event::VoiceStateUpdate { old, new, .. } => { | ||||
|             if let Some(past_state) = old { | ||||
|             if let (Some(guild_id), None) = (guild_id_opt, new.channel_id) { | ||||
|                 if let (Some(guild_id), None) = (past_state.guild_id, new.channel_id) { | ||||
|                     if let Some(channel_id) = past_state.channel_id { | ||||
|                         if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) { | ||||
|                             if channel.members(&ctx).await.map(|m| m.len()).unwrap_or(0) <= 1 { | ||||
|                             let songbird = songbird::get(&ctx).await.unwrap(); | ||||
|                                 let songbird = songbird::get(ctx).await.unwrap(); | ||||
|  | ||||
|                                 let _ = songbird.remove(guild_id).await; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|         } else if let (Some(guild_id), Some(user_channel)) = (guild_id_opt, new.channel_id) { | ||||
|             } else if let (Some(guild_id), Some(user_channel)) = (new.guild_id, new.channel_id) { | ||||
|                 if let Some(guild) = ctx.cache.guild(guild_id) { | ||||
|                 let pool = ctx | ||||
|                     .data | ||||
|                     .read() | ||||
|                     .await | ||||
|                     .get::<MySQL>() | ||||
|                     .cloned() | ||||
|                     .expect("Could not get SQLPool from data"); | ||||
|  | ||||
|                 let guild_data_opt = ctx.guild_data(guild.id).await; | ||||
|                     let guild_data_opt = data.guild_data(guild.id).await; | ||||
|  | ||||
|                     if let Ok(guild_data) = guild_data_opt { | ||||
|                         let volume; | ||||
| @@ -132,8 +93,15 @@ impl EventHandler for Handler { | ||||
|                             allowed_greets = read.allow_greets; | ||||
|                         } | ||||
|  | ||||
|                     if allowed_greets { | ||||
|                         if let Some(join_id) = ctx.join_sound(new.user_id).await { | ||||
|                         if allowed_greets != AllowGreet::Disabled { | ||||
|                             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!( | ||||
|                                     Sound, | ||||
|                                     " | ||||
| @@ -143,72 +111,151 @@ SELECT name, id, public, server_id, uploader_id | ||||
|                                         ", | ||||
|                                     join_id | ||||
|                                 ) | ||||
|                             .fetch_one(&pool) | ||||
|                                 .fetch_one(&data.database) | ||||
|                                 .await | ||||
|                                 .unwrap(); | ||||
|  | ||||
|                                 let (handler, _) = join_channel(&ctx, guild, user_channel).await; | ||||
|  | ||||
|                             let _ = play_audio( | ||||
|                                 play_audio( | ||||
|                                     &mut sound, | ||||
|                                     volume, | ||||
|                                     &mut handler.lock().await, | ||||
|                                 pool, | ||||
|                                     &data.database, | ||||
|                                     false, | ||||
|                                 ) | ||||
|                             .await; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async fn interaction_create(&self, ctx: Context, interaction: Interaction) { | ||||
|         match interaction { | ||||
|             Interaction::ApplicationCommand(application_command) => { | ||||
|                 if application_command.guild_id.is_none() { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 let framework = ctx | ||||
|                     .data | ||||
|                     .read() | ||||
|                                 .await | ||||
|                     .get::<RegexFramework>() | ||||
|                     .cloned() | ||||
|                     .expect("RegexFramework not found in context"); | ||||
|  | ||||
|                 framework.execute(ctx, application_command).await; | ||||
|                                 .unwrap(); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         poise::Event::InteractionCreate { interaction } => match interaction { | ||||
|             Interaction::MessageComponent(component) => { | ||||
|                 if component.guild_id.is_none() { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 let mut args = Args { | ||||
|                     args: Default::default(), | ||||
|                 }; | ||||
|                 args.args | ||||
|                     .insert("query".to_string(), component.data.custom_id.clone()); | ||||
|  | ||||
|                 play_from_query( | ||||
|                     &ctx, | ||||
|                     component.guild_id.unwrap().to_guild_cached(&ctx).unwrap(), | ||||
|                     component.user.id, | ||||
|                     args, | ||||
|                     false, | ||||
|                 ) | ||||
|                 .await; | ||||
|  | ||||
|                 if let Some(guild_id) = component.guild_id { | ||||
|                     if let Ok(()) = SoundPager::handle_interaction(ctx, &data, component).await { | ||||
|                     } else { | ||||
|                         let mode = component.data.custom_id.as_str(); | ||||
|                         match mode { | ||||
|                             "#stop" => { | ||||
|                                 component | ||||
|                                     .create_interaction_response(ctx, |r| { | ||||
|                                         r.kind(InteractionResponseType::DeferredUpdateMessage) | ||||
|                                     }) | ||||
|                                     .await | ||||
|                                     .unwrap(); | ||||
|  | ||||
|                                 let songbird = songbird::get(ctx).await.unwrap(); | ||||
|                                 let call_opt = songbird.get(guild_id); | ||||
|  | ||||
|                                 if let Some(call) = call_opt { | ||||
|                                     let mut lock = call.lock().await; | ||||
|  | ||||
|                                     lock.stop(); | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             "#loop" | "#queue" | "#instant" => { | ||||
|                                 component | ||||
|                                     .create_interaction_response(ctx, |r| { | ||||
|                                         r.kind(InteractionResponseType::UpdateMessage) | ||||
|                                             .interaction_response_data(|d| { | ||||
|                                                 let mut c: CreateComponents = Default::default(); | ||||
|  | ||||
|                                                 for action_row in &component.message.components { | ||||
|                                                     let mut a: CreateActionRow = Default::default(); | ||||
|                                                     // These are always buttons | ||||
|                                                     for component in &action_row.components { | ||||
|                                                         match component { | ||||
|                                                             ActionRowComponent::Button(button) => { | ||||
|                                                                 a.create_button(|b| { | ||||
|                                                                     if let Some(label) = | ||||
|                                                                         &button.label | ||||
|                                                                     { | ||||
|                                                                         b.label(label); | ||||
|                                                                     } | ||||
|                                                                     if let Some(emoji) = | ||||
|                                                                         &button.emoji | ||||
|                                                                     { | ||||
|                                                                         b.emoji(emoji.clone()); | ||||
|                                                                     } | ||||
|                                                                     if let Some(custom_id) = | ||||
|                                                                         &button.custom_id | ||||
|                                                                     { | ||||
|                                                                         if custom_id | ||||
|                                                                             .starts_with('#') | ||||
|                                                                         { | ||||
|                                                                             b.custom_id(custom_id) | ||||
|                                                                                 .disabled( | ||||
|                                                                                     custom_id | ||||
|                                                                                         == "#mode" | ||||
|                                                                                         || custom_id | ||||
|                                                                                             == mode, | ||||
|                                                                                 ); | ||||
|                                                                         } else { | ||||
|                                                                             b.custom_id(format!( | ||||
|                                                                                 "{}{}", | ||||
|                                                                                 custom_id | ||||
|                                                                                     .split('#') | ||||
|                                                                                     .next() | ||||
|                                                                                     .unwrap(), | ||||
|                                                                                 mode | ||||
|                                                                             )); | ||||
|                                                                         } | ||||
|                                                                     } | ||||
|  | ||||
|                                                                     b.style(button.style); | ||||
|  | ||||
|                                                                     b | ||||
|                                                                 }); | ||||
|                                                             } | ||||
|                                                             _ => {} | ||||
|                                                         } | ||||
|                                                     } | ||||
|  | ||||
|                                                     c.add_action_row(a); | ||||
|                                                 } | ||||
|  | ||||
|                                                 d.set_components(c) | ||||
|                                             }) | ||||
|                                     }) | ||||
|                                     .await | ||||
|                                     .unwrap(); | ||||
|                             } | ||||
|  | ||||
|                             id_mode => { | ||||
|                                 component | ||||
|                                     .create_interaction_response(ctx, |r| { | ||||
|                                         r.kind(InteractionResponseType::DeferredUpdateMessage) | ||||
|                                     }) | ||||
|                                     .await | ||||
|                                     .unwrap(); | ||||
|  | ||||
|                                 let mut it = id_mode.split('#'); | ||||
|                                 let id = it.next().unwrap(); | ||||
|                                 let mode = it.next().unwrap_or("instant"); | ||||
|  | ||||
|                                 play_from_query( | ||||
|                                     &ctx, | ||||
|                                     &data, | ||||
|                                     guild_id.to_guild_cached(&ctx).unwrap(), | ||||
|                                     component.user.id, | ||||
|                                     None, | ||||
|                                     id.split('#').next().unwrap(), | ||||
|                                     mode == "loop", | ||||
|                                 ) | ||||
|                                 .await; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             _ => {} | ||||
|         }, | ||||
|         _ => {} | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|   | ||||
							
								
								
									
										735
									
								
								src/framework.rs
									
									
									
									
									
								
							
							
						
						
									
										735
									
								
								src/framework.rs
									
									
									
									
									
								
							| @@ -1,735 +0,0 @@ | ||||
| use std::{ | ||||
|     collections::{HashMap, HashSet}, | ||||
|     env, fmt, | ||||
|     hash::{Hash, Hasher}, | ||||
|     sync::Arc, | ||||
| }; | ||||
|  | ||||
| use log::{debug, error, info, warn}; | ||||
| use regex::{Match, Regex, RegexBuilder}; | ||||
| use serde_json::Value; | ||||
| use serenity::{ | ||||
|     async_trait, | ||||
|     builder::{CreateApplicationCommands, CreateComponents, CreateEmbed}, | ||||
|     cache::Cache, | ||||
|     client::Context, | ||||
|     framework::{standard::CommandResult, Framework}, | ||||
|     futures::prelude::future::BoxFuture, | ||||
|     http::Http, | ||||
|     model::{ | ||||
|         channel::{Channel, GuildChannel, Message}, | ||||
|         guild::{Guild, Member}, | ||||
|         id::{ChannelId, GuildId, RoleId, UserId}, | ||||
|         interactions::{ | ||||
|             application_command::{ | ||||
|                 ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType, | ||||
|             }, | ||||
|             InteractionResponseType, | ||||
|         }, | ||||
|     }, | ||||
|     prelude::TypeMapKey, | ||||
|     Result as SerenityResult, | ||||
| }; | ||||
|  | ||||
| use crate::guild_data::CtxGuildData; | ||||
|  | ||||
| type CommandFn = for<'fut> fn( | ||||
|     &'fut Context, | ||||
|     &'fut (dyn CommandInvoke + Sync + Send), | ||||
|     Args, | ||||
| ) -> BoxFuture<'fut, CommandResult>; | ||||
|  | ||||
| pub struct Args { | ||||
|     pub args: HashMap<String, String>, | ||||
| } | ||||
|  | ||||
| impl Args { | ||||
|     pub fn from(message: &str, arg_schema: &'static [&'static Arg]) -> Self { | ||||
|         // construct regex from arg schema | ||||
|         let mut re = arg_schema | ||||
|             .iter() | ||||
|             .map(|a| a.to_regex()) | ||||
|             .collect::<Vec<String>>() | ||||
|             .join(r#"\s*"#); | ||||
|  | ||||
|         re.push_str("$"); | ||||
|  | ||||
|         let regex = Regex::new(&re).unwrap(); | ||||
|         let capture_names = regex.capture_names(); | ||||
|         let captures = regex.captures(message); | ||||
|  | ||||
|         let mut args = HashMap::new(); | ||||
|  | ||||
|         if let Some(captures) = captures { | ||||
|             for name in capture_names.filter(|n| n.is_some()).map(|n| n.unwrap()) { | ||||
|                 if let Some(cap) = captures.name(name) { | ||||
|                     args.insert(name.to_string(), cap.as_str().to_string()); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Self { args } | ||||
|     } | ||||
|  | ||||
|     pub fn named<D: ToString>(&self, name: D) -> Option<&String> { | ||||
|         let name = name.to_string(); | ||||
|  | ||||
|         self.args.get(&name) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct CreateGenericResponse { | ||||
|     content: String, | ||||
|     embed: Option<CreateEmbed>, | ||||
|     components: Option<CreateComponents>, | ||||
| } | ||||
|  | ||||
| impl CreateGenericResponse { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             content: "".to_string(), | ||||
|             embed: None, | ||||
|             components: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn content<D: ToString>(mut self, content: D) -> Self { | ||||
|         self.content = content.to_string(); | ||||
|  | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn embed<F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed>(mut self, f: F) -> Self { | ||||
|         let mut embed = CreateEmbed::default(); | ||||
|         f(&mut embed); | ||||
|  | ||||
|         self.embed = Some(embed); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn components<F: FnOnce(&mut CreateComponents) -> &mut CreateComponents>( | ||||
|         mut self, | ||||
|         f: F, | ||||
|     ) -> Self { | ||||
|         let mut components = CreateComponents::default(); | ||||
|         f(&mut components); | ||||
|  | ||||
|         self.components = Some(components); | ||||
|         self | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[async_trait] | ||||
| pub trait CommandInvoke { | ||||
|     fn channel_id(&self) -> ChannelId; | ||||
|     fn guild_id(&self) -> Option<GuildId>; | ||||
|     fn guild(&self, cache: Arc<Cache>) -> Option<Guild>; | ||||
|     fn author_id(&self) -> UserId; | ||||
|     async fn member(&self, context: &Context) -> SerenityResult<Member>; | ||||
|     fn msg(&self) -> Option<Message>; | ||||
|     fn interaction(&self) -> Option<ApplicationCommandInteraction>; | ||||
|     async fn respond( | ||||
|         &self, | ||||
|         http: Arc<Http>, | ||||
|         generic_response: CreateGenericResponse, | ||||
|     ) -> SerenityResult<()>; | ||||
|     async fn followup( | ||||
|         &self, | ||||
|         http: Arc<Http>, | ||||
|         generic_response: CreateGenericResponse, | ||||
|     ) -> SerenityResult<()>; | ||||
| } | ||||
|  | ||||
| #[async_trait] | ||||
| impl CommandInvoke for Message { | ||||
|     fn channel_id(&self) -> ChannelId { | ||||
|         self.channel_id | ||||
|     } | ||||
|  | ||||
|     fn guild_id(&self) -> Option<GuildId> { | ||||
|         self.guild_id | ||||
|     } | ||||
|  | ||||
|     fn guild(&self, cache: Arc<Cache>) -> Option<Guild> { | ||||
|         self.guild(cache) | ||||
|     } | ||||
|  | ||||
|     fn author_id(&self) -> UserId { | ||||
|         self.author.id | ||||
|     } | ||||
|  | ||||
|     async fn member(&self, context: &Context) -> SerenityResult<Member> { | ||||
|         self.member(context).await | ||||
|     } | ||||
|  | ||||
|     fn msg(&self) -> Option<Message> { | ||||
|         Some(self.clone()) | ||||
|     } | ||||
|  | ||||
|     fn interaction(&self) -> Option<ApplicationCommandInteraction> { | ||||
|         None | ||||
|     } | ||||
|  | ||||
|     async fn respond( | ||||
|         &self, | ||||
|         http: Arc<Http>, | ||||
|         generic_response: CreateGenericResponse, | ||||
|     ) -> SerenityResult<()> { | ||||
|         self.channel_id | ||||
|             .send_message(http, |m| { | ||||
|                 m.content(generic_response.content); | ||||
|  | ||||
|                 if let Some(embed) = generic_response.embed { | ||||
|                     m.set_embed(embed.clone()); | ||||
|                 } | ||||
|  | ||||
|                 if let Some(components) = generic_response.components { | ||||
|                     m.components(|c| { | ||||
|                         *c = components; | ||||
|                         c | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 m | ||||
|             }) | ||||
|             .await | ||||
|             .map(|_| ()) | ||||
|     } | ||||
|  | ||||
|     async fn followup( | ||||
|         &self, | ||||
|         http: Arc<Http>, | ||||
|         generic_response: CreateGenericResponse, | ||||
|     ) -> SerenityResult<()> { | ||||
|         self.channel_id | ||||
|             .send_message(http, |m| { | ||||
|                 m.content(generic_response.content); | ||||
|  | ||||
|                 if let Some(embed) = generic_response.embed { | ||||
|                     m.set_embed(embed.clone()); | ||||
|                 } | ||||
|  | ||||
|                 if let Some(components) = generic_response.components { | ||||
|                     m.components(|c| { | ||||
|                         *c = components; | ||||
|                         c | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 m | ||||
|             }) | ||||
|             .await | ||||
|             .map(|_| ()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[async_trait] | ||||
| impl CommandInvoke for ApplicationCommandInteraction { | ||||
|     fn channel_id(&self) -> ChannelId { | ||||
|         self.channel_id | ||||
|     } | ||||
|  | ||||
|     fn guild_id(&self) -> Option<GuildId> { | ||||
|         self.guild_id | ||||
|     } | ||||
|  | ||||
|     fn guild(&self, cache: Arc<Cache>) -> Option<Guild> { | ||||
|         if let Some(guild_id) = self.guild_id { | ||||
|             guild_id.to_guild_cached(cache) | ||||
|         } else { | ||||
|             None | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn author_id(&self) -> UserId { | ||||
|         self.member.as_ref().unwrap().user.id | ||||
|     } | ||||
|  | ||||
|     async fn member(&self, _: &Context) -> SerenityResult<Member> { | ||||
|         Ok(self.member.clone().unwrap()) | ||||
|     } | ||||
|  | ||||
|     fn msg(&self) -> Option<Message> { | ||||
|         None | ||||
|     } | ||||
|  | ||||
|     fn interaction(&self) -> Option<ApplicationCommandInteraction> { | ||||
|         Some(self.clone()) | ||||
|     } | ||||
|  | ||||
|     async fn respond( | ||||
|         &self, | ||||
|         http: Arc<Http>, | ||||
|         generic_response: CreateGenericResponse, | ||||
|     ) -> SerenityResult<()> { | ||||
|         self.create_interaction_response(http, |r| { | ||||
|             r.kind(InteractionResponseType::ChannelMessageWithSource) | ||||
|                 .interaction_response_data(|d| { | ||||
|                     d.content(generic_response.content); | ||||
|  | ||||
|                     if let Some(embed) = generic_response.embed { | ||||
|                         d.add_embed(embed.clone()); | ||||
|                     } | ||||
|  | ||||
|                     if let Some(components) = generic_response.components { | ||||
|                         d.components(|c| { | ||||
|                             *c = components; | ||||
|                             c | ||||
|                         }); | ||||
|                     } | ||||
|  | ||||
|                     d | ||||
|                 }) | ||||
|         }) | ||||
|         .await | ||||
|         .map(|_| ()) | ||||
|     } | ||||
|  | ||||
|     async fn followup( | ||||
|         &self, | ||||
|         http: Arc<Http>, | ||||
|         generic_response: CreateGenericResponse, | ||||
|     ) -> SerenityResult<()> { | ||||
|         self.create_followup_message(http, |d| { | ||||
|             d.content(generic_response.content); | ||||
|  | ||||
|             if let Some(embed) = generic_response.embed { | ||||
|                 d.add_embed(embed.clone()); | ||||
|             } | ||||
|  | ||||
|             if let Some(components) = generic_response.components { | ||||
|                 d.components(|c| { | ||||
|                     *c = components; | ||||
|                     c | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             d | ||||
|         }) | ||||
|         .await | ||||
|         .map(|_| ()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq)] | ||||
| pub enum PermissionLevel { | ||||
|     Unrestricted, | ||||
|     Managed, | ||||
|     Restricted, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq)] | ||||
| pub enum CommandKind { | ||||
|     Slash, | ||||
|     Both, | ||||
|     Text, | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Arg { | ||||
|     pub name: &'static str, | ||||
|     pub description: &'static str, | ||||
|     pub kind: ApplicationCommandOptionType, | ||||
|     pub required: bool, | ||||
| } | ||||
|  | ||||
| impl Arg { | ||||
|     pub fn to_regex(&self) -> String { | ||||
|         match self.kind { | ||||
|             ApplicationCommandOptionType::String => format!(r#"(?P<{}>.+?)"#, self.name), | ||||
|             ApplicationCommandOptionType::Integer => format!(r#"(?P<{}>\d+)"#, self.name), | ||||
|             ApplicationCommandOptionType::Boolean => format!(r#"(?P<{0}>{0})?"#, self.name), | ||||
|             ApplicationCommandOptionType::User => format!(r#"<(@|@!)(?P<{}>\d+)>"#, self.name), | ||||
|             ApplicationCommandOptionType::Channel => format!(r#"<#(?P<{}>\d+)>"#, self.name), | ||||
|             ApplicationCommandOptionType::Role => format!(r#"<@&(?P<{}>\d+)>"#, self.name), | ||||
|             ApplicationCommandOptionType::Mentionable => { | ||||
|                 format!(r#"<(?P<{0}_pref>@|@!|@&|#)(?P<{0}>\d+)>"#, self.name) | ||||
|             } | ||||
|             _ => String::new(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct Command { | ||||
|     pub fun: CommandFn, | ||||
|  | ||||
|     pub names: &'static [&'static str], | ||||
|  | ||||
|     pub desc: &'static str, | ||||
|     pub examples: &'static [&'static str], | ||||
|     pub group: &'static str, | ||||
|  | ||||
|     pub kind: CommandKind, | ||||
|     pub required_permissions: PermissionLevel, | ||||
|     pub args: &'static [&'static Arg], | ||||
| } | ||||
|  | ||||
| impl Hash for Command { | ||||
|     fn hash<H: Hasher>(&self, state: &mut H) { | ||||
|         self.names[0].hash(state) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl PartialEq for Command { | ||||
|     fn eq(&self, other: &Self) -> bool { | ||||
|         self.names[0] == other.names[0] | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Eq for Command {} | ||||
|  | ||||
| impl Command { | ||||
|     async fn check_permissions(&self, ctx: &Context, guild: &Guild, member: &Member) -> bool { | ||||
|         if self.required_permissions == PermissionLevel::Unrestricted { | ||||
|             true | ||||
|         } else { | ||||
|             let permissions = guild.member_permissions(&ctx, &member.user).await.unwrap(); | ||||
|  | ||||
|             if permissions.manage_guild() { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             if self.required_permissions == PermissionLevel::Managed { | ||||
|                 match ctx.guild_data(guild.id).await { | ||||
|                     Ok(guild_data) => guild_data.read().await.allowed_role.map_or(true, |role| { | ||||
|                         role == guild.id.0 || { | ||||
|                             let role_id = RoleId(role); | ||||
|  | ||||
|                             member.roles.contains(&role_id) | ||||
|                         } | ||||
|                     }), | ||||
|  | ||||
|                     Err(e) => { | ||||
|                         warn!("Unexpected error occurred querying roles: {:?}", e); | ||||
|  | ||||
|                         false | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 false | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl fmt::Debug for Command { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         f.debug_struct("Command") | ||||
|             .field("name", &self.names[0]) | ||||
|             .field("required_permissions", &self.required_permissions) | ||||
|             .field("args", &self.args) | ||||
|             .finish() | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct RegexFramework { | ||||
|     pub commands: HashMap<String, &'static Command>, | ||||
|     pub commands_: HashSet<&'static Command>, | ||||
|     command_matcher: Regex, | ||||
|     default_prefix: String, | ||||
|     client_id: u64, | ||||
|     ignore_bots: bool, | ||||
|     case_insensitive: bool, | ||||
| } | ||||
|  | ||||
| impl TypeMapKey for RegexFramework { | ||||
|     type Value = Arc<RegexFramework>; | ||||
| } | ||||
|  | ||||
| impl RegexFramework { | ||||
|     pub fn new<T: Into<u64>>(client_id: T) -> Self { | ||||
|         Self { | ||||
|             commands: HashMap::new(), | ||||
|             commands_: HashSet::new(), | ||||
|             command_matcher: Regex::new(r#"^$"#).unwrap(), | ||||
|             default_prefix: "".to_string(), | ||||
|             client_id: client_id.into(), | ||||
|             ignore_bots: true, | ||||
|             case_insensitive: true, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn case_insensitive(mut self, case_insensitive: bool) -> Self { | ||||
|         self.case_insensitive = case_insensitive; | ||||
|  | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn default_prefix<T: ToString>(mut self, new_prefix: T) -> Self { | ||||
|         self.default_prefix = new_prefix.to_string(); | ||||
|  | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn ignore_bots(mut self, ignore_bots: bool) -> Self { | ||||
|         self.ignore_bots = ignore_bots; | ||||
|  | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn add_command(mut self, command: &'static Command) -> Self { | ||||
|         self.commands_.insert(command); | ||||
|  | ||||
|         for name in command.names { | ||||
|             self.commands.insert(name.to_string(), command); | ||||
|         } | ||||
|  | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn build(mut self) -> Self { | ||||
|         let command_names; | ||||
|  | ||||
|         { | ||||
|             let mut command_names_vec = self.commands.keys().map(|k| &k[..]).collect::<Vec<&str>>(); | ||||
|  | ||||
|             command_names_vec.sort_unstable_by(|a, b| b.len().cmp(&a.len())); | ||||
|  | ||||
|             command_names = command_names_vec.join("|"); | ||||
|         } | ||||
|  | ||||
|         debug!("Command names: {}", command_names); | ||||
|  | ||||
|         { | ||||
|             let match_string = r#"^(?:(?:<@ID>\s*)|(?:<@!ID>\s*)|(?P<prefix>\S{1,5}?))(?P<cmd>COMMANDS)(?:$|\s+(?P<args>.*))$"# | ||||
|                     .replace("COMMANDS", command_names.as_str()) | ||||
|                     .replace("ID", self.client_id.to_string().as_str()); | ||||
|  | ||||
|             self.command_matcher = RegexBuilder::new(match_string.as_str()) | ||||
|                 .case_insensitive(self.case_insensitive) | ||||
|                 .dot_matches_new_line(true) | ||||
|                 .build() | ||||
|                 .unwrap(); | ||||
|         } | ||||
|  | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     fn _populate_commands<'a>( | ||||
|         &self, | ||||
|         commands: &'a mut CreateApplicationCommands, | ||||
|     ) -> &'a mut CreateApplicationCommands { | ||||
|         for command in &self.commands_ { | ||||
|             commands.create_application_command(|c| { | ||||
|                 c.name(command.names[0]).description(command.desc); | ||||
|  | ||||
|                 for arg in command.args { | ||||
|                     c.create_option(|o| { | ||||
|                         o.name(arg.name) | ||||
|                             .description(arg.description) | ||||
|                             .kind(arg.kind) | ||||
|                             .required(arg.required) | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 c | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         commands | ||||
|     } | ||||
|  | ||||
|     pub async fn build_slash(&self, http: impl AsRef<Http>) { | ||||
|         info!("Building slash commands..."); | ||||
|  | ||||
|         match env::var("TEST_GUILD") | ||||
|             .map(|i| i.parse::<u64>().ok()) | ||||
|             .ok() | ||||
|             .flatten() | ||||
|             .map(|i| GuildId(i)) | ||||
|         { | ||||
|             None => { | ||||
|                 ApplicationCommand::set_global_application_commands(&http, |c| { | ||||
|                     self._populate_commands(c) | ||||
|                 }) | ||||
|                 .await | ||||
|                 .unwrap(); | ||||
|             } | ||||
|             Some(debug_guild) => { | ||||
|                 debug_guild | ||||
|                     .set_application_commands(&http, |c| self._populate_commands(c)) | ||||
|                     .await | ||||
|                     .unwrap(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         info!("Slash commands built!"); | ||||
|     } | ||||
|  | ||||
|     pub async fn execute(&self, ctx: Context, interaction: ApplicationCommandInteraction) { | ||||
|         let command = { | ||||
|             self.commands.get(&interaction.data.name).expect(&format!( | ||||
|                 "Received invalid command: {}", | ||||
|                 interaction.data.name | ||||
|             )) | ||||
|         }; | ||||
|  | ||||
|         if command | ||||
|             .check_permissions( | ||||
|                 &ctx, | ||||
|                 &interaction.guild(ctx.cache.clone()).unwrap(), | ||||
|                 &interaction.clone().member.unwrap(), | ||||
|             ) | ||||
|             .await | ||||
|         { | ||||
|             let mut args = HashMap::new(); | ||||
|  | ||||
|             for arg in interaction | ||||
|                 .data | ||||
|                 .options | ||||
|                 .iter() | ||||
|                 .filter(|o| o.value.is_some()) | ||||
|             { | ||||
|                 args.insert( | ||||
|                     arg.name.clone(), | ||||
|                     match arg.value.clone().unwrap() { | ||||
|                         Value::Bool(b) => { | ||||
|                             if b { | ||||
|                                 arg.name.clone() | ||||
|                             } else { | ||||
|                                 String::new() | ||||
|                             } | ||||
|                         } | ||||
|                         Value::Number(n) => n.to_string(), | ||||
|                         Value::String(s) => s, | ||||
|                         _ => String::new(), | ||||
|                     }, | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             info!( | ||||
|                 "[Shard {}] [Guild {}] /{} {:?}", | ||||
|                 ctx.shard_id, | ||||
|                 interaction.guild_id.unwrap(), | ||||
|                 interaction.data.name, | ||||
|                 args | ||||
|             ); | ||||
|  | ||||
|             (command.fun)(&ctx, &interaction, Args { args }) | ||||
|                 .await | ||||
|                 .unwrap(); | ||||
|         } else if command.required_permissions == PermissionLevel::Managed { | ||||
|             let _ = interaction | ||||
|                     .respond( | ||||
|                         ctx.http.clone(), | ||||
|                         CreateGenericResponse::new().content("You must either be an Admin or have a role specified by `/roles` to do this command") | ||||
|                     ) | ||||
|                     .await; | ||||
|         } else if command.required_permissions == PermissionLevel::Restricted { | ||||
|             let _ = interaction | ||||
|                 .respond( | ||||
|                     ctx.http.clone(), | ||||
|                     CreateGenericResponse::new().content("You must be an Admin to do this command"), | ||||
|                 ) | ||||
|                 .await; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| enum PermissionCheck { | ||||
|     None, // No permissions | ||||
|     All,  // Sufficient permissions | ||||
| } | ||||
|  | ||||
| #[async_trait] | ||||
| impl Framework for RegexFramework { | ||||
|     async fn dispatch(&self, ctx: Context, msg: Message) { | ||||
|         async fn check_self_permissions( | ||||
|             ctx: &Context, | ||||
|             channel: &GuildChannel, | ||||
|         ) -> SerenityResult<PermissionCheck> { | ||||
|             let user_id = ctx.cache.current_user_id(); | ||||
|  | ||||
|             let channel_perms = channel.permissions_for_user(ctx, user_id)?; | ||||
|  | ||||
|             Ok( | ||||
|                 if channel_perms.send_messages() && channel_perms.embed_links() { | ||||
|                     PermissionCheck::All | ||||
|                 } else { | ||||
|                     PermissionCheck::None | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         async fn check_prefix(ctx: &Context, guild: &Guild, prefix_opt: Option<Match<'_>>) -> bool { | ||||
|             if let Some(prefix) = prefix_opt { | ||||
|                 match ctx.guild_data(guild.id).await { | ||||
|                     Ok(guild_data) => prefix.as_str() == guild_data.read().await.prefix, | ||||
|  | ||||
|                     Err(_) => prefix.as_str() == "?", | ||||
|                 } | ||||
|             } else { | ||||
|                 true | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // gate to prevent analysing messages unnecessarily | ||||
|         if msg.author.bot || msg.content.is_empty() { | ||||
|         } | ||||
|         // Guild Command | ||||
|         else if let (Some(guild), Ok(Channel::Guild(channel))) = | ||||
|             (msg.guild(&ctx), msg.channel(&ctx).await) | ||||
|         { | ||||
|             if let Some(full_match) = self.command_matcher.captures(&msg.content) { | ||||
|                 if check_prefix(&ctx, &guild, full_match.name("prefix")).await { | ||||
|                     match check_self_permissions(&ctx, &channel).await { | ||||
|                         Ok(perms) => match perms { | ||||
|                             PermissionCheck::All => { | ||||
|                                 let command = self | ||||
|                                     .commands | ||||
|                                     .get(&full_match.name("cmd").unwrap().as_str().to_lowercase()) | ||||
|                                     .unwrap(); | ||||
|  | ||||
|                                 if command.kind != CommandKind::Slash { | ||||
|                                     let args = full_match | ||||
|                                         .name("args") | ||||
|                                         .map(|m| m.as_str()) | ||||
|                                         .unwrap_or("") | ||||
|                                         .to_string(); | ||||
|  | ||||
|                                     let member = guild.member(&ctx, &msg.author).await.unwrap(); | ||||
|  | ||||
|                                     if command.check_permissions(&ctx, &guild, &member).await { | ||||
|                                         let _ = msg.channel_id.say( | ||||
|                                             &ctx, | ||||
|                                             format!( | ||||
|                                                 "You **must** begin to switch to slash commands. All commands are available via slash commands now. If slash commands don't display in your server, please use this link: https://discord.com/api/oauth2/authorize?client_id={}&permissions=3165184&scope=applications.commands%20bot", | ||||
|                                                 ctx.cache.current_user().id | ||||
|                                             ) | ||||
|                                         ).await; | ||||
|  | ||||
|                                         (command.fun)(&ctx, &msg, Args::from(&args, command.args)) | ||||
|                                             .await | ||||
|                                             .unwrap(); | ||||
|                                     } else if command.required_permissions | ||||
|                                         == PermissionLevel::Managed | ||||
|                                     { | ||||
|                                         let _ = msg.channel_id.say(&ctx, "You must either be an Admin or have a role specified in `?roles` to do this command").await; | ||||
|                                     } else if command.required_permissions | ||||
|                                         == PermissionLevel::Restricted | ||||
|                                     { | ||||
|                                         let _ = msg | ||||
|                                             .channel_id | ||||
|                                             .say(&ctx, "You must be an Admin to do this command") | ||||
|                                             .await; | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             PermissionCheck::None => { | ||||
|                                 warn!("Missing enough permissions for guild {}", guild.id); | ||||
|                             } | ||||
|                         }, | ||||
|  | ||||
|                         Err(e) => { | ||||
|                             error!( | ||||
|                                 "Error occurred getting permissions in guild {}: {:?}", | ||||
|                                 guild.id, e | ||||
|                             ); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										454
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										454
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -2,349 +2,159 @@ | ||||
| extern crate lazy_static; | ||||
|  | ||||
| mod cmds; | ||||
| mod consts; | ||||
| mod error; | ||||
| mod event_handlers; | ||||
| mod framework; | ||||
| mod guild_data; | ||||
| mod sound; | ||||
| mod models; | ||||
| mod utils; | ||||
|  | ||||
| use std::{collections::HashMap, env, sync::Arc}; | ||||
| use std::{env, path::Path, sync::Arc}; | ||||
|  | ||||
| use dashmap::DashMap; | ||||
| use dotenv::dotenv; | ||||
| use log::info; | ||||
| use serenity::{ | ||||
|     client::{bridge::gateway::GatewayIntents, Client, Context}, | ||||
|     http::Http, | ||||
| use poise::serenity_prelude::{ | ||||
|     builder::CreateApplicationCommands, | ||||
|     model::{ | ||||
|         channel::Channel, | ||||
|         guild::Guild, | ||||
|         id::{ChannelId, GuildId, UserId}, | ||||
|         gateway::GatewayIntents, | ||||
|         id::{GuildId, UserId}, | ||||
|     }, | ||||
|     prelude::{Mutex, TypeMapKey}, | ||||
| }; | ||||
| use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call, SerenityInit}; | ||||
| use sqlx::mysql::MySqlPool; | ||||
| use tokio::sync::{MutexGuard, RwLock}; | ||||
| use songbird::SerenityInit; | ||||
| use sqlx::{MySql, Pool}; | ||||
| use tokio::sync::RwLock; | ||||
|  | ||||
| use crate::{ | ||||
|     event_handlers::Handler, | ||||
|     framework::{Args, RegexFramework}, | ||||
|     guild_data::{CtxGuildData, GuildData}, | ||||
|     sound::Sound, | ||||
| }; | ||||
| use crate::{event_handlers::listener, models::guild_data::GuildData}; | ||||
|  | ||||
| struct MySQL; | ||||
| type Database = MySql; | ||||
|  | ||||
| impl TypeMapKey for MySQL { | ||||
|     type Value = MySqlPool; | ||||
| pub struct Data { | ||||
|     database: Pool<Database>, | ||||
|     http: reqwest::Client, | ||||
|     guild_data_cache: DashMap<GuildId, Arc<RwLock<GuildData>>>, | ||||
|     join_sound_cache: DashMap<UserId, DashMap<Option<GuildId>, Option<u32>>>, | ||||
| } | ||||
|  | ||||
| struct ReqwestClient; | ||||
| type Error = Box<dyn std::error::Error + Send + Sync>; | ||||
| type Context<'a> = poise::Context<'a, Data, Error>; | ||||
|  | ||||
| impl TypeMapKey for ReqwestClient { | ||||
|     type Value = Arc<reqwest::Client>; | ||||
| } | ||||
|  | ||||
| struct AudioIndex; | ||||
|  | ||||
| impl TypeMapKey for AudioIndex { | ||||
|     type Value = Arc<HashMap<String, String>>; | ||||
| } | ||||
|  | ||||
| struct GuildDataCache; | ||||
|  | ||||
| impl TypeMapKey for GuildDataCache { | ||||
|     type Value = Arc<DashMap<GuildId, Arc<RwLock<GuildData>>>>; | ||||
| } | ||||
|  | ||||
| struct JoinSoundCache; | ||||
|  | ||||
| impl TypeMapKey for JoinSoundCache { | ||||
|     type Value = Arc<DashMap<UserId, Option<u32>>>; | ||||
| } | ||||
|  | ||||
| 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(); | ||||
| pub async fn register_application_commands( | ||||
|     ctx: &poise::serenity_prelude::Context, | ||||
|     framework: &poise::Framework<Data, Error>, | ||||
|     guild_id: Option<GuildId>, | ||||
| ) -> Result<(), poise::serenity_prelude::Error> { | ||||
|     let mut commands_builder = CreateApplicationCommands::default(); | ||||
|     let commands = &framework.options().commands; | ||||
|     for command in commands { | ||||
|         if let Some(slash_command) = command.create_as_slash_command() { | ||||
|             commands_builder.add_application_command(slash_command); | ||||
|         } | ||||
|  | ||||
|     call_handler.play(track); | ||||
|  | ||||
|     Ok(track_handler) | ||||
| } | ||||
|  | ||||
| async fn join_channel( | ||||
|     ctx: &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, | ||||
|     args: Args, | ||||
|     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 search_term = args.named("query").unwrap(); | ||||
|  | ||||
|             let pool = ctx | ||||
|                 .data | ||||
|                 .read() | ||||
|                 .await | ||||
|                 .get::<MySQL>() | ||||
|                 .cloned() | ||||
|                 .expect("Could not get SQLPool from data"); | ||||
|  | ||||
|             let mut sound_vec = | ||||
|                 Sound::search_for_sound(search_term, 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, 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(), | ||||
|         if let Some(context_menu_command) = command.create_as_context_menu_command() { | ||||
|             commands_builder.add_application_command(context_menu_command); | ||||
|         } | ||||
|     } | ||||
|     let commands_builder = poise::serenity_prelude::json::Value::Array(commands_builder.0); | ||||
|  | ||||
|         None => "You are not in a voice chat!".to_string(), | ||||
|     } | ||||
| } | ||||
|  | ||||
| // entry point | ||||
| #[tokio::main] | ||||
| async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { | ||||
|     env_logger::init(); | ||||
|  | ||||
|     dotenv()?; | ||||
|  | ||||
|     let token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment"); | ||||
|  | ||||
|     let http = Http::new_with_token(&token); | ||||
|  | ||||
|     let logged_in_id = http.get_current_user().await?.id; | ||||
|     let application_id = http.get_current_application_info().await?.id; | ||||
|  | ||||
|     let audio_index = if let Ok(static_audio) = std::fs::read_to_string("audio/audio.json") { | ||||
|         if let Ok(json) = serde_json::from_str::<HashMap<String, String>>(&static_audio) { | ||||
|             Some(json) | ||||
|         } else { | ||||
|             println!( | ||||
|                 "Invalid `audio.json` file. Not loading static audio or providing ambience command" | ||||
|             ); | ||||
|  | ||||
|             None | ||||
|         } | ||||
|     } else { | ||||
|         println!("No `audio.json` file. Not loading static audio or providing ambience command"); | ||||
|  | ||||
|         None | ||||
|     }; | ||||
|  | ||||
|     let mut framework = RegexFramework::new(logged_in_id) | ||||
|         .default_prefix("?") | ||||
|         .case_insensitive(true) | ||||
|         .ignore_bots(true) | ||||
|         // info commands | ||||
|         .add_command(&cmds::info::HELP_COMMAND) | ||||
|         .add_command(&cmds::info::INFO_COMMAND) | ||||
|         // play commands | ||||
|         .add_command(&cmds::play::LOOP_PLAY_COMMAND) | ||||
|         .add_command(&cmds::play::PLAY_COMMAND) | ||||
|         .add_command(&cmds::play::SOUNDBOARD_COMMAND) | ||||
|         .add_command(&cmds::stop::STOP_PLAYING_COMMAND) | ||||
|         .add_command(&cmds::stop::DISCONNECT_COMMAND) | ||||
|         // sound management commands | ||||
|         .add_command(&cmds::manage::UPLOAD_NEW_SOUND_COMMAND) | ||||
|         .add_command(&cmds::manage::DELETE_SOUND_COMMAND) | ||||
|         .add_command(&cmds::manage::CHANGE_PUBLIC_COMMAND) | ||||
|         // setting commands | ||||
|         .add_command(&cmds::settings::CHANGE_PREFIX_COMMAND) | ||||
|         .add_command(&cmds::settings::SET_ALLOWED_ROLES_COMMAND) | ||||
|         .add_command(&cmds::settings::CHANGE_VOLUME_COMMAND) | ||||
|         .add_command(&cmds::settings::ALLOW_GREET_SOUNDS_COMMAND) | ||||
|         .add_command(&cmds::settings::SET_GREET_SOUND_COMMAND) | ||||
|         // search commands | ||||
|         .add_command(&cmds::search::LIST_SOUNDS_COMMAND) | ||||
|         .add_command(&cmds::search::SEARCH_SOUNDS_COMMAND) | ||||
|         .add_command(&cmds::search::SHOW_RANDOM_SOUNDS_COMMAND); | ||||
|  | ||||
|     if audio_index.is_some() { | ||||
|         framework = framework.add_command(&cmds::play::PLAY_AMBIENCE_COMMAND); | ||||
|     } | ||||
|  | ||||
|     framework = framework.build(); | ||||
|  | ||||
|     let framework_arc = Arc::new(framework); | ||||
|  | ||||
|     let mut client = | ||||
|         Client::builder(&env::var("DISCORD_TOKEN").expect("Missing token from environment")) | ||||
|             .intents( | ||||
|                 GatewayIntents::GUILD_VOICE_STATES | ||||
|                     | GatewayIntents::GUILD_MESSAGES | ||||
|                     | GatewayIntents::GUILDS, | ||||
|             ) | ||||
|             .framework_arc(framework_arc.clone()) | ||||
|             .application_id(application_id.0) | ||||
|             .event_handler(Handler) | ||||
|             .register_songbird() | ||||
|             .await | ||||
|             .expect("Error occurred creating client"); | ||||
|  | ||||
|     { | ||||
|         let mysql_pool = | ||||
|             MySqlPool::connect(&env::var("DATABASE_URL").expect("No database URL provided")) | ||||
|                 .await | ||||
|                 .unwrap(); | ||||
|  | ||||
|         let guild_data_cache = Arc::new(DashMap::new()); | ||||
|         let join_sound_cache = Arc::new(DashMap::new()); | ||||
|         let mut data = client.data.write().await; | ||||
|  | ||||
|         data.insert::<GuildDataCache>(guild_data_cache); | ||||
|         data.insert::<JoinSoundCache>(join_sound_cache); | ||||
|         data.insert::<MySQL>(mysql_pool); | ||||
|         data.insert::<RegexFramework>(framework_arc.clone()); | ||||
|         data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new())); | ||||
|  | ||||
|         if let Some(audio_index) = audio_index { | ||||
|             data.insert::<AudioIndex>(Arc::new(audio_index)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     framework_arc.build_slash(&client.cache_and_http.http).await; | ||||
|  | ||||
|     if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| { | ||||
|         let mut split = sr | ||||
|             .split(',') | ||||
|             .map(|val| val.parse::<u64>().expect("SHARD_RANGE not an integer")); | ||||
|  | ||||
|         (split.next(), split.next()) | ||||
|     }) { | ||||
|         let total_shards = env::var("SHARD_COUNT") | ||||
|             .map(|shard_count| shard_count.parse::<u64>().ok()) | ||||
|             .ok() | ||||
|             .flatten() | ||||
|             .expect("No SHARD_COUNT provided, but SHARD_RANGE was provided"); | ||||
|  | ||||
|         assert!( | ||||
|             lower < upper, | ||||
|             "SHARD_RANGE lower limit is not less than the upper limit" | ||||
|         ); | ||||
|  | ||||
|         info!( | ||||
|             "Starting client fragment with shards {}-{}/{}", | ||||
|             lower, upper, total_shards | ||||
|         ); | ||||
|  | ||||
|         client | ||||
|             .start_shard_range([lower, upper], total_shards) | ||||
|     if let Some(guild_id) = guild_id { | ||||
|         ctx.http | ||||
|             .create_guild_application_commands(guild_id.0, &commands_builder) | ||||
|             .await?; | ||||
|     } else if let Ok(total_shards) = env::var("SHARD_COUNT").map(|shard_count| { | ||||
|         shard_count | ||||
|             .parse::<u64>() | ||||
|             .expect("SHARD_COUNT not an integer") | ||||
|     }) { | ||||
|         info!("Starting client with {} shards", total_shards); | ||||
|  | ||||
|         client.start_shards(total_shards).await?; | ||||
|     } else { | ||||
|         info!("Starting client as autosharded"); | ||||
|  | ||||
|         client.start_autosharded().await?; | ||||
|         ctx.http | ||||
|             .create_global_application_commands(&commands_builder) | ||||
|             .await?; | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { | ||||
|     if Path::new("/etc/soundfx-rs/config.env").exists() { | ||||
|         dotenv::from_path("/etc/soundfx-rs/config.env").unwrap(); | ||||
|     } | ||||
|  | ||||
|     env_logger::init(); | ||||
|  | ||||
|     let discord_token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment"); | ||||
|  | ||||
|     let options = poise::FrameworkOptions { | ||||
|         commands: vec![ | ||||
|             cmds::info::help(), | ||||
|             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::queue_play(), | ||||
|             cmds::play::loop_play(), | ||||
|             cmds::play::soundboard(), | ||||
|             poise::Command { | ||||
|                 subcommands: vec![ | ||||
|                     cmds::search::list_guild_sounds(), | ||||
|                     cmds::search::list_user_sounds(), | ||||
|                 ], | ||||
|                 ..cmds::search::list_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![ | ||||
|                     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::enable_greet_sound(), | ||||
|                 ], | ||||
|                 ..cmds::settings::greet_sound() | ||||
|             }, | ||||
|         ], | ||||
|         allowed_mentions: None, | ||||
|         listener: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)), | ||||
|         ..Default::default() | ||||
|     }; | ||||
|  | ||||
|     let database = Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")) | ||||
|         .await | ||||
|         .unwrap(); | ||||
|  | ||||
|     sqlx::migrate!().run(&database).await?; | ||||
|  | ||||
|     poise::Framework::builder() | ||||
|         .token(discord_token) | ||||
|         .user_data_setup(move |ctx, _bot, framework| { | ||||
|             Box::pin(async move { | ||||
|                 register_application_commands(ctx, framework, None) | ||||
|                     .await | ||||
|                     .unwrap(); | ||||
|  | ||||
|                 Ok(Data { | ||||
|                     http: reqwest::Client::new(), | ||||
|                     database, | ||||
|                     guild_data_cache: Default::default(), | ||||
|                     join_sound_cache: Default::default(), | ||||
|                 }) | ||||
|             }) | ||||
|         }) | ||||
|         .options(options) | ||||
|         .client_settings(move |client_builder| client_builder.register_songbird()) | ||||
|         .intents(GatewayIntents::GUILD_VOICE_STATES | GatewayIntents::GUILDS) | ||||
|         .run_autosharded() | ||||
|         .await?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|   | ||||
| @@ -1,17 +1,25 @@ | ||||
| use std::sync::Arc; | ||||
| 
 | ||||
| use serenity::{async_trait, model::id::GuildId, prelude::Context}; | ||||
| use sqlx::mysql::MySqlPool; | ||||
| use poise::serenity_prelude::{async_trait, model::id::GuildId}; | ||||
| use sqlx::{Executor, Type}; | ||||
| use tokio::sync::RwLock; | ||||
| 
 | ||||
| use crate::{GuildDataCache, MySQL}; | ||||
| use crate::{Context, Data, Database}; | ||||
| 
 | ||||
| #[derive(Copy, Clone, Type, PartialEq)] | ||||
| #[repr(i32)] | ||||
| pub enum AllowGreet { | ||||
|     Enabled = 1, | ||||
|     GuildOnly = 0, | ||||
|     Disabled = -1, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct GuildData { | ||||
|     pub id: u64, | ||||
|     pub prefix: String, | ||||
|     pub volume: u8, | ||||
|     pub allow_greets: bool, | ||||
|     pub allow_greets: AllowGreet, | ||||
|     pub allowed_role: Option<u64>, | ||||
| } | ||||
| 
 | ||||
| @@ -24,31 +32,31 @@ pub trait CtxGuildData { | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl CtxGuildData for Context { | ||||
| impl CtxGuildData for Context<'_> { | ||||
|     async fn guild_data<G: Into<GuildId> + Send + Sync>( | ||||
|         &self, | ||||
|         guild_id: G, | ||||
|     ) -> Result<Arc<RwLock<GuildData>>, sqlx::Error> { | ||||
|         self.data().guild_data(guild_id).await | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl CtxGuildData for Data { | ||||
|     async fn guild_data<G: Into<GuildId> + Send + Sync>( | ||||
|         &self, | ||||
|         guild_id: G, | ||||
|     ) -> Result<Arc<RwLock<GuildData>>, sqlx::Error> { | ||||
|         let guild_id = guild_id.into(); | ||||
| 
 | ||||
|         let guild_cache = self | ||||
|             .data | ||||
|             .read() | ||||
|             .await | ||||
|             .get::<GuildDataCache>() | ||||
|             .cloned() | ||||
|             .unwrap(); | ||||
| 
 | ||||
|         let x = if let Some(guild_data) = guild_cache.get(&guild_id) { | ||||
|         let x = if let Some(guild_data) = self.guild_data_cache.get(&guild_id) { | ||||
|             Ok(guild_data.clone()) | ||||
|         } else { | ||||
|             let pool = self.data.read().await.get::<MySQL>().cloned().unwrap(); | ||||
| 
 | ||||
|             match GuildData::from_id(guild_id, pool).await { | ||||
|             match GuildData::from_id(guild_id, &self.database).await { | ||||
|                 Ok(d) => { | ||||
|                     let lock = Arc::new(RwLock::new(d)); | ||||
| 
 | ||||
|                     guild_cache.insert(guild_id, lock.clone()); | ||||
|                     self.guild_data_cache.insert(guild_id, lock.clone()); | ||||
| 
 | ||||
|                     Ok(lock) | ||||
|                 } | ||||
| @@ -64,7 +72,7 @@ impl CtxGuildData for Context { | ||||
| impl GuildData { | ||||
|     pub async fn from_id<G: Into<GuildId>>( | ||||
|         guild_id: G, | ||||
|         db_pool: MySqlPool, | ||||
|         db_pool: impl Executor<'_, Database = Database> + Copy, | ||||
|     ) -> Result<GuildData, sqlx::Error> { | ||||
|         let guild_id = guild_id.into(); | ||||
| 
 | ||||
| @@ -77,7 +85,7 @@ SELECT id, prefix, volume, allow_greets, allowed_role | ||||
|             ",
 | ||||
|             guild_id.as_u64() | ||||
|         ) | ||||
|         .fetch_one(&db_pool) | ||||
|         .fetch_one(db_pool) | ||||
|         .await; | ||||
| 
 | ||||
|         match guild_data { | ||||
| @@ -91,7 +99,7 @@ SELECT id, prefix, volume, allow_greets, allowed_role | ||||
| 
 | ||||
|     async fn create_from_guild<G: Into<GuildId>>( | ||||
|         guild_id: G, | ||||
|         db_pool: MySqlPool, | ||||
|         db_pool: impl Executor<'_, Database = Database>, | ||||
|     ) -> Result<GuildData, sqlx::Error> { | ||||
|         let guild_id = guild_id.into(); | ||||
| 
 | ||||
| @@ -102,21 +110,21 @@ INSERT INTO servers (id) | ||||
|             ",
 | ||||
|             guild_id.as_u64() | ||||
|         ) | ||||
|         .execute(&db_pool) | ||||
|         .execute(db_pool) | ||||
|         .await?; | ||||
| 
 | ||||
|         Ok(GuildData { | ||||
|             id: guild_id.as_u64().to_owned(), | ||||
|             prefix: String::from("?"), | ||||
|             volume: 100, | ||||
|             allow_greets: true, | ||||
|             allow_greets: AllowGreet::Enabled, | ||||
|             allowed_role: None, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     pub async fn commit( | ||||
|         &self, | ||||
|         db_pool: MySqlPool, | ||||
|         db_pool: impl Executor<'_, Database = Database>, | ||||
|     ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { | ||||
|         sqlx::query!( | ||||
|             " | ||||
| @@ -135,7 +143,7 @@ WHERE | ||||
|             self.allowed_role, | ||||
|             self.id | ||||
|         ) | ||||
|         .execute(&db_pool) | ||||
|         .execute(db_pool) | ||||
|         .await?; | ||||
| 
 | ||||
|         Ok(()) | ||||
							
								
								
									
										145
									
								
								src/models/join_sound.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/models/join_sound.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | ||||
| use poise::serenity_prelude::{async_trait, model::id::UserId, GuildId}; | ||||
|  | ||||
| use crate::Data; | ||||
|  | ||||
| #[async_trait] | ||||
| pub trait JoinSoundCtx { | ||||
|     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>; | ||||
|     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>, | ||||
|     ) -> Result<(), sqlx::Error>; | ||||
| } | ||||
|  | ||||
| struct JoinSound { | ||||
|     join_sound_id: u32, | ||||
| } | ||||
|  | ||||
| #[async_trait] | ||||
| impl JoinSoundCtx for Data { | ||||
|     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 guild_id = guild_id.map(|g| g.into()); | ||||
|  | ||||
|         let cached_join_id = self | ||||
|             .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 { | ||||
|             let join_sound_id = { | ||||
|                 let join_id_res = if guild_only { | ||||
|                     sqlx::query_as!( | ||||
|                         JoinSound, | ||||
|                         " | ||||
| SELECT join_sound_id | ||||
|     FROM join_sounds | ||||
|     WHERE user = ? | ||||
|     AND guild = ? | ||||
|     ORDER BY guild IS NULL | ||||
|                     ", | ||||
|                         user_id.as_u64(), | ||||
|                         guild_id.map(|g| g.0) | ||||
|                     ) | ||||
|                     .fetch_one(&self.database) | ||||
|                     .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 { | ||||
|                     Some(row.join_sound_id) | ||||
|                 } else { | ||||
|                     None | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             self.join_sound_cache.entry(user_id).and_modify(|d| { | ||||
|                 d.insert(guild_id, join_sound_id); | ||||
|             }); | ||||
|  | ||||
|             join_sound_id | ||||
|         }; | ||||
|  | ||||
|         x | ||||
|     } | ||||
|  | ||||
|     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>, | ||||
|     ) -> Result<(), sqlx::Error> { | ||||
|         let user_id = user_id.into(); | ||||
|         let guild_id = guild_id.map(|g| g.into()); | ||||
|  | ||||
|         self.join_sound_cache.entry(user_id).and_modify(|d| { | ||||
|             d.insert(guild_id, join_id); | ||||
|         }); | ||||
|  | ||||
|         let mut transaction = self.database.begin().await?; | ||||
|  | ||||
|         match join_id { | ||||
|             Some(join_id) => { | ||||
|                 sqlx::query!( | ||||
|                     "DELETE FROM join_sounds WHERE user = ? AND guild <=> ?", | ||||
|                     user_id.0, | ||||
|                     guild_id.map(|g| g.0) | ||||
|                 ) | ||||
|                 .execute(&mut transaction) | ||||
|                 .await?; | ||||
|  | ||||
|                 sqlx::query!( | ||||
|                     "INSERT INTO join_sounds (user, join_sound_id, guild) VALUES (?, ?, ?)", | ||||
|                     user_id.0, | ||||
|                     join_id, | ||||
|                     guild_id.map(|g| g.0) | ||||
|                 ) | ||||
|                 .execute(&mut transaction) | ||||
|                 .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(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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,110 +1,11 @@ | ||||
| use std::{env, path::Path}; | ||||
| 
 | ||||
| use serenity::{async_trait, model::id::UserId, prelude::Context}; | ||||
| use poise::serenity_prelude::async_trait; | ||||
| use songbird::input::restartable::Restartable; | ||||
| use sqlx::mysql::MySqlPool; | ||||
| use sqlx::Executor; | ||||
| use tokio::{fs::File, io::AsyncWriteExt, process::Command}; | ||||
| 
 | ||||
| use super::error::ErrorTypes; | ||||
| use crate::{JoinSoundCache, MySQL}; | ||||
| 
 | ||||
| #[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 Context { | ||||
|     async fn join_sound<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Option<u32> { | ||||
|         let user_id = user_id.into(); | ||||
|         let join_sound_cache = self | ||||
|             .data | ||||
|             .read() | ||||
|             .await | ||||
|             .get::<JoinSoundCache>() | ||||
|             .cloned() | ||||
|             .unwrap(); | ||||
| 
 | ||||
|         let x = if let Some(join_sound_id) = join_sound_cache.get(&user_id) { | ||||
|             join_sound_id.value().clone() | ||||
|         } else { | ||||
|             let join_sound_id = { | ||||
|                 let pool = self.data.read().await.get::<MySQL>().cloned().unwrap(); | ||||
| 
 | ||||
|                 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 | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             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(); | ||||
|         let join_sound_cache = self | ||||
|             .data | ||||
|             .read() | ||||
|             .await | ||||
|             .get::<JoinSoundCache>() | ||||
|             .cloned() | ||||
|             .unwrap(); | ||||
| 
 | ||||
|         join_sound_cache.insert(user_id, join_id); | ||||
| 
 | ||||
|         let pool = self.data.read().await.get::<MySQL>().cloned().unwrap(); | ||||
| 
 | ||||
|         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::{consts::UPLOAD_MAX_SIZE, error::ErrorTypes, Data, Database}; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct Sound { | ||||
| @@ -121,16 +22,50 @@ 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, | ||||
|         page: Option<u64>, | ||||
|     ) -> Result<Vec<Sound>, sqlx::Error>; | ||||
|     async fn guild_sounds<G: Into<u64> + Send>( | ||||
|         &self, | ||||
|         guild_id: G, | ||||
|         page: Option<u64>, | ||||
|     ) -> 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] | ||||
| 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:") { | ||||
| @@ -148,7 +83,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 | ||||
| @@ -172,7 +107,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 | ||||
| @@ -193,7 +128,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 | ||||
| @@ -218,7 +153,141 @@ SELECT name, id, public, server_id, uploader_id | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async fn src(&self, db_pool: MySqlPool) -> Vec<u8> { | ||||
|     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> { | ||||
|         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, | ||||
|         page: Option<u64>, | ||||
|     ) -> Result<Vec<Sound>, sqlx::Error> { | ||||
|         let sounds = match page { | ||||
|             Some(page) => { | ||||
|                 sqlx::query_as_unchecked!( | ||||
|                     Sound, | ||||
|                     " | ||||
| SELECT name, id, public, server_id, uploader_id | ||||
|     FROM sounds | ||||
|     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() | ||||
|                 ) | ||||
|                 .fetch_all(&self.database) | ||||
|                 .await? | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         Ok(sounds) | ||||
|     } | ||||
| 
 | ||||
|     async fn guild_sounds<G: Into<u64> + Send>( | ||||
|         &self, | ||||
|         guild_id: G, | ||||
|         page: Option<u64>, | ||||
|     ) -> Result<Vec<Sound>, sqlx::Error> { | ||||
|         let sounds = match page { | ||||
|             Some(page) => { | ||||
|                 sqlx::query_as_unchecked!( | ||||
|                     Sound, | ||||
|                     " | ||||
| SELECT name, id, public, server_id, uploader_id | ||||
|     FROM sounds | ||||
|     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() | ||||
|                 ) | ||||
|                 .fetch_all(&self.database) | ||||
|                 .await? | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         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 { | ||||
|     async fn src(&self, db_pool: impl Executor<'_, Database = Database>) -> Vec<u8> { | ||||
|         struct Src { | ||||
|             src: Vec<u8>, | ||||
|         } | ||||
| @@ -233,7 +302,7 @@ SELECT src | ||||
|             ",
 | ||||
|             self.id | ||||
|         ) | ||||
|         .fetch_one(&db_pool) | ||||
|         .fetch_one(db_pool) | ||||
|         .await | ||||
|         .unwrap(); | ||||
| 
 | ||||
| @@ -242,8 +311,8 @@ SELECT src | ||||
| 
 | ||||
|     pub async fn store_sound_source( | ||||
|         &self, | ||||
|         db_pool: MySqlPool, | ||||
|     ) -> Result<Restartable, Box<dyn std::error::Error + Send + Sync>> { | ||||
|         db_pool: impl Executor<'_, Database = Database>, | ||||
|     ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { | ||||
|         let caching_location = env::var("CACHING_LOCATION").unwrap_or(String::from("/tmp")); | ||||
| 
 | ||||
|         let path_name = format!("{}/sound-{}", caching_location, self.id); | ||||
| @@ -255,15 +324,26 @@ SELECT src | ||||
|             file.write_all(&self.src(db_pool).await).await?; | ||||
|         } | ||||
| 
 | ||||
|         Ok(path_name) | ||||
|     } | ||||
| 
 | ||||
|     pub async fn playable( | ||||
|         &self, | ||||
|         db_pool: impl Executor<'_, Database = Database>, | ||||
|     ) -> Result<Restartable, Box<dyn std::error::Error + Send + Sync>> { | ||||
|         let path_name = self.store_sound_source(db_pool).await?; | ||||
| 
 | ||||
|         Ok(Restartable::ffmpeg(path_name, false) | ||||
|             .await | ||||
|             .expect("FFMPEG ERROR!")) | ||||
|     } | ||||
| 
 | ||||
|     pub async fn count_user_sounds( | ||||
|         user_id: u64, | ||||
|         db_pool: MySqlPool, | ||||
|     ) -> Result<u32, sqlx::error::Error> { | ||||
|     pub async fn count_user_sounds<U: Into<u64>>( | ||||
|         user_id: U, | ||||
|         db_pool: impl Executor<'_, Database = Database>, | ||||
|     ) -> Result<u32, sqlx::Error> { | ||||
|         let user_id = user_id.into(); | ||||
| 
 | ||||
|         let c = sqlx::query!( | ||||
|             " | ||||
| SELECT COUNT(1) as count | ||||
| @@ -272,18 +352,20 @@ SELECT COUNT(1) as count | ||||
|         ",
 | ||||
|             user_id | ||||
|         ) | ||||
|         .fetch_one(&db_pool) | ||||
|         .fetch_one(db_pool) | ||||
|         .await? | ||||
|         .count; | ||||
| 
 | ||||
|         Ok(c as u32) | ||||
|     } | ||||
| 
 | ||||
|     pub async fn count_named_user_sounds( | ||||
|         user_id: u64, | ||||
|     pub async fn count_named_user_sounds<U: Into<u64>>( | ||||
|         user_id: U, | ||||
|         name: &String, | ||||
|         db_pool: MySqlPool, | ||||
|     ) -> Result<u32, sqlx::error::Error> { | ||||
|         db_pool: impl Executor<'_, Database = Database>, | ||||
|     ) -> Result<u32, sqlx::Error> { | ||||
|         let user_id = user_id.into(); | ||||
| 
 | ||||
|         let c = sqlx::query!( | ||||
|             " | ||||
| SELECT COUNT(1) as count | ||||
| @@ -295,7 +377,7 @@ SELECT COUNT(1) as count | ||||
|             user_id, | ||||
|             name | ||||
|         ) | ||||
|         .fetch_one(&db_pool) | ||||
|         .fetch_one(db_pool) | ||||
|         .await? | ||||
|         .count; | ||||
| 
 | ||||
| @@ -304,7 +386,7 @@ SELECT COUNT(1) as count | ||||
| 
 | ||||
|     pub async fn commit( | ||||
|         &self, | ||||
|         db_pool: MySqlPool, | ||||
|         db_pool: impl Executor<'_, Database = Database>, | ||||
|     ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { | ||||
|         sqlx::query!( | ||||
|             " | ||||
| @@ -317,7 +399,7 @@ WHERE | ||||
|             self.public, | ||||
|             self.id | ||||
|         ) | ||||
|         .execute(&db_pool) | ||||
|         .execute(db_pool) | ||||
|         .await?; | ||||
| 
 | ||||
|         Ok(()) | ||||
| @@ -325,29 +407,25 @@ WHERE | ||||
| 
 | ||||
|     pub async fn delete( | ||||
|         &self, | ||||
|         db_pool: MySqlPool, | ||||
|         db_pool: impl Executor<'_, Database = Database>, | ||||
|     ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { | ||||
|         sqlx::query!( | ||||
|             " | ||||
| DELETE | ||||
|     FROM sounds | ||||
|     WHERE id = ? | ||||
|             ",
 | ||||
|             self.id | ||||
|         ) | ||||
|         .execute(&db_pool) | ||||
|         sqlx::query!("DELETE FROM sounds WHERE id = ?", self.id) | ||||
|             .execute(db_pool) | ||||
|             .await?; | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub async fn create_anon( | ||||
|     pub async fn create_anon<G: Into<u64>, U: Into<u64>>( | ||||
|         name: &str, | ||||
|         src_url: &str, | ||||
|         server_id: u64, | ||||
|         user_id: u64, | ||||
|         db_pool: MySqlPool, | ||||
|         server_id: G, | ||||
|         user_id: U, | ||||
|         db_pool: impl Executor<'_, Database = Database>, | ||||
|     ) -> Result<(), Box<dyn std::error::Error + Send + Sync + Send>> { | ||||
|         let server_id = server_id.into(); | ||||
|         let user_id = user_id.into(); | ||||
| 
 | ||||
|         async fn process_src(src_url: &str) -> Option<Vec<u8>> { | ||||
|             let output = Command::new("ffmpeg") | ||||
|                 .kill_on_drop(true) | ||||
| @@ -355,12 +433,10 @@ DELETE | ||||
|                 .arg(src_url) | ||||
|                 .arg("-loglevel") | ||||
|                 .arg("error") | ||||
|                 .arg("-b:a") | ||||
|                 .arg("28000") | ||||
|                 .arg("-f") | ||||
|                 .arg("opus") | ||||
|                 .arg("-fs") | ||||
|                 .arg("1048576") | ||||
|                 .arg(UPLOAD_MAX_SIZE.to_string()) | ||||
|                 .arg("pipe:1") | ||||
|                 .output() | ||||
|                 .await; | ||||
| @@ -392,7 +468,7 @@ INSERT INTO sounds (name, server_id, uploader_id, public, src) | ||||
|                     user_id, | ||||
|                     data | ||||
|                 ) | ||||
|                 .execute(&db_pool) | ||||
|                 .execute(db_pool) | ||||
|                 .await | ||||
|                 { | ||||
|                     Ok(_) => Ok(()), | ||||
| @@ -404,42 +480,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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										159
									
								
								src/utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/utils.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| use std::sync::Arc; | ||||
|  | ||||
| use poise::serenity_prelude::model::{ | ||||
|     channel::Channel, | ||||
|     guild::Guild, | ||||
|     id::{ChannelId, UserId}, | ||||
| }; | ||||
| use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call}; | ||||
| use sqlx::Executor; | ||||
| use tokio::sync::{Mutex, MutexGuard}; | ||||
|  | ||||
| use crate::{ | ||||
|     models::{ | ||||
|         guild_data::CtxGuildData, | ||||
|         sound::{Sound, SoundCtx}, | ||||
|     }, | ||||
|     Data, Database, | ||||
| }; | ||||
|  | ||||
| pub async fn play_audio( | ||||
|     sound: &Sound, | ||||
|     volume: u8, | ||||
|     call_handler: &mut MutexGuard<'_, Call>, | ||||
|     db_pool: impl Executor<'_, Database = Database>, | ||||
|     loop_: bool, | ||||
| ) -> Result<TrackHandle, Box<dyn std::error::Error + Send + Sync>> { | ||||
|     let (track, track_handler) = create_player(sound.playable(db_pool).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 queue_audio( | ||||
|     sounds: &[Sound], | ||||
|     volume: u8, | ||||
|     call_handler: &mut MutexGuard<'_, Call>, | ||||
|     db_pool: impl Executor<'_, Database = Database> + Copy, | ||||
| ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { | ||||
|     for sound in sounds { | ||||
|         let (a, b) = create_player(sound.playable(db_pool).await?.into()); | ||||
|  | ||||
|         let _ = b.set_volume(volume as f32 / 100.0); | ||||
|  | ||||
|         call_handler.enqueue(a); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| 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, | ||||
|     channel: Option<ChannelId>, | ||||
|     query: &str, | ||||
|     loop_: bool, | ||||
| ) -> String { | ||||
|     let guild_id = guild.id; | ||||
|  | ||||
|     let channel_to_join = channel.or_else(|| { | ||||
|         guild | ||||
|             .voice_states | ||||
|             .get(&user_id) | ||||
|             .and_then(|voice_state| voice_state.channel_id) | ||||
|     }); | ||||
|  | ||||
|     match channel_to_join { | ||||
|         Some(user_channel) => { | ||||
|             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, | ||||
|                             &data.database, | ||||
|                             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(), | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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