This commit is contained in:
Tanner Sommers 2024-11-25 00:29:09 -06:00
parent 6e1e91936f
commit 008ff5b7cf
20 changed files with 1617 additions and 31 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
.env
.env
config.toml

507
Cargo.lock generated
View File

@ -88,6 +88,12 @@ dependencies = [
"num-traits",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.4.0"
@ -229,11 +235,34 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.6",
]
[[package]]
name = "chrono-tz"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6"
dependencies = [
"chrono",
"chrono-tz-build",
"phf",
]
[[package]]
name = "chrono-tz-build"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7"
dependencies = [
"parse-zoneinfo",
"phf_codegen",
]
[[package]]
name = "command_attr"
version = "0.5.3"
@ -563,6 +592,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@ -732,6 +776,25 @@ dependencies = [
"tracing",
]
[[package]]
name = "h2"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http 1.1.0",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
@ -835,6 +898,29 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.1.0",
]
[[package]]
name = "http-body-util"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.9.5"
@ -857,9 +943,9 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"h2",
"h2 0.3.26",
"http 0.2.12",
"http-body",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
@ -871,6 +957,26 @@ dependencies = [
"want",
]
[[package]]
name = "hyper"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.7",
"http 1.1.0",
"http-body 1.0.1",
"httparse",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
@ -879,12 +985,64 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper",
"hyper 0.14.31",
"rustls 0.21.12",
"tokio",
"tokio-rustls 0.24.1",
]
[[package]]
name = "hyper-rustls"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http 1.1.0",
"hyper 1.5.1",
"hyper-util",
"rustls 0.23.18",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.0",
"tower-service",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper 1.5.1",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"hyper 1.5.1",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
@ -1224,6 +1382,23 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "native-tls"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -1312,6 +1487,50 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "openssl"
version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.89",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -1347,6 +1566,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "parse-zoneinfo"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
dependencies = [
"regex",
]
[[package]]
name = "paste"
version = "1.0.15"
@ -1368,6 +1596,44 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "phf"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.15"
@ -1559,11 +1825,11 @@ dependencies = [
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"h2 0.3.26",
"http 0.2.12",
"http-body",
"hyper",
"hyper-rustls",
"http-body 0.4.6",
"hyper 0.14.31",
"hyper-rustls 0.24.2",
"ipnet",
"js-sys",
"log",
@ -1577,8 +1843,8 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"sync_wrapper 0.1.2",
"system-configuration 0.5.1",
"tokio",
"tokio-rustls 0.24.1",
"tokio-util",
@ -1592,6 +1858,49 @@ dependencies = [
"winreg",
]
[[package]]
name = "reqwest"
version = "0.12.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f"
dependencies = [
"base64 0.22.1",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.4.7",
"http 1.1.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.1",
"hyper-rustls 0.27.3",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile 2.2.0",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 1.0.2",
"system-configuration 0.6.1",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-registry",
]
[[package]]
name = "ring"
version = "0.17.8"
@ -1746,6 +2055,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -1772,6 +2090,29 @@ dependencies = [
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.6.0",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "1.0.23"
@ -1822,6 +2163,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -1855,7 +2205,7 @@ dependencies = [
"mime_guess",
"parking_lot",
"percent-encoding",
"reqwest",
"reqwest 0.11.27",
"secrecy",
"serde",
"serde_cow",
@ -1931,16 +2281,27 @@ dependencies = [
name = "sillycord-bot"
version = "0.1.0"
dependencies = [
"chrono",
"chrono-tz",
"dotenv",
"poise",
"reqwest 0.12.9",
"serde",
"serde_json",
"serenity",
"sqlx",
"tokio",
"toml",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "skeptic"
version = "0.13.7"
@ -2273,6 +2634,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
version = "0.13.1"
@ -2292,7 +2662,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys",
"system-configuration-sys 0.5.0",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.6.0",
"core-foundation",
"system-configuration-sys 0.6.0",
]
[[package]]
@ -2305,6 +2686,16 @@ dependencies = [
"libc",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tagptr"
version = "0.2.0"
@ -2439,6 +2830,16 @@ dependencies = [
"syn 2.0.89",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
@ -2460,6 +2861,17 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.18",
"rustls-pki-types",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.16"
@ -2500,6 +2912,40 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tower-service"
version = "0.3.3"
@ -2923,6 +3369,36 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@ -3071,6 +3547,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.50.0"

View File

@ -11,4 +11,9 @@ sqlx = { version = "0.8", features = [ "runtime-tokio", "tls-rustls-ring", "mysq
tokio = { version = "1", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
serde = { version = "1.0.215", features = ["derive"] }
serde = { version = "1.0.215", features = ["derive"] }
toml = "0.8.19"
chrono = "0.4.38"
chrono-tz = "0.10.0"
reqwest = "0.12.9"
serde_json = "1.0.133"

View File

@ -0,0 +1,9 @@
-- Create basic users table for fun stuff around the bot
CREATE TABLE users (
id SERIAL PRIMARY KEY,
discord_id VARCHAR(255) NOT NULL UNIQUE,
actions_allowed TINYINT DEFAULT 1,
about TEXT,
pronouns VARCHAR(255)
);

159
src/commands/action.rs Normal file
View File

@ -0,0 +1,159 @@
use crate::{utils, Context, Error};
use poise::CreateReply;
use serenity::all::{CreateAllowedMentions, CreateAttachment, Message};
use serenity::builder::CreateMessage;
use serenity::model::prelude::UserId;
use serenity::prelude::Mentionable;
async fn check_if_allowed(ctx: Context<'_>, message: Message) -> Result<bool, Error> {
let user = ctx
.data()
.database_controller
.get_user_by_discord_id(message.author.id.into())
.await?;
if user.is_none() {
ctx.data()
.database_controller
.create_user(message.author.id.into())
.await?;
}
let user = ctx
.data()
.database_controller
.get_user_by_discord_id(message.author.id.into())
.await?;
Ok(user.unwrap().actions_allowed)
}
#[poise::command(context_menu_command = "Hug User")]
pub async fn use_action_hug(
ctx: Context<'_>,
#[description = "The target message to use the action with"] message: Message,
) -> Result<(), Error> {
ctx.defer().await?;
let allowed = check_if_allowed(ctx, message.clone()).await?;
if !allowed {
ctx.send(CreateReply::default().content(
":x: Sorry, either that user has disabled actions or has no profile to check against",
).ephemeral(true))
.await?;
return Ok(());
}
let action_img = utils::get_random_action_image("hug".to_string()).await;
// Match the result of the action image
match action_img {
Ok(img) => {
let user = UserId::new(ctx.author().id.into());
let target = UserId::new(message.clone().author.id.into());
let builder = CreateReply::default()
.content(format!("{} hugs {}", user.mention(), target.mention()))
.attachment(CreateAttachment::url(ctx.http(), &img).await?)
.allowed_mentions(CreateAllowedMentions::default().users(vec![user, target]));
ctx.send(builder).await?;
}
Err(_) => {
ctx.send(
CreateReply::default()
.content(":x: Something went wrong while fetching the action image")
.ephemeral(true),
)
.await?;
}
}
Ok(())
}
#[poise::command(context_menu_command = "Kiss User")]
pub async fn use_action_kiss(
ctx: Context<'_>,
#[description = "The target message to use the action with"] message: Message,
) -> Result<(), Error> {
ctx.defer().await?;
let allowed = check_if_allowed(ctx, message.clone()).await?;
if !allowed {
ctx.send(CreateReply::default().content(
":x: Sorry, either that user has disabled actions or has no profile to check against",
).ephemeral(true))
.await?;
return Ok(());
}
let action_img = utils::get_random_action_image("kiss".to_string()).await;
// Match the result of the action image
match action_img {
Ok(img) => {
let user = UserId::new(ctx.author().id.into());
let target = UserId::new(message.author.id.into());
let builder = CreateReply::default()
.content(format!("{} kisses {}", user.mention(), target.mention()))
.attachment(CreateAttachment::url(ctx.http(), &img).await?)
.allowed_mentions(CreateAllowedMentions::default().users(vec![user, target]));
ctx.send(builder).await?;
}
Err(_) => {
ctx.send(
CreateReply::default()
.content(":x: Something went wrong while fetching the action image")
.ephemeral(true),
)
.await?;
}
}
Ok(())
}
#[poise::command(context_menu_command = "Pat User")]
pub async fn use_action_pat(
ctx: Context<'_>,
#[description = "The target message to use the action with"] message: Message,
) -> Result<(), Error> {
ctx.defer().await?;
let allowed = check_if_allowed(ctx, message.clone()).await?;
if !allowed {
ctx.send(CreateReply::default().content(
":x: Sorry, either that user has disabled actions or has no profile to check against",
).ephemeral(true))
.await?;
return Ok(());
}
let action_img = utils::get_random_action_image("pat".to_string()).await;
// Match the result of the action image
match action_img {
Ok(img) => {
let user = UserId::new(ctx.author().id.into());
let target = UserId::new(message.author.id.into());
let builder = CreateReply::default()
.content(format!("{} pats {}", user.mention(), target.mention()))
.attachment(CreateAttachment::url(ctx.http(), &img).await?)
.allowed_mentions(CreateAllowedMentions::default().users(vec![user, target]));
ctx.send(builder).await?;
}
Err(_) => {
ctx.send(
CreateReply::default()
.content(":x: Something went wrong while fetching the action image")
.ephemeral(true),
)
.await?;
}
}
Ok(())
}

34
src/commands/cta.rs Normal file
View File

@ -0,0 +1,34 @@
use poise::CreateReply;
use serenity::{all::CreateEmbed, model::colour};
use reqwest::Client;
use serde_json::Value;
use crate::{Context, Error};
/// Returns a random cat image, along with a random cat fact.
#[poise::command(slash_command)]
pub async fn cta(ctx: Context<'_>) -> Result<(), Error> {
let cat_fact_res = Client::new()
.get("https://catfact.ninja/fact")
.send()
.await?;
let cat_fact: Value = serde_json::from_str(&cat_fact_res.text().await?)?;
let cat_fact = cat_fact["fact"].as_str().unwrap();
let cat_image_res = Client::new()
.get("https://api.thecatapi.com/v1/images/search")
.send()
.await?;
let cat_image: Value = serde_json::from_str(&cat_image_res.text().await?)?;
let cat_image = cat_image[0]["url"].as_str().unwrap();
let embed = CreateEmbed::default()
.title("Random Cat")
.description(cat_fact)
.image(cat_image)
.color(colour::Colour::from_rgb(0, 255, 255));
ctx.send(CreateReply::default().embed(embed)).await?;
Ok(())
}

25
src/commands/dog.rs Normal file
View File

@ -0,0 +1,25 @@
use crate::{Context, Error};
use poise::CreateReply;
use reqwest::Client;
use serde_json::Value;
use serenity::{all::CreateEmbed, model::colour};
/// Returns a random dog image.
#[poise::command(slash_command)]
pub async fn dog(ctx: Context<'_>) -> Result<(), Error> {
let dog_res = Client::new()
.get("https://dog.ceo/api/breeds/image/random")
.send()
.await?;
let dog: Value = serde_json::from_str(&dog_res.text().await?)?;
let dog = dog["message"].as_str().unwrap();
let embed = CreateEmbed::default()
.title("Random Dog")
.image(dog)
.color(colour::Colour::from_rgb(0, 255, 255));
ctx.send(CreateReply::default().embed(embed)).await?;
Ok(())
}

View File

@ -1 +1,6 @@
pub mod ping;
pub mod vouch;
pub mod cta;
pub mod dog;
pub mod profile;
pub mod action;

View File

@ -1,16 +1,32 @@
use poise::CreateReply;
use serenity::{all::CreateEmbed, model::colour};
use crate::{Context, Error};
/// Pong? Returns the ping of the bot and process uptime
#[poise::command(slash_command)]
pub async fn ping(ctx: Context<'_>) -> Result<(), Error> {
let start = std::time::Instant::now();
let msg = ctx.say("Pong?").await?;
let latency = start.elapsed();
msg.edit(
ctx,
poise::CreateReply::default().content(format!("Pong! Latency: {:?}", latency)),
)
.await?;
// Create pining embed
let mut embed = CreateEmbed::default()
.title("Ping?")
.description("Pinging...")
.color(colour::Colour::PURPLE);
let msg = ctx.send(CreateReply::default().embed(embed)).await?;
let ping = start.elapsed().as_millis();
embed = CreateEmbed::default()
.title("Pong!")
.description(format!(
"Pong! Took {}ms!\nProcess uptime: {:?}",
ping,
// Round to 2 decimal places
(ctx.data().uptime.elapsed().as_secs_f64() * 100.0).floor() / 100.0
))
.color(colour::Colour::DARK_GREEN);
msg.edit(ctx, CreateReply::default().embed(embed)).await?; // Edit the message with the ping
Ok(())
}

142
src/commands/profile.rs Normal file
View File

@ -0,0 +1,142 @@
use crate::structs::user::User as UserStruct;
use crate::{Context, Error};
use poise::CreateReply;
use serenity::all::{Colour, CreateEmbed, User};
/// Commands related to profiles in the bot
#[poise::command(
slash_command,
subcommands("view", "edit"),
check = "ensure_profile_is_setup"
)]
pub async fn profiles(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// Ensure the user has a profile setup
/// If the user doesn't have a profile, create one
async fn ensure_profile_is_setup(ctx: Context<'_>) -> Result<bool, Error> {
let profile = ctx
.data()
.database_controller
.get_user_by_discord_id(ctx.author().id.into())
.await?;
if profile.is_none() {
ctx.data()
.database_controller
.create_user(ctx.author().id.into())
.await?;
}
Ok(true)
}
/// View a user's profile
#[poise::command(slash_command)]
pub async fn view(
_ctx: Context<'_>,
#[description = "The user to view the profile of, defaults to yourself"] user: Option<User>,
) -> Result<(), Error> {
// Get the profile of the provided user, or the user who ran the command
let target_user = user.unwrap_or_else(|| _ctx.author().clone());
// Get the profile of the target user
let profile = _ctx
.data()
.database_controller
.get_user_by_discord_id(target_user.id.into())
.await?;
match profile {
Some(profile) => {
let profile_embed = CreateEmbed::default()
.title(format!("Profile of {}", target_user.tag()))
.description(format!(
"About: {}\nPronouns: {}\nActions Allowed: {}",
profile.about.unwrap_or("No about section".to_string()),
profile.pronouns.unwrap_or("No pronouns set".to_string()),
if profile.actions_allowed { "Yes" } else { "No" }
))
.color(Colour::FABLED_PINK);
_ctx.send(CreateReply::default().embed(profile_embed))
.await?;
}
None => {
// Send a emphul messagrespond(formate if the user doesn't have a profile
_ctx.send(
CreateReply::default()
.content(":x: User has no profile")
.ephemeral(true),
)
.await?;
}
}
Ok(())
}
/// Edit your profile
#[poise::command(slash_command)]
pub async fn edit(
_ctx: Context<'_>,
#[description = "Your about section"] about: Option<String>,
#[description = "Your pronouns"] pronouns: Option<String>,
#[description = "Whether you want to allow actions to be performed on you"]
actions_allowed: Option<bool>,
) -> Result<(), Error> {
// If no options are provided, send a help message
if about.is_none() && pronouns.is_none() && actions_allowed.is_none() {
_ctx.send(
CreateReply::default()
.content("Please provide at least one option to edit")
.ephemeral(true),
)
.await?;
return Ok(());
}
// Get the profile of the user who ran the command
let profile = _ctx
.data()
.database_controller
.get_user_by_discord_id(_ctx.author().id.into())
.await?;
// If the user doesn't have a profile, create one
let profile = match profile {
Some(profile) => profile,
None => {
_ctx.data()
.database_controller
.create_user(_ctx.author().id.into())
.await?
}
};
// Generate a user struct with the updated values
let updated_profile = UserStruct {
id: profile.id,
discord_id: profile.discord_id,
about,
pronouns,
actions_allowed: actions_allowed.unwrap_or(profile.actions_allowed),
};
// Update the user's profile
_ctx.data()
.database_controller
.update_user(updated_profile)
.await?;
// Send a success message
_ctx.send(
CreateReply::default()
.content(":white_check_mark: Profile updated successfully!")
.ephemeral(true),
)
.await?;
Ok(())
}

303
src/commands/vouch.rs Normal file
View File

@ -0,0 +1,303 @@
use crate::{structs::vouch::Vouch, Context, Error};
use serenity::all::{
Colour, CreateAllowedMentions, CreateEmbed, CreateEmbedFooter, CreateMessage, Mentionable, User,
};
/// Commands related to vouching for new users
#[poise::command(slash_command, subcommands("submit", "approve", "deny"))]
pub async fn vouch(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// Vouch for a new user
#[poise::command(slash_command, guild_only)]
pub async fn submit(
ctx: Context<'_>,
#[description = "The user to vouch for"] user: User,
) -> Result<(), Error> {
// Defer emphemeral response
ctx.defer_ephemeral().await?;
// Do we have a vouch for this user already?
if ctx
.data()
.vouch_store
.lock()
.await
.iter()
.any(|vouch| vouch.user.id == user.id)
{
ctx.say(":x: This user already has a vouch pending!")
.await?;
return Ok(());
}
let guild_id = ctx.guild_id().ok_or("Must be run in a guild")?;
let member = guild_id.member(ctx.serenity_context(), user.id).await?;
// Does the user have a silly role?
if member
.roles
.iter()
.any(|role_id| *role_id == ctx.data().config.roles.silly_role)
{
ctx.say(":x: This user already is a vouched member!")
.await?;
return Ok(());
}
// Create a new vouch
let vouch = Vouch::new(user.clone(), ctx.author().clone());
// Add the vouch to the store, we need to create a mutable reference to .data().vouch_store,
// because we want to modify the store - and poise context is immutable by default
ctx.data().vouch_store.lock().await.push(vouch);
// Send a messasge to the mod-logs channel with a ping that a new vouch has been submitted
let log_msg = format!(
"{} {}\n:notepad_spiral: A new vouch has been submitted for {} by {}, please either approve or deny this vouch.",
serenity::model::id::RoleId::new(ctx.data().config.roles.mod_role).mention(),
serenity::model::id::RoleId::new(ctx.data().config.roles.admin).mention(),
user.clone().mention(),
ctx.author().mention()
);
let log_channel_id = ctx.data().config.channels.logs_mod;
let channel_hash = guild_id.channels(ctx.serenity_context()).await?;
let channel = channel_hash.get(&log_channel_id.into()).unwrap();
channel
.send_message(
ctx.serenity_context(),
CreateMessage::new().content(log_msg).allowed_mentions(
CreateAllowedMentions::new().roles(vec![
ctx.data().config.roles.mod_role,
ctx.data().config.roles.admin,
]),
),
)
.await?;
ctx.say(":white_check_mark: Vouch submitted! An admin will review the vouch when able.")
.await?;
Ok(())
}
/// Approve a vouch for a user
#[poise::command(slash_command, guild_only)]
pub async fn approve(
ctx: Context<'_>,
#[description = "The user to approve the vouch for"] user: User,
) -> Result<(), Error> {
// Defer emphemeral response
ctx.defer_ephemeral().await?;
// Grab user
let author_user = ctx.author_member().await.clone();
match author_user {
Some(author_user) => {
// Check if the author is an admin
if !author_user.roles.iter().any(|role_id| {
*role_id == ctx.data().config.roles.admin
|| *role_id == ctx.data().config.roles.mod_role
}) {
ctx.say(":x: You must be an admin to approve vouches!")
.await?;
return Ok(());
}
}
None => {
ctx.say(":x: You must be an admin to approve vouches!")
.await?;
return Ok(());
}
}
// Do we have a vouch for this user?
let mut vouch_store = ctx.data().vouch_store.lock().await;
// Find the vouch for the user and its index
if let Some((vouch_index, vouch)) = vouch_store
.iter()
.enumerate()
.find(|(_, v)| v.user.id == user.id)
.map(|(index, vouch)| (index, vouch.clone()))
{
// Add the silly role to the user
let guild_id = ctx.guild_id().ok_or("Must be run in a guild")?;
let member = guild_id.member(ctx.serenity_context(), user.id).await?;
member
.add_role(ctx.serenity_context(), ctx.data().config.roles.silly_role)
.await?;
// Remove the vouch from the store
vouch_store.remove(vouch_index);
// Send a message to the logs channel (mod)
let log_msg = format!(
":white_check_mark: Vouch approved for {} by {} at {} CDT, vouched by {}",
user.mention(),
ctx.author().mention(),
vouch.get_vouch_time(),
vouch.vouched_by.mention()
);
let public_msg = CreateEmbed::default()
.title(format!("Welcome to sillycord, {}!", user.tag()))
.description("Enjoy your stay in our silly little community!")
.footer(CreateEmbedFooter::new(format!(
"Vouched by {} - Approved by {}",
vouch.vouched_by.tag(),
ctx.author().tag()
)))
.color(Colour::DARK_PURPLE);
let log_channel_id = ctx.data().config.channels.logs_mod;
let public_channel_id = ctx.data().config.channels.main;
let channels = guild_id.channels(ctx.serenity_context()).await?;
let channel = channels.get(&log_channel_id.into()).unwrap();
let public_channel = channels.get(&public_channel_id.into()).unwrap();
channel
.send_message(
ctx.serenity_context(),
CreateMessage::new().content(log_msg),
)
.await?;
public_channel
.send_message(
ctx.serenity_context(),
CreateMessage::new()
.embed(public_msg)
.content(user.mention().to_string())
.allowed_mentions(CreateAllowedMentions::new().users(vec![user.id])),
)
.await?;
ctx.data()
.database_controller
.create_user(user.id.into())
.await?;
ctx.say(":white_check_mark: Vouch approved!").await?;
} else {
ctx.say(":x: No vouch found for this user!").await?;
}
drop(vouch_store);
Ok(())
}
/// Deny a vouch for a user
#[poise::command(slash_command, guild_only)]
pub async fn deny(
ctx: Context<'_>,
#[description = "The user to deny the vouch for"] user: User,
#[flag] kick: bool,
#[description = "The reason for denying the vouch"] _reason: Option<String>,
) -> Result<(), Error> {
// Defer emphemeral response
ctx.defer_ephemeral().await?;
// Grab user
let author_user = ctx.author_member().await.clone();
match author_user {
Some(author_user) => {
// Check if the author is an admin
if !author_user.roles.iter().any(|role_id| {
*role_id == ctx.data().config.roles.admin
|| *role_id == ctx.data().config.roles.mod_role
}) {
ctx.say(":x: You must be an admin to deny vouches!").await?;
return Ok(());
}
}
None => {
ctx.say(":x: You must be an admin to deny vouches!").await?;
return Ok(());
}
}
// Do we have a vouch for this user?
// Do we have a vouch for this user?
let mut vouch_store = ctx.data().vouch_store.lock().await;
// Find the vouch for the user and its index
if let Some((vouch_index, vouch)) = vouch_store
.iter()
.enumerate()
.find(|(_, v)| v.user.id == user.id)
.map(|(index, vouch)| (index, vouch.clone()))
{
// Remove the vouch from the store
vouch_store.remove(vouch_index);
// Send a message to the logs channel (mod)
let log_msg = format!(
":x: Vouch denied for {} by {} at {} CDT, vouched by {} with a reason of '{}'",
user.mention(),
ctx.author().mention(),
vouch.get_vouch_time(),
vouch.vouched_by.mention(),
_reason.clone().unwrap_or("No reason provided".to_string())
);
let log_channel_id = ctx.data().config.channels.logs_mod;
let guild = ctx.guild_id().unwrap().clone();
let channels = guild.channels(ctx.serenity_context()).await?;
let channel = channels.get(&log_channel_id.into()).unwrap();
channel
.send_message(
ctx.serenity_context(),
CreateMessage::new().content(log_msg),
)
.await?;
// Then kick the user
let member = guild.member(ctx.serenity_context(), user.id).await?;
// Attempt to notify then kick the user
let dm_msg = format!(
":warning: Your vouch has been denied by {} with a reason of '{}'. If you have any questions, feel free to ask a moderator or admin.",
ctx.author().mention(),
_reason.unwrap_or("No reason provided".to_string())
);
let notify_result = vouch
.user
.direct_message(ctx.serenity_context(), CreateMessage::new().content(dm_msg))
.await;
match notify_result {
Ok(_) => {
if kick {
member.kick(ctx.serenity_context()).await?;
ctx.say(":white_check_mark: Vouch denied and user kicked! User notified.")
.await?;
} else {
ctx.say(":white_check_mark: Vouch denied! User notified.")
.await?;
}
}
Err(_) => {
if kick {
member.kick(ctx.serenity_context()).await?;
ctx.say(":white_check_mark: Vouch denied and user kicked! User not notified.")
.await?;
} else {
ctx.say(":white_check_mark: Vouch denied! User not notified.")
.await?;
}
}
}
} else {
ctx.say(":x: No vouch found for this user!").await?;
}
drop(vouch_store);
Ok(())
}

View File

@ -1,13 +1,16 @@
use crate::{Data, Error};
use poise::serenity_prelude::{self as serenity, ActivityData, Interaction, OnlineStatus};
use tracing::{info, warn};
use crate::{handlers, utils::get_rustc_version, Data, Error};
use ::serenity::all::{
ChannelId, Colour, CreateEmbed, CreateEmbedFooter, CreateMessage, Mentionable,
};
use poise::serenity_prelude::{self as serenity, ActivityData, OnlineStatus};
use tracing::{error, info};
// Create a span for every event
#[tracing::instrument(skip(ctx, event, framework, data))]
#[tracing::instrument(skip(ctx, event, _framework, data))]
pub async fn event_handler(
ctx: &serenity::Context,
event: &serenity::FullEvent,
framework: poise::FrameworkContext<'_, Data, Error>,
_framework: poise::FrameworkContext<'_, Data, Error>,
data: &Data,
) -> Result<(), Error> {
match event {
@ -17,6 +20,82 @@ pub async fn event_handler(
Some(ActivityData::watching("sillycord")),
OnlineStatus::Online,
);
let embed = CreateEmbed::default()
.title("Bot Ready!")
.description(format!(
"Bot is ready! Running on rust version {}, sillycord-bot version {}",
get_rustc_version().await,
env!("CARGO_PKG_VERSION")
))
.footer(CreateEmbedFooter::new(
"Bot created for sillycord. Made with ❤️ by sticks and others",
))
.color(Colour::DARK_GREEN);
let msg = CreateMessage::default().embed(embed);
let channel_id = ChannelId::new(data.config.channels.logs_public);
// We have to use http here to be future safe and not block the event loop
ctx.http.send_message(channel_id, vec![], &msg).await?;
}
serenity::FullEvent::GuildMemberAddition { new_member, .. } => {
// Is the new user in the main guild?
if new_member.guild_id != data.config.main_guild_id {
return Ok(());
}
info!("Handling new user {}", new_member.user.tag());
ctx.http
.send_message(
ChannelId::new(data.config.channels.logs_mod),
vec![],
&CreateMessage::default().content(format!(
"<:join:1310407968503894158> New user joined {} - created at: <t:{}:f>",
new_member.mention(),
new_member.user.created_at().timestamp_millis() / 1000
)),
)
.await?;
let handler_result =
handlers::join::join_handler(ctx.clone(), data, new_member.clone()).await;
if let Err(e) = handler_result {
error!(
"Error invoking join handler for new user {}: {:?}",
new_member.user.tag(),
e
);
} else {
info!(
"Join handler completed successfully for new user {}",
new_member.user.tag()
);
}
}
serenity::FullEvent::GuildMemberRemoval { user, .. } => {
// We can safely ignore the user's guild_id here, as they are leaving the guild -
// because the bot is only in one guild, we know the user is leaving the main guild
info!("Handling user leave {}", user.tag());
data.database_controller
.delete_user_by_discord_id(user.id.into())
.await?;
ctx.http
.send_message(
ChannelId::new(data.config.channels.logs_mod),
vec![],
&CreateMessage::default().content(format!(
"<:leave:1310407968503894158> User left {}",
user.mention()
)),
)
.await?;
}
serenity::FullEvent::ShardsReady { total_shards, .. } => {

102
src/handlers/db.rs Normal file
View File

@ -0,0 +1,102 @@
use crate::structs::user::User;
use sqlx::MySqlPool;
pub struct DatabaseController {
db: MySqlPool,
}
impl DatabaseController {
pub fn new(db: MySqlPool) -> Self {
Self { db }
}
pub async fn get_user_by_discord_id(
&self,
discord_id: u64,
) -> Result<Option<User>, sqlx::Error> {
let user = sqlx::query!("SELECT * FROM users WHERE discord_id = ?", discord_id)
.fetch_optional(&self.db)
.await?;
match user {
Some(user) => Ok(Some(User {
id: user.id,
discord_id: user
.discord_id
.parse::<u64>()
.map_err(|_| sqlx::Error::Decode("Failed to parse discord_id".into()))?,
actions_allowed: user.actions_allowed == Some(1),
about: user.about,
pronouns: user.pronouns,
})),
None => Ok(None),
}
}
async fn get_user_by_id(&self, id: u64) -> Result<Option<User>, sqlx::Error> {
let user = sqlx::query!("SELECT * FROM users WHERE id = ?", id)
.fetch_optional(&self.db)
.await?;
match user {
Some(user) => Ok(Some(User {
id: user.id,
discord_id: user
.discord_id
.parse::<u64>()
.map_err(|_| sqlx::Error::Decode("Failed to parse discord_id".into()))?,
actions_allowed: user.actions_allowed == Some(1),
about: user.about,
pronouns: user.pronouns,
})),
None => Ok(None),
}
}
pub async fn create_user(&self, discord_id: u64) -> Result<User, sqlx::Error> {
let user = sqlx::query!(
"INSERT INTO users (discord_id) VALUES (?)",
discord_id.to_string()
)
.execute(&self.db)
.await?;
Ok(User {
id: user.last_insert_id(),
discord_id,
actions_allowed: true,
about: None,
pronouns: None,
})
}
pub async fn update_user(&self, user: User) -> Result<(), sqlx::Error> {
sqlx::query!(
"UPDATE users SET actions_allowed = ?, about = ?, pronouns = ? WHERE id = ?",
user.actions_allowed as i8,
user.about,
user.pronouns,
user.id
)
.execute(&self.db)
.await?;
Ok(())
}
pub async fn delete_user(&self, user: User) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM users WHERE id = ?", user.id)
.execute(&self.db)
.await?;
Ok(())
}
pub async fn delete_user_by_discord_id(&self, discord_id: u64) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM users WHERE discord_id = ?", discord_id)
.execute(&self.db)
.await?;
Ok(())
}
}

73
src/handlers/join.rs Normal file
View File

@ -0,0 +1,73 @@
use serenity::all::{
ChannelId, Colour, Context, CreateAllowedMentions, CreateEmbed, CreateEmbedFooter,
CreateMessage, Member, Mentionable,
};
use std::error::Error;
use tracing::{error, warn};
#[tracing::instrument(skip(ctx, new_member, data))]
pub async fn join_handler(
ctx: Context,
data: &crate::Data,
new_member: Member,
) -> Result<(), Box<dyn Error>> {
let welcome_msg = CreateEmbed::default()
.title("Welcome to sillycord!")
.description(format!(
"Welcome! To keep sillycord a safe and fun place, we require all newly invited members to be vouched by a current member. Please wait for a member of the community to vouch for you. If you do not receive a vouch within 24 hours, you will be removed from the server. If you have any questions, feel free to ask a moderator or admin.",
))
.color(Colour::PURPLE)
.footer(CreateEmbedFooter::new(
"I am a bot, and this action was performed automatically. If you have any questions or concerns, please contact a moderator or admin.",
));
// Try to send the welcome message to the new member
let dm_result = new_member
.user
.direct_message(&ctx, CreateMessage::default().embed(welcome_msg.clone()))
.await;
if let Err(why) = dm_result {
warn!(
"Failed to send welcome message to {}: {:?} - defaulting to public channel",
new_member.user.tag(),
why
);
let new_welcome = welcome_msg.clone().footer(CreateEmbedFooter::new(
"We were unable to send you a direct message. We've posted the welcome message here instead - please make sure to read it!",
));
let send_result = ctx
.http
.send_message(
ChannelId::new(data.config.channels.welcome),
vec![],
&CreateMessage::default()
.embed(new_welcome)
.content(format!(
"{} Please make sure to read the welcome message below!",
new_member.mention()
))
.allowed_mentions(CreateAllowedMentions::new().users(vec![new_member.user.id])),
)
.await;
if let Err(why) = send_result {
error!(
"Failed to send welcome message to {} in public channel: {:?}",
new_member.user.tag(),
why
);
error!(
"All options to send welcome message to {} failed - aborting",
new_member.user.tag()
);
return Err(why.into());
}
}
Ok(())
}

View File

@ -0,0 +1,2 @@
pub mod join;
pub mod db;

View File

@ -1,10 +1,19 @@
mod commands;
mod events;
mod handlers;
mod structs;
mod utils;
use events::event_handler;
use serenity::all::{ClientBuilder, GatewayIntents};
use handlers::db::DatabaseController;
use serde::{Deserialize, Serialize};
use serenity::{
all::{ClientBuilder, GatewayIntents},
futures::lock::Mutex,
};
use sqlx::mysql::MySqlPoolOptions;
use sqlx::MySqlPool;
use structs::vouch::Vouch;
use tracing::{event, info, info_span, Level};
fn check_required_env_vars() {
@ -68,11 +77,37 @@ async fn init_sqlx() -> MySqlPool {
}
struct Data {
sqlx_pool: MySqlPool,
database_controller: DatabaseController,
uptime: std::time::Instant,
config: Config,
vouch_store: Mutex<Vec<Vouch>>,
} // User data, which is stored and accessible in all command invocations
type Error = Box<dyn std::error::Error + Send + Sync>;
pub type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
type Context<'a> = poise::Context<'a, Data, Error>;
#[derive(Deserialize, Serialize)]
struct Config {
main_guild_id: u64,
channels: Channels,
roles: Roles,
}
#[derive(Deserialize, Serialize)]
struct Channels {
welcome: u64,
main: u64,
logs_public: u64,
logs_mod: u64,
}
#[derive(Deserialize, Serialize)]
struct Roles {
admin: u64,
mod_role: u64,
silly_role: u64,
}
#[tokio::main]
async fn main() {
let start_time = std::time::Instant::now();
@ -88,15 +123,57 @@ async fn main() {
info!("checking required environment variables");
check_required_env_vars();
info!("loading config");
// Do we have a config.toml file? If we do, load it
// If we don't, create it with the default values, then exit the program and tell the user to fill it out
let config: Config = match std::fs::read_to_string("config.toml") {
Ok(config) => toml::from_str(&config).expect("failed to parse config.toml"),
Err(_) => {
let default_config = Config {
main_guild_id: 0,
channels: Channels {
welcome: 0,
main: 0,
logs_public: 0,
logs_mod: 0,
},
roles: Roles {
admin: 0,
mod_role: 0,
silly_role: 0,
},
};
let default_config_toml = toml::to_string_pretty(&default_config).unwrap();
std::fs::write("config.toml", default_config_toml)
.expect("failed to write config.toml");
event!(Level::WARN, "config.toml not found, created a default one");
event!(
Level::ERROR,
"please fill out config.toml and restart the bot"
);
panic!("config.toml not found, created a default one, please fill it out and restart the bot");
}
};
info!("initializing SQLx");
let pool = init_sqlx().await;
info!("initializing bot");
let intents = GatewayIntents::non_privileged();
let intents = GatewayIntents::privileged();
let framework = poise::Framework::builder()
.options(poise::FrameworkOptions::<Data, Error> {
commands: vec![commands::ping::ping()],
commands: vec![
commands::ping::ping(),
commands::vouch::vouch(),
commands::profile::profiles(),
commands::dog::dog(),
commands::cta::cta(),
commands::action::use_action_hug(),
commands::action::use_action_kiss(),
commands::action::use_action_pat(),
],
event_handler: |ctx, event, framework, data| {
Box::pin(event_handler(ctx, event, framework, data))
},
@ -107,7 +184,10 @@ async fn main() {
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Data {
// Initialize user data here
sqlx_pool: pool,
database_controller: DatabaseController::new(pool.clone()),
uptime: std::time::Instant::now(),
config,
vouch_store: Mutex::new(Vec::new()),
})
})
})

2
src/structs/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod vouch;
pub mod user;

7
src/structs/user.rs Normal file
View File

@ -0,0 +1,7 @@
pub struct User {
pub id: u64,
pub discord_id: u64,
pub actions_allowed: bool,
pub about: Option<String>,
pub pronouns: Option<String>,
}

27
src/structs/vouch.rs Normal file
View File

@ -0,0 +1,27 @@
use chrono::{DateTime, Utc};
use chrono_tz::US::Central;
use serenity::all::User;
#[derive(Debug, Clone)]
pub struct Vouch {
pub user: User,
pub vouched_by: User,
pub vouch_time: DateTime<Utc>,
}
impl Vouch {
pub fn new(user: User, vouched_by: User) -> Self {
Self {
user,
vouched_by,
vouch_time: Utc::now(),
}
}
pub fn get_vouch_time(&self) -> String {
self.vouch_time
.with_timezone(&Central)
.format("%Y-%m-%d %H:%M:%S")
.to_string()
}
}

30
src/utils.rs Normal file
View File

@ -0,0 +1,30 @@
use std::process::Command;
pub async fn get_rustc_version() -> String {
let rustc_version = Command::new("rustc")
.arg("--version")
.output()
.expect("failed to get rustc version");
String::from_utf8(rustc_version.stdout).expect("failed to convert rustc version to string")
}
pub async fn get_random_action_image(action: String) -> Result<String, reqwest::Error> {
let action = action.to_lowercase();
let action = action.replace(" ", "_");
let url = format!("https://api.otakugifs.xyz/gif?reaction={}", action);
let response = reqwest::get(&url)
.await
.expect("failed to get random action image");
let response_json: serde_json::Value =
serde_json::from_str(&response.text().await.expect("failed to get response text"))
.expect("failed to parse response json");
let image_url = response_json["url"]
.as_str()
.expect("failed to get image url from response json");
Ok(image_url.to_string())
}