From 008ff5b7cf4d05004d6a657ec7df049d3b81cc76 Mon Sep 17 00:00:00 2001
From: sticks <tanner@teamhydra.dev>
Date: Mon, 25 Nov 2024 00:29:09 -0600
Subject: [PATCH] v1.0

---
 .gitignore                         |   3 +-
 Cargo.lock                         | 507 ++++++++++++++++++++++++++++-
 Cargo.toml                         |   7 +-
 migrations/20241125045124_init.sql |   9 +
 src/commands/action.rs             | 159 +++++++++
 src/commands/cta.rs                |  34 ++
 src/commands/dog.rs                |  25 ++
 src/commands/mod.rs                |   5 +
 src/commands/ping.rs               |  30 +-
 src/commands/profile.rs            | 142 ++++++++
 src/commands/vouch.rs              | 303 +++++++++++++++++
 src/events.rs                      |  89 ++++-
 src/handlers/db.rs                 | 102 ++++++
 src/handlers/join.rs               |  73 +++++
 src/handlers/mod.rs                |   2 +
 src/main.rs                        |  92 +++++-
 src/structs/mod.rs                 |   2 +
 src/structs/user.rs                |   7 +
 src/structs/vouch.rs               |  27 ++
 src/utils.rs                       |  30 ++
 20 files changed, 1617 insertions(+), 31 deletions(-)
 create mode 100644 migrations/20241125045124_init.sql
 create mode 100644 src/commands/action.rs
 create mode 100644 src/commands/cta.rs
 create mode 100644 src/commands/dog.rs
 create mode 100644 src/commands/profile.rs
 create mode 100644 src/commands/vouch.rs
 create mode 100644 src/handlers/db.rs
 create mode 100644 src/handlers/join.rs
 create mode 100644 src/structs/mod.rs
 create mode 100644 src/structs/user.rs
 create mode 100644 src/structs/vouch.rs
 create mode 100644 src/utils.rs

diff --git a/.gitignore b/.gitignore
index 0b745e2..aafb6ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 /target
-.env
\ No newline at end of file
+.env
+config.toml
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 2e9fa91..242fda2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 9f150e9..67f2a77 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"] }
\ No newline at end of file
+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"
diff --git a/migrations/20241125045124_init.sql b/migrations/20241125045124_init.sql
new file mode 100644
index 0000000..dda48a0
--- /dev/null
+++ b/migrations/20241125045124_init.sql
@@ -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)
+);
+
diff --git a/src/commands/action.rs b/src/commands/action.rs
new file mode 100644
index 0000000..8989184
--- /dev/null
+++ b/src/commands/action.rs
@@ -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(())
+}
diff --git a/src/commands/cta.rs b/src/commands/cta.rs
new file mode 100644
index 0000000..a9f0101
--- /dev/null
+++ b/src/commands/cta.rs
@@ -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(())
+}
diff --git a/src/commands/dog.rs b/src/commands/dog.rs
new file mode 100644
index 0000000..1295e8c
--- /dev/null
+++ b/src/commands/dog.rs
@@ -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(())
+}
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index a766209..c5fafd5 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -1 +1,6 @@
 pub mod ping;
+pub mod vouch;
+pub mod cta;
+pub mod dog;
+pub mod profile;
+pub mod action;
\ No newline at end of file
diff --git a/src/commands/ping.rs b/src/commands/ping.rs
index 0774b67..7448b90 100644
--- a/src/commands/ping.rs
+++ b/src/commands/ping.rs
@@ -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(())
 }
diff --git a/src/commands/profile.rs b/src/commands/profile.rs
new file mode 100644
index 0000000..5bd3c50
--- /dev/null
+++ b/src/commands/profile.rs
@@ -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(())
+}
diff --git a/src/commands/vouch.rs b/src/commands/vouch.rs
new file mode 100644
index 0000000..b6e015c
--- /dev/null
+++ b/src/commands/vouch.rs
@@ -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(())
+}
diff --git a/src/events.rs b/src/events.rs
index 2beb3b8..dc5b004 100644
--- a/src/events.rs
+++ b/src/events.rs
@@ -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, .. } => {
diff --git a/src/handlers/db.rs b/src/handlers/db.rs
new file mode 100644
index 0000000..78381f9
--- /dev/null
+++ b/src/handlers/db.rs
@@ -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(())
+    }
+}
diff --git a/src/handlers/join.rs b/src/handlers/join.rs
new file mode 100644
index 0000000..6511241
--- /dev/null
+++ b/src/handlers/join.rs
@@ -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(())
+}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index e69de29..89da20b 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -0,0 +1,2 @@
+pub mod join;
+pub mod db;
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index b4f4c2e..1b67686 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -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()),
                 })
             })
         })
diff --git a/src/structs/mod.rs b/src/structs/mod.rs
new file mode 100644
index 0000000..fd43a72
--- /dev/null
+++ b/src/structs/mod.rs
@@ -0,0 +1,2 @@
+pub mod vouch;
+pub mod user;
\ No newline at end of file
diff --git a/src/structs/user.rs b/src/structs/user.rs
new file mode 100644
index 0000000..e74a420
--- /dev/null
+++ b/src/structs/user.rs
@@ -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>,
+}
diff --git a/src/structs/vouch.rs b/src/structs/vouch.rs
new file mode 100644
index 0000000..cec89e9
--- /dev/null
+++ b/src/structs/vouch.rs
@@ -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()
+    }
+}
diff --git a/src/utils.rs b/src/utils.rs
new file mode 100644
index 0000000..f55629f
--- /dev/null
+++ b/src/utils.rs
@@ -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())
+}