init
This commit is contained in:
commit
ebdc182e86
47 changed files with 8090 additions and 0 deletions
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
builds/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
# Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# IDE stuff
|
||||||
|
*/.idea
|
||||||
|
*/.dccache
|
||||||
|
|
||||||
|
# Added by cargo
|
||||||
|
/target
|
||||||
|
|
||||||
|
# My trace log files
|
||||||
|
*_rusted-fbt.verbose.log
|
||||||
|
*_rusted-fbt.info.log
|
3218
Cargo.lock
generated
Normal file
3218
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
74
Cargo.toml
Normal file
74
Cargo.toml
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
[[bin]]
|
||||||
|
name = "rusted-fbt"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "rusted_fbt_lib"
|
||||||
|
path = "src/lib/lib.rs"
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "rusted-fbt"
|
||||||
|
version = "2.7.2"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = [
|
||||||
|
"database",
|
||||||
|
] # Comment out this line when adding new imports and functions to see what needs a #[cfg(feature = "database")]
|
||||||
|
database = []
|
||||||
|
beta = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
poise = { version = "0.5.6", features = [
|
||||||
|
"cache",
|
||||||
|
"collector",
|
||||||
|
] }
|
||||||
|
tokio = { version = "1.32.0", features = ["full"] }
|
||||||
|
csv = "1.2.2"
|
||||||
|
serde = "1.0.188"
|
||||||
|
serde_json = "1.0.107"
|
||||||
|
uwuify = "0.2.2"
|
||||||
|
chrono = "0.4.31"
|
||||||
|
clap = { version = "4.0.32", features = ["derive"] }
|
||||||
|
maplit = "1.0.2"
|
||||||
|
colored = "2.0.4"
|
||||||
|
strip_markdown = "0.2.0"
|
||||||
|
reqwest = "0.11.20"
|
||||||
|
futures = "0.3.28"
|
||||||
|
redis = { version = "0.23.3", features = ["aio", "tokio-comp"] }
|
||||||
|
merge = { version = "0.1.0", features = ["std", "num", "derive"] }
|
||||||
|
derivative = "2.2.0"
|
||||||
|
rand = "0.8.5"
|
||||||
|
unix-time = "0.1.5"
|
||||||
|
regex = "1.9.5"
|
||||||
|
once_cell = "1.18.0"
|
||||||
|
chrono-tz = "0.8.3"
|
||||||
|
meilisearch-sdk = "0.17.0"
|
||||||
|
serde_with = "3.3.0"
|
||||||
|
tracing = { version = "0.1.37", features = ["async-await"] }
|
||||||
|
tracing-subscriber = { version = "0.3.17", features = [
|
||||||
|
"parking_lot",
|
||||||
|
"registry",
|
||||||
|
] }
|
||||||
|
linkify = "0.10.0"
|
||||||
|
anyhow = "1.0.75"
|
||||||
|
thiserror = "1.0.49"
|
||||||
|
pastemyst = "1.0.0"
|
||||||
|
# oxipng = "5.0.1"
|
||||||
|
# mozjpeg = "0.9.3"
|
||||||
|
# lz4_flex = "0.9.3"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
|
winres = "0.1.12"
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 1
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = "thin"
|
||||||
|
|
||||||
|
[profile.release-full]
|
||||||
|
inherits = "release"
|
||||||
|
lto = true
|
1
README.md
Normal file
1
README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# FBT Security - Rust
|
BIN
assets/DILLIGAF.ogg
Normal file
BIN
assets/DILLIGAF.ogg
Normal file
Binary file not shown.
BIN
assets/Discord_meme.mp4
Normal file
BIN
assets/Discord_meme.mp4
Normal file
Binary file not shown.
BIN
assets/FBT_Security_Unedited.ogg
Normal file
BIN
assets/FBT_Security_Unedited.ogg
Normal file
Binary file not shown.
BIN
assets/PomfPomf.ogg
Normal file
BIN
assets/PomfPomf.ogg
Normal file
Binary file not shown.
BIN
assets/amogus.gif
Normal file
BIN
assets/amogus.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 317 KiB |
BIN
assets/astro.ogg
Normal file
BIN
assets/astro.ogg
Normal file
Binary file not shown.
BIN
assets/egirls.ogg
Normal file
BIN
assets/egirls.ogg
Normal file
Binary file not shown.
BIN
assets/erp-tonight.mp3
Normal file
BIN
assets/erp-tonight.mp3
Normal file
Binary file not shown.
BIN
assets/femboys.mp3
Normal file
BIN
assets/femboys.mp3
Normal file
Binary file not shown.
BIN
assets/icon.ico
Normal file
BIN
assets/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
110
assets/languages.txt
Normal file
110
assets/languages.txt
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
Supported languages and their language codes (case-sensitive!):
|
||||||
|
(Download this file to see all supported languages)
|
||||||
|
|
||||||
|
Afrikaans : af
|
||||||
|
Albanian : sq
|
||||||
|
Amharic : am
|
||||||
|
Arabic : ar
|
||||||
|
Armenian : hy
|
||||||
|
Azerbaijani : az
|
||||||
|
Basque : eu
|
||||||
|
Belarusian : be
|
||||||
|
Bengali : bn
|
||||||
|
Bosnian : bs
|
||||||
|
Bulgarian : bg
|
||||||
|
Catalan : ca
|
||||||
|
Cebuano : ceb
|
||||||
|
Chichewa : ny
|
||||||
|
Chinese (simplified) : zh-cn
|
||||||
|
Chinese (traditional) : zh-tw
|
||||||
|
Corsican : co
|
||||||
|
Croatian : hr
|
||||||
|
Czech : cs
|
||||||
|
Danish : da
|
||||||
|
Dutch : nl
|
||||||
|
English : en
|
||||||
|
Esperanto : eo
|
||||||
|
Estonian : et
|
||||||
|
Filipino : tl
|
||||||
|
Finnish : fi
|
||||||
|
French : fr
|
||||||
|
Frisian : fy
|
||||||
|
Galician : gl
|
||||||
|
Georgian : ka
|
||||||
|
German : de
|
||||||
|
Greek : el
|
||||||
|
Gujarati : gu
|
||||||
|
Haitian creole : ht
|
||||||
|
Hausa : ha
|
||||||
|
Hawaiian : haw
|
||||||
|
Hebrew : iw
|
||||||
|
Hebrew : he
|
||||||
|
Hindi : hi
|
||||||
|
Hmong : hmn
|
||||||
|
Hungarian : hu
|
||||||
|
Icelandic : is
|
||||||
|
Igbo : ig
|
||||||
|
Indonesian : id
|
||||||
|
Irish : ga
|
||||||
|
Italian : it
|
||||||
|
Japanese : ja
|
||||||
|
Javanese : jw
|
||||||
|
Kannada : kn
|
||||||
|
Kazakh : kk
|
||||||
|
Khmer : km
|
||||||
|
Korean : ko
|
||||||
|
Kurdish (kurmanji) : ku
|
||||||
|
Kyrgyz : ky
|
||||||
|
Lao : lo
|
||||||
|
Latin : la
|
||||||
|
Latvian : lv
|
||||||
|
Lithuanian : lt
|
||||||
|
Luxembourgish : lb
|
||||||
|
Macedonian : mk
|
||||||
|
Malagasy : mg
|
||||||
|
Malay : ms
|
||||||
|
Malayalam : ml
|
||||||
|
Maltese : mt
|
||||||
|
Maori : mi
|
||||||
|
Marathi : mr
|
||||||
|
Mongolian : mn
|
||||||
|
Myanmar (burmese) : my
|
||||||
|
Nepali : ne
|
||||||
|
Norwegian : no
|
||||||
|
Odia : or
|
||||||
|
Pashto : ps
|
||||||
|
Persian : fa
|
||||||
|
Polish : pl
|
||||||
|
Portuguese : pt
|
||||||
|
Punjabi : pa
|
||||||
|
Romanian : ro
|
||||||
|
Russian : ru
|
||||||
|
Samoan : sm
|
||||||
|
Scots gaelic : gd
|
||||||
|
Serbian : sr
|
||||||
|
Sesotho : st
|
||||||
|
Shona : sn
|
||||||
|
Sindhi : sd
|
||||||
|
Sinhala : si
|
||||||
|
Slovak : sk
|
||||||
|
Slovenian : sl
|
||||||
|
Somali : so
|
||||||
|
Spanish : es
|
||||||
|
Sundanese : su
|
||||||
|
Swahili : sw
|
||||||
|
Swedish : sv
|
||||||
|
Tajik : tg
|
||||||
|
Tamil : ta
|
||||||
|
Telugu : te
|
||||||
|
Thai : th
|
||||||
|
Turkish : tr
|
||||||
|
Ukrainian : uk
|
||||||
|
Urdu : ur
|
||||||
|
Uyghur : ug
|
||||||
|
Uzbek : uz
|
||||||
|
Vietnamese : vi
|
||||||
|
Welsh : cy
|
||||||
|
Xhosa : xh
|
||||||
|
Yiddish : yi
|
||||||
|
Yoruba : yo
|
||||||
|
Zulu : zu
|
51
assets/lyrics.txt
Normal file
51
assets/lyrics.txt
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
Astro-naut
|
||||||
|
What you know about rollin' down in the deep?
|
||||||
|
When your brain goes numb, you can call that mental freeze
|
||||||
|
When these people talk too much, put that shit in slow motion, yeah
|
||||||
|
I feel like an astronaut in the ocean, ayy
|
||||||
|
What you know about rollin' down in the deep?
|
||||||
|
When your brain goes numb, you can call that mental freeze
|
||||||
|
When these people talk too much, put that shit in slow motion, yeah
|
||||||
|
I feel like an astronaut in the ocean
|
||||||
|
She say that I'm cool (damn straight)
|
||||||
|
I'm like "yeah, that's true" (that's true)
|
||||||
|
I believe in G-O-D (ayy)
|
||||||
|
Don't believe in T-H-O-T
|
||||||
|
She keep playing me dumb (play me)
|
||||||
|
I'ma play her for fun (uh-huh)
|
||||||
|
Y'all don't really know my mental
|
||||||
|
Lemme give you the picture like stencil
|
||||||
|
Falling out, in a drought
|
||||||
|
No flow, rain wasn't pouring down (pouring down)
|
||||||
|
See, that pain was all around
|
||||||
|
See, my mode was kinda lounged
|
||||||
|
Didn't know which-which way to turn
|
||||||
|
Flow was cool but I still felt burnt
|
||||||
|
Energy up, you can feel my surge
|
||||||
|
I'ma kill everything like this purge (ayy)
|
||||||
|
Let's just get this straight for a second, I'ma work
|
||||||
|
Even if I don't get paid for progression, I'ma get it (get it)
|
||||||
|
Everything that I do is electric
|
||||||
|
I'ma keep it in a motion, keep it moving like kinetic, ayy (yeah, yeah, yeah, yeah)
|
||||||
|
Put this shit in a frame, better know I don't blame
|
||||||
|
Everything that I say, man I seen you deflate
|
||||||
|
Let me elevate, this ain't a prank
|
||||||
|
Have you walkin' on a plank, la-la-la-la-la, like
|
||||||
|
Both hands together, God, let me pray (now let me pray)
|
||||||
|
Uh, I've been going right, right around, call that relay (Masked Wolf)
|
||||||
|
Pass the baton, back and I'm on
|
||||||
|
Swimming in the pool, Kendrick Lamar, uh
|
||||||
|
Want a piece of this, a piece of mine, my peace a sign
|
||||||
|
Can you please read between the lines?
|
||||||
|
My rhyme's inclined to break your spine
|
||||||
|
They say that I'm so fine
|
||||||
|
You could never match my grind
|
||||||
|
Please do not, not waste my time
|
||||||
|
What you know about rollin' down in the deep?
|
||||||
|
When your brain goes numb, you can call that mental freeze
|
||||||
|
When these people talk too much, put that shit in slow motion, yeah
|
||||||
|
I feel like an astronaut in the ocean, ayy
|
||||||
|
What you know about rollin' down in the deep?
|
||||||
|
When your brain goes numb, you can call that mental freeze
|
||||||
|
When these people talk too much, put that shit in slow motion, yeah
|
||||||
|
I feel like an astronaut in the ocean
|
BIN
assets/pog-frog.gif
Normal file
BIN
assets/pog-frog.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/toxic.gif
Normal file
BIN
assets/toxic.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 MiB |
BIN
assets/user.gif
Normal file
BIN
assets/user.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 368 KiB |
BIN
assets/white.mp4
Normal file
BIN
assets/white.mp4
Normal file
Binary file not shown.
BIN
assets/wrestle.mp4
Normal file
BIN
assets/wrestle.mp4
Normal file
Binary file not shown.
1
build.ps1
Normal file
1
build.ps1
Normal file
|
@ -0,0 +1 @@
|
||||||
|
$Env:RUSTFLAGS='-C target-cpu=native'; cargo build --release
|
21
build.rs
Normal file
21
build.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
use std::io;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use winres::WindowsResource;
|
||||||
|
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
|
// I'm just keeping this as the example had it, no need to warn me about this since it doesn't affect the actual code
|
||||||
|
/// This code will set the windows program icon to the fbt logo
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if the icon file cannot be found at compile time.
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
WindowsResource::new()
|
||||||
|
// This path can be absolute, or relative to your crate root.
|
||||||
|
.set_icon("assets/icon.ico")
|
||||||
|
.compile()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
4
clippy.sh
Normal file
4
clippy.sh
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cargo +nightly clippy --all-features -Z unstable-options "$@" -- -D clippy::correctness -W clippy::style -W clippy::complexity -W clippy::perf -W clippy::nursery -W clippy::pedantic -W clippy::cargo -A clippy::too_many_lines
|
||||||
|
# cargo +nightly clippy --all-features -Z unstable-options -- -D clippy::correctness -W clippy::style -W clippy::complexity -W clippy::perf -W clippy::nursery -W clippy::pedantic -W clippy::cargo -A clippy::too_many_lines
|
14
haswell.Dockerfile
Normal file
14
haswell.Dockerfile
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
FROM rust:latest as builder
|
||||||
|
WORKDIR /usr/src/rusted-fbt
|
||||||
|
COPY . .
|
||||||
|
RUN env RUSTFLAGS="-C target-cpu=haswell" cargo install --path .
|
||||||
|
|
||||||
|
FROM debian:buster-slim
|
||||||
|
# RUN apt-get update && apt-get install -y extra-runtime-dependencies && rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/rusted-fbt /usr/local/bin/rusted-fbt
|
||||||
|
run apt update -y
|
||||||
|
run apt install ca-certificates -y
|
||||||
|
run apt update -y
|
||||||
|
# This is the only dependancie missing as far as I can tell which is great
|
||||||
|
run apt install libssl-dev -y
|
||||||
|
CMD ["rusted-fbt"]
|
1
slow build.ps1
Normal file
1
slow build.ps1
Normal file
|
@ -0,0 +1 @@
|
||||||
|
$Env:RUSTFLAGS='-C target-cpu=native'; cargo build --profile=release-full-lto
|
224
src/commands/_deprecated.rs
Normal file
224
src/commands/_deprecated.rs
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
// https://sqlitebrowser.org/
|
||||||
|
/// Transfer from sqlite DB, get the json files using "File>Export>Table(s) to json" in sqlitebrowser
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[allow(non_snake_case, non_camel_case_types)] // Keeping these badly names variables since that's what they are called in the SQLite DB
|
||||||
|
#[poise::command(slash_command, category = "Admin", owners_only, hide_in_help)]
|
||||||
|
async fn sqlite_transfer(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "vrc_data.json"] vrc_data: Attachment,
|
||||||
|
// #[description = "no_bot_perms.json"] no_bot_perms: Attachment,
|
||||||
|
#[description = "guild_channels.json"] guild_channels: Attachment,
|
||||||
|
#[description = "authorized_users.json"] authorized_users: Attachment,
|
||||||
|
#[description = "Cleared_IDs.json"] cleard_ids: Attachment,
|
||||||
|
#[description = "Monitored Guilds.json"] monitored_guilds: Attachment,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
ctx.defer().await?;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AuthorizedUser {
|
||||||
|
user_id: u64,
|
||||||
|
guild_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ClearedID {
|
||||||
|
Discord_ID: u64,
|
||||||
|
Name: String,
|
||||||
|
Where_found: String,
|
||||||
|
Cleared_Reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct MonitoredGuild {
|
||||||
|
Guild_Name: String,
|
||||||
|
Guild_ID: u64,
|
||||||
|
Invite_link: Option<String>,
|
||||||
|
Updated: Option<String>,
|
||||||
|
DMCA_Takedown_Nuked: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct GuildChannel {
|
||||||
|
guild: u64,
|
||||||
|
channel_id: u64,
|
||||||
|
kick_active: u64,
|
||||||
|
Name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct vrc_data {
|
||||||
|
vrc_id: Option<String>,
|
||||||
|
guild_id: Option<u64>,
|
||||||
|
name: Option<String>,
|
||||||
|
discord_id: u64,
|
||||||
|
reason: String,
|
||||||
|
image: Option<String>,
|
||||||
|
extra: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut con = open_redis_connection().await?;
|
||||||
|
let mut pipe = redis::pipe();
|
||||||
|
|
||||||
|
let msg = ctx.say("Downloading files").await?;
|
||||||
|
|
||||||
|
let authorized_users_json = authorized_users.download().await?;
|
||||||
|
let guild_channel_json = guild_channels.download().await?;
|
||||||
|
let cleard_ids_json = cleard_ids.download().await?;
|
||||||
|
let monitored_guilds_json = monitored_guilds.download().await?;
|
||||||
|
let vrc_data_json = vrc_data.download().await?;
|
||||||
|
|
||||||
|
msg.edit(ctx, |b| b.content("Converting json to structs"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let auth_user_vec: Vec<AuthorizedUser> =
|
||||||
|
serde_json::from_str(std::str::from_utf8(&authorized_users_json)?)?;
|
||||||
|
let guild_channel_vec: Vec<GuildChannel> =
|
||||||
|
serde_json::from_str(std::str::from_utf8(&guild_channel_json)?)?;
|
||||||
|
let cleard_id_vec: Vec<ClearedID> =
|
||||||
|
serde_json::from_str(std::str::from_utf8(&cleard_ids_json)?)?;
|
||||||
|
let monitored_guilds_vec: Vec<MonitoredGuild> =
|
||||||
|
serde_json::from_str(std::str::from_utf8(&monitored_guilds_json)?)?;
|
||||||
|
let vrc_data_vec: Vec<vrc_data> = serde_json::from_str(std::str::from_utf8(&vrc_data_json)?)?;
|
||||||
|
|
||||||
|
msg.edit(ctx, |b| b.content("Preparing authorized_users data"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for authed_user in auth_user_vec {
|
||||||
|
pipe.cmd("SADD").arg(&[
|
||||||
|
format!("authed-server-users:{}", authed_user.guild_id),
|
||||||
|
format!("{}", authed_user.user_id),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.edit(ctx, |b| b.content("Preparing guild_channels data"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for guild_settings in guild_channel_vec {
|
||||||
|
let formatted = GuildSettings {
|
||||||
|
channel_id: format!("{}", guild_settings.channel_id),
|
||||||
|
kick: match guild_settings.kick_active {
|
||||||
|
1 => true,
|
||||||
|
0 => false,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
server_name: guild_settings.Name,
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&formatted).unwrap();
|
||||||
|
pipe.cmd("JSON.SET").arg(&[
|
||||||
|
format!("guild-settings:{}", guild_settings.guild),
|
||||||
|
"$".to_string(),
|
||||||
|
json,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.edit(ctx, |b| b.content("Preparing cleard_ids data"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for cleared_id in cleard_id_vec {
|
||||||
|
let formatted: ClearedUser = ClearedUser {
|
||||||
|
user_id: format!("{}", cleared_id.Discord_ID),
|
||||||
|
username: cleared_id.Name,
|
||||||
|
where_found: cleared_id.Where_found,
|
||||||
|
reason: cleared_id.Cleared_Reason,
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&formatted).unwrap();
|
||||||
|
pipe.cmd("JSON.SET").arg(&[
|
||||||
|
format!("cleared-user:{}", cleared_id.Discord_ID),
|
||||||
|
"$".to_string(),
|
||||||
|
json,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.edit(ctx, |b| b.content("Preparing monitored_guilds data"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for guild in monitored_guilds_vec {
|
||||||
|
let formatted: MonitoredGuildInfo = MonitoredGuildInfo {
|
||||||
|
guild_name: guild.Guild_Name.to_string(),
|
||||||
|
guild_id: format!("{}", guild.Guild_ID),
|
||||||
|
invite_link: match guild.Invite_link {
|
||||||
|
None => "N/A".to_string(),
|
||||||
|
Some(link) => link.to_string(),
|
||||||
|
},
|
||||||
|
updated: match guild.Updated {
|
||||||
|
None => "Never".to_string(),
|
||||||
|
Some(date) => date.to_string(),
|
||||||
|
},
|
||||||
|
status: match guild.DMCA_Takedown_Nuked {
|
||||||
|
None => "Unknown".to_string(),
|
||||||
|
Some(status) => status.to_string(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&formatted).unwrap();
|
||||||
|
pipe.cmd("JSON.SET").arg(&[
|
||||||
|
format!("monitored-guild:{}", guild.Guild_ID),
|
||||||
|
"$".to_string(),
|
||||||
|
json,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.edit(ctx, |b| b.content("Preparing vrc_data")).await?;
|
||||||
|
|
||||||
|
let mut parsed_ids: HashSet<String> = HashSet::new();
|
||||||
|
|
||||||
|
for user_data in vrc_data_vec {
|
||||||
|
match parsed_ids.contains(&format!("{}", user_data.discord_id)) {
|
||||||
|
false => {
|
||||||
|
let mut new_user = UserInfo {
|
||||||
|
vrc_id: user_data.vrc_id,
|
||||||
|
username: user_data.name,
|
||||||
|
discord_id: Some(format!("{}", user_data.discord_id)),
|
||||||
|
offences: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let offense = vec![Offense {
|
||||||
|
guild_id: match user_data.guild_id {
|
||||||
|
None => "N/A".to_string(),
|
||||||
|
Some(gid) => format!("{}", gid),
|
||||||
|
},
|
||||||
|
reason: user_data.reason,
|
||||||
|
image: user_data.image,
|
||||||
|
extra: user_data.extra,
|
||||||
|
}];
|
||||||
|
new_user.offences = offense;
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&new_user).unwrap();
|
||||||
|
pipe.cmd("JSON.SET").arg(&[
|
||||||
|
format!("user:{}", user_data.discord_id),
|
||||||
|
"$".to_string(),
|
||||||
|
json,
|
||||||
|
]);
|
||||||
|
|
||||||
|
parsed_ids.insert(format!("{}", user_data.discord_id));
|
||||||
|
}
|
||||||
|
true => {
|
||||||
|
let offense = Offense {
|
||||||
|
guild_id: match user_data.guild_id {
|
||||||
|
None => "N/A".to_string(),
|
||||||
|
Some(gid) => format!("{}", gid),
|
||||||
|
},
|
||||||
|
reason: user_data.reason,
|
||||||
|
image: user_data.image,
|
||||||
|
extra: user_data.extra,
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&offense).unwrap();
|
||||||
|
pipe.cmd("JSON.ARRAPPEND")
|
||||||
|
.arg(format!("user:{}", user_data.discord_id))
|
||||||
|
.arg("$.offences".to_string())
|
||||||
|
.arg(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.edit(ctx, |b| b.content("Uploading data to DB")).await?;
|
||||||
|
|
||||||
|
pipe.query_async(&mut con).await?;
|
||||||
|
|
||||||
|
msg.edit(ctx, |b| b.content("All done!")).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
516
src/commands/admin.rs
Normal file
516
src/commands/admin.rs
Normal file
|
@ -0,0 +1,516 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use core::time;
|
||||||
|
use poise::serenity_prelude::Attachment;
|
||||||
|
use poise::serenity_prelude::{self as serenity, Activity, Member, OnlineStatus};
|
||||||
|
use poise::serenity_prelude::{ChannelId, Colour};
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
use rand::Rng;
|
||||||
|
use rusted_fbt_lib::checks::guild_auth_check;
|
||||||
|
use rusted_fbt_lib::structs::GuildSettings;
|
||||||
|
use rusted_fbt_lib::utils::{auth, open_redis_connection, set_guild_settings};
|
||||||
|
use rusted_fbt_lib::{
|
||||||
|
args::Args,
|
||||||
|
checks::bot_admin_check,
|
||||||
|
types::{Context, Error},
|
||||||
|
utils::{inc_execution_count, verbose_mode},
|
||||||
|
};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::ops::Add;
|
||||||
|
use std::process::exit;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use tracing::instrument;
|
||||||
|
use tracing::{event, Level};
|
||||||
|
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
category = "Admin",
|
||||||
|
check = "bot_admin_check",
|
||||||
|
hide_in_help
|
||||||
|
)]
|
||||||
|
/// Sends message to specified user ID
|
||||||
|
pub async fn botmsg(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "User ID"] user: serenity::User,
|
||||||
|
#[description = "Message"] msg: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
ctx.defer().await?;
|
||||||
|
|
||||||
|
user.direct_message(ctx, |f| f.content(&msg)).await?;
|
||||||
|
|
||||||
|
ctx.say(format!("Sent message to: {}", user.name)).await?;
|
||||||
|
ctx.say(format!("Message: {}", &msg)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(
|
||||||
|
prefix_command,
|
||||||
|
slash_command,
|
||||||
|
category = "Admin",
|
||||||
|
required_permissions = "BAN_MEMBERS",
|
||||||
|
required_bot_permissions = "BAN_MEMBERS",
|
||||||
|
guild_only,
|
||||||
|
ephemeral
|
||||||
|
)]
|
||||||
|
/// Explains how to ban a list of users with `ban
|
||||||
|
pub async fn ban_help(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
ctx.say("To ban a single user the easiest way is with the slash command `/ban ban_user @USER/ID` since you don't need to provide message deletion numbers or a reason.").await?;
|
||||||
|
ctx.say(format!("In order to ban multiple people please use this command as a a prefix command like so:\n```\n{}ban ban_user \"Reason in qutoation marks\" 0(A number from 0 to 7, how many days worth of messages you want to delet) userID1 userID2 userID3\n```", args.prefix)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(
|
||||||
|
prefix_command,
|
||||||
|
slash_command,
|
||||||
|
category = "Admin",
|
||||||
|
required_permissions = "BAN_MEMBERS",
|
||||||
|
required_bot_permissions = "BAN_MEMBERS",
|
||||||
|
guild_only,
|
||||||
|
ephemeral
|
||||||
|
)]
|
||||||
|
/// Explains how to ban a list of users with `ban
|
||||||
|
pub async fn ban_user(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Ban reason"] reason: Option<String>,
|
||||||
|
#[description = "How many days of messages to purge (Max of 7)"]
|
||||||
|
#[min = 0]
|
||||||
|
#[max = 7]
|
||||||
|
dmd: Option<u8>,
|
||||||
|
#[description = "Member(s) to ban"] members: Vec<Member>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let delete_count: u8 = dmd.map_or(0u8, |num| {
|
||||||
|
let num_check = num;
|
||||||
|
if num_check.le(&7u8) {
|
||||||
|
num
|
||||||
|
} else {
|
||||||
|
7u8
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let reason_sanitised = reason.map_or_else(|| "Banned via bot ban command".to_string(), |r| r);
|
||||||
|
|
||||||
|
// TODO: Change to your own emojis!
|
||||||
|
|
||||||
|
// Mojo test server emoji version
|
||||||
|
// let phrase_list = vec!["has been ejected", "banned quietly", "terminated", "thrown out", "<a:Banned1:1000474420864880831><a:Banned2:1000474423683452998><a:Banned3:1000474426447503441>"];
|
||||||
|
|
||||||
|
// FBT Emoji version
|
||||||
|
let phrase_list = ["has been ejected", "banned quietly", "terminated", "thrown out", "<a:Banned1:1000474106929631433><a:Banned2:1000474109802725457><a:Banned3:1000474112734531715>"];
|
||||||
|
|
||||||
|
match ctx.guild() {
|
||||||
|
Some(guild) => match members.len() {
|
||||||
|
0 => {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
ctx.say("You must provide at least one user to ban!")
|
||||||
|
.await?;
|
||||||
|
ctx.say(format!("In order to ban multiple people please use this command as a a prefix command like so:\n```\n{}ban userID1 userID2 userID3\n```", args.prefix)).await?;
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
let member = guild.member(ctx, members[0].user.id).await?;
|
||||||
|
if let Err(error) = member
|
||||||
|
.ban_with_reason(ctx, delete_count, reason_sanitised.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if verbose_mode() {
|
||||||
|
ctx.say(format!(
|
||||||
|
"Failed to ban {} because of {:?}",
|
||||||
|
member.display_name(),
|
||||||
|
error
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
ctx.say(format!("Failed to ban {}", member.display_name()))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
// 0u8
|
||||||
|
} else {
|
||||||
|
let phrase = phrase_list
|
||||||
|
.choose(&mut rand::thread_rng())
|
||||||
|
.expect("Unable to get meme phrase for ban");
|
||||||
|
ctx.say(format!("{} has been {phrase}", member.display_name()))
|
||||||
|
.await?;
|
||||||
|
// 0u8
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
for member in members {
|
||||||
|
if let Err(error) = member
|
||||||
|
.ban_with_reason(ctx, delete_count, reason_sanitised.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if verbose_mode() {
|
||||||
|
ctx.say(format!(
|
||||||
|
"Failed to ban {} because of {:?}",
|
||||||
|
member.display_name(),
|
||||||
|
error
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
ctx.say(format!("Failed to ban {}", member.display_name()))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
// 0u8
|
||||||
|
} else {
|
||||||
|
let phrase = phrase_list
|
||||||
|
.choose(&mut rand::thread_rng())
|
||||||
|
.expect("Unable to get meme phrase for ban");
|
||||||
|
ctx.say(format!("{} has been {phrase}", member.display_name()))
|
||||||
|
.await?;
|
||||||
|
// 0u8
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
ctx.say("This must be ran from inside a guild").await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(
|
||||||
|
prefix_command,
|
||||||
|
slash_command,
|
||||||
|
category = "Admin",
|
||||||
|
required_permissions = "BAN_MEMBERS",
|
||||||
|
required_bot_permissions = "BAN_MEMBERS",
|
||||||
|
guild_only,
|
||||||
|
subcommands("ban_help", "ban_user")
|
||||||
|
)]
|
||||||
|
/// Ban a member or list of members by ID or Mention
|
||||||
|
pub async fn ban(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
ctx.say("Run `/ban ban_help` to learn how to use this command and then use ")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Change to your own emojis!
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(prefix_command, slash_command, category = "Admin", owners_only)]
|
||||||
|
/// Literally just shoot the bot!
|
||||||
|
pub async fn shutdown(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let pewpew = ctx
|
||||||
|
.say("<:GunPoint:908506214915276851> <:FBT:795660945627676712>")
|
||||||
|
.await?;
|
||||||
|
sleep(time::Duration::from_secs(1)).await;
|
||||||
|
pewpew
|
||||||
|
.edit(ctx, |b| {
|
||||||
|
b.content("<:GunPoint:908506214915276851> 💥 <:FBT:795660945627676712>")
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
sleep(time::Duration::from_secs(1)).await;
|
||||||
|
pewpew
|
||||||
|
.edit(ctx, |b| {
|
||||||
|
b.content("<:GunPoint:908506214915276851> <:FBT:795660945627676712>")
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
sleep(time::Duration::from_secs(1)).await;
|
||||||
|
pewpew
|
||||||
|
.edit(ctx, |b| {
|
||||||
|
b.content("<:GunPoint:908506214915276851> 🩸 <:FBT:795660945627676712> 🩸")
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
sleep(time::Duration::from_secs(1)).await;
|
||||||
|
ctx.say("Exiting now!").await?;
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
inc_execution_count().await?;
|
||||||
|
|
||||||
|
let activity = Activity::playing("Sleeping");
|
||||||
|
let status = OnlineStatus::Offline;
|
||||||
|
|
||||||
|
ctx.serenity_context()
|
||||||
|
.set_presence(Some(activity), status)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
/// Authorize someone in this guild
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(
|
||||||
|
prefix_command,
|
||||||
|
slash_command,
|
||||||
|
category = "Admin",
|
||||||
|
check = "guild_auth_check",
|
||||||
|
guild_only
|
||||||
|
)]
|
||||||
|
pub async fn authorize(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "User to authorise in this server"] user: Member,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
ctx.defer_or_broadcast().await?;
|
||||||
|
|
||||||
|
let uid = format!("{}", user.user.id.as_u64());
|
||||||
|
|
||||||
|
let mut con = open_redis_connection().await?;
|
||||||
|
|
||||||
|
// * json format: {users:[ID1, ID2, IDect]}
|
||||||
|
let key_list: Option<HashSet<String>> = redis::cmd("SMEMBERS")
|
||||||
|
.arg(format!(
|
||||||
|
"authed-server-users:{}",
|
||||||
|
ctx.guild_id().unwrap().as_u64()
|
||||||
|
))
|
||||||
|
.clone()
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(list) = key_list {
|
||||||
|
if list.contains(&uid) {
|
||||||
|
ctx.say("User already authorised in this server!").await?;
|
||||||
|
} else {
|
||||||
|
match auth(ctx, &mut con, uid).await {
|
||||||
|
Ok(()) => {
|
||||||
|
ctx.say(format!(
|
||||||
|
"{} is now authorized to use commands in this server!",
|
||||||
|
user.display_name()
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Err(_error) if !verbose_mode() => {
|
||||||
|
ctx.say(format!("Failed to auth {}!", user.display_name()))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
ctx.say(format!(
|
||||||
|
"Failed to auth {}! Caused by {:?}",
|
||||||
|
user.display_name(),
|
||||||
|
error
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auth(ctx, &mut con, uid).await?;
|
||||||
|
|
||||||
|
ctx.say(format!(
|
||||||
|
"{} is now authorized to use commands in this server!",
|
||||||
|
user.display_name()
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(
|
||||||
|
prefix_command,
|
||||||
|
slash_command,
|
||||||
|
category = "Admin",
|
||||||
|
check = "bot_admin_check"
|
||||||
|
)]
|
||||||
|
/// Send annoucement to any server that has been setup
|
||||||
|
pub async fn announcement(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Title of announcement embed"] title: String,
|
||||||
|
#[description = "Message to send to all servers (As a .txt file!)"] message_file: Attachment,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
ctx.defer_or_broadcast().await?;
|
||||||
|
|
||||||
|
let message_content = message_file.download().await?;
|
||||||
|
let message = std::str::from_utf8(&message_content)?;
|
||||||
|
|
||||||
|
let mut con = open_redis_connection().await?;
|
||||||
|
|
||||||
|
let key_list: Vec<String> = redis::cmd("KEYS")
|
||||||
|
.arg("guild-settings:*")
|
||||||
|
.clone()
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut key_pipe = redis::pipe();
|
||||||
|
|
||||||
|
for key in key_list {
|
||||||
|
key_pipe.cmd("JSON.GET").arg(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
let setting_entries: Vec<String> = key_pipe.atomic().query_async(&mut con).await?;
|
||||||
|
|
||||||
|
let mut guild_settings_collection = Vec::new();
|
||||||
|
for settings in setting_entries {
|
||||||
|
let gs: GuildSettings = serde_json::from_str(&settings)?;
|
||||||
|
|
||||||
|
guild_settings_collection.push(gs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Change to custom announcement message!
|
||||||
|
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
for guild in guild_settings_collection.clone() {
|
||||||
|
let colour = &mut rand::thread_rng().gen_range(0..10_000_000);
|
||||||
|
match ChannelId(guild.channel_id.parse::<u64>()?).send_message(ctx, |f| {
|
||||||
|
f.embed(|e| {
|
||||||
|
e.title(format!("New announcement from FBT Security: {}", title.clone()))
|
||||||
|
.description(message)
|
||||||
|
.color(Colour::new(*colour))
|
||||||
|
.author(|a| {
|
||||||
|
a.icon_url("https://cdn.discordapp.com/avatars/743269383438073856/959512463b1559b14818590d8c8a9d2a.webp?size=4096")
|
||||||
|
.name("FBT Security")
|
||||||
|
})
|
||||||
|
.thumbnail("https://media.giphy.com/media/U4sfHXAALLYBQzPcWk/giphy.gif")
|
||||||
|
})
|
||||||
|
}).await {
|
||||||
|
Err(e)=>{
|
||||||
|
event!(
|
||||||
|
Level::INFO,
|
||||||
|
"Failed to send announcement to a server because of" = ?e
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Ok(msg) => {
|
||||||
|
count = count.add(1);
|
||||||
|
println!("Sent to: {}", msg.link());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.say(format!(
|
||||||
|
"Sent annoucement to {}/{} servers!",
|
||||||
|
count,
|
||||||
|
guild_settings_collection.len()
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(
|
||||||
|
prefix_command,
|
||||||
|
slash_command,
|
||||||
|
category = "Admin",
|
||||||
|
required_permissions = "ADMINISTRATOR",
|
||||||
|
guild_only
|
||||||
|
)]
|
||||||
|
/// Request an FBT staff member to come and auth your server
|
||||||
|
pub async fn request_setup(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Do you want to kick accounts that are under 90 days old"] alt_protection: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
ctx.defer().await?;
|
||||||
|
let link = ChannelId(*ctx.channel_id().as_u64())
|
||||||
|
.create_invite(ctx, |f| f.temporary(false).max_age(0).unique(false))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: this channel is where the bot alterts you when a server is requesting use of the bot's moderation stuff
|
||||||
|
ChannelId(953_435_498_318_286_898).send_message(ctx, |f| {
|
||||||
|
f.content(format!("{0} is requesting authentication! {1}\n They requested for alt protection to be: `{alt_protection}`", ctx.guild().unwrap().name, link.url()))
|
||||||
|
}).await?;
|
||||||
|
|
||||||
|
ctx.send(|b| b.content("Request sent, sit tight!\nOnce an administrator joins make sure to give them permissions to acess the channel so they can set it up!").ephemeral(true)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
category = "Admin",
|
||||||
|
check = "guild_auth_check",
|
||||||
|
guild_only
|
||||||
|
)]
|
||||||
|
/// Setup your server's settings
|
||||||
|
pub async fn setup(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Do you want to kick accounts that are under 90 days old when they join"]
|
||||||
|
alt_protection: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut con = open_redis_connection().await?;
|
||||||
|
|
||||||
|
let guild_settings_json_in: Option<String> = redis::cmd("JSON.GET")
|
||||||
|
.arg(format!(
|
||||||
|
"guild-settings:{}",
|
||||||
|
ctx.guild_id().unwrap().as_u64()
|
||||||
|
))
|
||||||
|
.clone()
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let ch_id = format!("{}", ctx.channel_id().as_u64());
|
||||||
|
let g_name = ctx
|
||||||
|
.partial_guild()
|
||||||
|
.await
|
||||||
|
.expect("Unable to get Guild info")
|
||||||
|
.name;
|
||||||
|
|
||||||
|
if let Some(json_in) = guild_settings_json_in {
|
||||||
|
let mut settings: GuildSettings = serde_json::from_str(&json_in)?;
|
||||||
|
settings.channel_id = ch_id.clone();
|
||||||
|
settings.kick = alt_protection;
|
||||||
|
settings.server_name = g_name;
|
||||||
|
|
||||||
|
set_guild_settings(ctx, &mut con, settings).await?;
|
||||||
|
ctx.say(format!("Settings have been updated for your server!\nChannel for kick messages and bot announcements: <#{0}>.\nAlt protection: {alt_protection:?}.", ch_id.clone())).await?;
|
||||||
|
} else {
|
||||||
|
let settings = GuildSettings {
|
||||||
|
channel_id: ch_id.clone(),
|
||||||
|
kick: alt_protection,
|
||||||
|
server_name: g_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
set_guild_settings(ctx, &mut con, settings).await?;
|
||||||
|
ctx.say(format!("Settings have been created for your server!\nChannel for kick messages and bot announcements: <#{0}>.\nAlt protection: {alt_protection:?}.", ch_id.clone())).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(
|
||||||
|
prefix_command,
|
||||||
|
slash_command,
|
||||||
|
category = "Admin",
|
||||||
|
check = "guild_auth_check",
|
||||||
|
guild_only
|
||||||
|
)]
|
||||||
|
/// Set your server's alt protection policy
|
||||||
|
pub async fn toggle_kick(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let mut con = open_redis_connection().await?;
|
||||||
|
|
||||||
|
let guild_settings_json_in: Option<String> = redis::cmd("JSON.GET")
|
||||||
|
.arg(format!(
|
||||||
|
"guild-settings:{}",
|
||||||
|
ctx.guild_id().unwrap().as_u64()
|
||||||
|
))
|
||||||
|
.clone()
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match guild_settings_json_in {
|
||||||
|
// Update settings
|
||||||
|
Some(json_in) => {
|
||||||
|
let mut settings: GuildSettings = serde_json::from_str(&json_in)?;
|
||||||
|
settings.kick = !settings.kick;
|
||||||
|
|
||||||
|
set_guild_settings(ctx, &mut con, settings.clone()).await?;
|
||||||
|
ctx.say(format!(
|
||||||
|
"Settings have been updated for your server!\nAlt protection: {:?}.",
|
||||||
|
settings.kick
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
// TODO: change to custom message
|
||||||
|
// This should not be able to trigger because of the auth check but better safe than sorry
|
||||||
|
None => {
|
||||||
|
ctx.say("Your server has not been setup by a bot admin yet! Please context a bot admin or azuki to get authorised.").await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
1216
src/commands/database.rs
Normal file
1216
src/commands/database.rs
Normal file
File diff suppressed because it is too large
Load diff
176
src/commands/fun.rs
Normal file
176
src/commands/fun.rs
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
use core::time;
|
||||||
|
|
||||||
|
use poise::serenity_prelude::{self as serenity, AttachmentType};
|
||||||
|
use rusted_fbt_lib::enums::WaifuTypes;
|
||||||
|
use rusted_fbt_lib::types::{Context, Error};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use tracing::instrument;
|
||||||
|
use uwuifier::uwuify_str_sse;
|
||||||
|
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(prefix_command, slash_command, category = "Fun", member_cooldown = 15)]
|
||||||
|
/// This user is cringe
|
||||||
|
pub async fn cringe(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Optionally call this user cringe"] user: Option<serenity::User>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let camera_message = ctx.say("<a:camera:870459823907553352>").await?;
|
||||||
|
|
||||||
|
sleep(time::Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
camera_message
|
||||||
|
.edit(ctx, |b| {
|
||||||
|
b.content("<a:camera_with_flash:870458599325986898>")
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sleep(time::Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
camera_message
|
||||||
|
.edit(ctx, |b| b.content("<a:camera:870459823907553352>"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match user {
|
||||||
|
None => {
|
||||||
|
ctx.say("Yep, that's going in my cringe compilation")
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Some(user) => {
|
||||||
|
ctx.send(|m| {
|
||||||
|
m.content(format!(
|
||||||
|
"Yep <@{}>, that's going in my cringe compilation",
|
||||||
|
user.id
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// OwOifys your message
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(prefix_command, slash_command, category = "Fun")]
|
||||||
|
pub async fn owo(ctx: Context<'_>, #[description = "Message"] msg: String) -> Result<(), Error> {
|
||||||
|
ctx.say(uwuify_str_sse(msg.as_str())).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replies with pog pog pog and pog frog!
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(prefix_command, slash_command, category = "Fun", member_cooldown = 10)]
|
||||||
|
pub async fn pog(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
ctx.send(|f| {
|
||||||
|
f.content("Pog pog pog!")
|
||||||
|
.ephemeral(false)
|
||||||
|
.attachment(AttachmentType::Bytes {
|
||||||
|
data: std::borrow::Cow::Borrowed(include_bytes!("../../assets/pog-frog.gif")),
|
||||||
|
filename: String::from("pog-frog.gif"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a random waifu (SFW)
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(slash_command, category = "Fun", member_cooldown = 5)]
|
||||||
|
pub async fn waifu(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "What waifu do you want?"] waifu_type: Option<WaifuTypes>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
struct Waifu {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let choice: String = match waifu_type {
|
||||||
|
None => "waifu".to_string(),
|
||||||
|
Some(WaifuTypes::Neko) => "neko".to_string(),
|
||||||
|
Some(WaifuTypes::Megumin) => "megumin".to_string(),
|
||||||
|
Some(WaifuTypes::Bully) => "bully".to_string(),
|
||||||
|
Some(WaifuTypes::Cuddle) => "cuddle".to_string(),
|
||||||
|
Some(WaifuTypes::Cry) => "cry".to_string(),
|
||||||
|
Some(WaifuTypes::Kiss) => "kiss".to_string(),
|
||||||
|
Some(WaifuTypes::Lick) => "lick".to_string(),
|
||||||
|
Some(WaifuTypes::Pat) => "pat".to_string(),
|
||||||
|
Some(WaifuTypes::Smug) => "smug".to_string(),
|
||||||
|
Some(WaifuTypes::Bonk) => "bonk".to_string(),
|
||||||
|
Some(WaifuTypes::Blush) => "blush".to_string(),
|
||||||
|
Some(WaifuTypes::Smile) => "smile".to_string(),
|
||||||
|
Some(WaifuTypes::Wave) => "wave".to_string(),
|
||||||
|
Some(WaifuTypes::Highfive) => "highfive".to_string(),
|
||||||
|
Some(WaifuTypes::Handhold) => "handhold".to_string(),
|
||||||
|
Some(WaifuTypes::Nom) => "nom".to_string(),
|
||||||
|
Some(WaifuTypes::Bite) => "bite".to_string(),
|
||||||
|
Some(WaifuTypes::Glomp) => "glomp".to_string(),
|
||||||
|
Some(WaifuTypes::Slap) => "slap".to_string(),
|
||||||
|
Some(WaifuTypes::Kill) => "kill".to_string(),
|
||||||
|
Some(WaifuTypes::Happy) => "happy".to_string(),
|
||||||
|
Some(WaifuTypes::Wink) => "wink".to_string(),
|
||||||
|
Some(WaifuTypes::Poke) => "poke".to_string(),
|
||||||
|
Some(WaifuTypes::Dance) => "dance".to_string(),
|
||||||
|
Some(WaifuTypes::Cringe) => "cringe".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = reqwest::get(format!("https://api.waifu.pics/sfw/{choice}")).await?;
|
||||||
|
let waifu: Waifu = response.json().await?;
|
||||||
|
|
||||||
|
// let waifu: Waifu = serde_json::from_str(json_content.as_str())?;
|
||||||
|
|
||||||
|
ctx.send(|b| b.embed(|e| e.title("Your waifu:").image(waifu.url)))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replies with pong!
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(prefix_command, slash_command, category = "Fun", member_cooldown = 15)]
|
||||||
|
pub async fn ping(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
ctx.say("Pong! 🏓").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// That's toxic
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(slash_command, category = "Fun", member_cooldown = 15)]
|
||||||
|
pub async fn toxic(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Optionally call this user cringe"] user: Option<serenity::User>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
const GIF_NAME: &str = "toxic.gif";
|
||||||
|
let toxic_gif = include_bytes!("../../assets/toxic.gif");
|
||||||
|
|
||||||
|
ctx.defer().await?;
|
||||||
|
|
||||||
|
match user {
|
||||||
|
None => {
|
||||||
|
ctx.send(|m| {
|
||||||
|
m.content("That's toxic!")
|
||||||
|
.attachment(AttachmentType::Bytes {
|
||||||
|
data: std::borrow::Cow::Borrowed(toxic_gif), // include_bytes! directly embeds the gif file into the executable at compile time.
|
||||||
|
filename: GIF_NAME.to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Some(user) => {
|
||||||
|
ctx.send(|m| {
|
||||||
|
m.content(format!("<@{}>, That's toxic!", user.id.as_u64()))
|
||||||
|
.attachment(AttachmentType::Bytes {
|
||||||
|
data: std::borrow::Cow::Borrowed(toxic_gif), // include_bytes! directly embeds the gif file into the executable at compile time.
|
||||||
|
filename: GIF_NAME.to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
147
src/commands/info.rs
Normal file
147
src/commands/info.rs
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
use poise::serenity_prelude::{ChannelId, Colour};
|
||||||
|
use rand::Rng;
|
||||||
|
use rusted_fbt_lib::{
|
||||||
|
types::{Context, Error},
|
||||||
|
utils::open_redis_connection,
|
||||||
|
vars::{FEEDBACK_CHANNEL_ID, HELP_EXTRA_TEXT, VERSION},
|
||||||
|
};
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use tracing::instrument;
|
||||||
|
use tracing::{event, Level};
|
||||||
|
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(prefix_command, track_edits, slash_command, category = "Info")]
|
||||||
|
/// Show this help menu
|
||||||
|
pub async fn help(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Specific command to show help about"]
|
||||||
|
#[autocomplete = "poise::builtins::autocomplete_command"]
|
||||||
|
command: Option<String>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
poise::builtins::help(
|
||||||
|
ctx,
|
||||||
|
command.as_deref(),
|
||||||
|
poise::builtins::HelpConfiguration {
|
||||||
|
extra_text_at_bottom: HELP_EXTRA_TEXT,
|
||||||
|
show_context_menu_commands: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(slash_command, category = "Info", member_cooldown = 10, ephemeral)]
|
||||||
|
/// Provide feedback for the bot team to look at!
|
||||||
|
pub async fn feedback(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Feedback you want to provide"] feedback: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
ctx.defer_ephemeral().await?;
|
||||||
|
let mut con = open_redis_connection().await?;
|
||||||
|
|
||||||
|
redis::cmd("SET")
|
||||||
|
.arg(format!(
|
||||||
|
"feedback:{}-{}-{}",
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs(),
|
||||||
|
ctx.author().id.as_u64(),
|
||||||
|
ctx.author().tag()
|
||||||
|
))
|
||||||
|
.arg(feedback.clone())
|
||||||
|
.clone()
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let colour = &mut rand::thread_rng().gen_range(0..10_000_000);
|
||||||
|
ChannelId(FEEDBACK_CHANNEL_ID)
|
||||||
|
.send_message(ctx, |f| {
|
||||||
|
f.embed(|e| {
|
||||||
|
e.title("New feedback!".to_string())
|
||||||
|
.description(feedback)
|
||||||
|
.color(Colour::new(*colour))
|
||||||
|
.author(|a| a.icon_url(ctx.author().face()).name(ctx.author().tag()))
|
||||||
|
.thumbnail("https://media.giphy.com/media/U4sfHXAALLYBQzPcWk/giphy.gif")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
ctx.say("Thank you for the feedback! It has been sent directly to our developers.")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(prefix_command, slash_command, category = "Info", member_cooldown = 10)]
|
||||||
|
/// Have some info about the bot
|
||||||
|
pub async fn about(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let guild_count = ctx
|
||||||
|
.serenity_context()
|
||||||
|
.cache
|
||||||
|
.guilds()
|
||||||
|
.len()
|
||||||
|
.clone()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// TODO: change to your own URLs
|
||||||
|
let mut fields = vec", false),
|
||||||
|
("Remove any of your info from the bot:", "[Delete your data](https://fbtsecurity.fbtheaven.com/data-and-privacy-policy#delete-your-data)", false),
|
||||||
|
("Bot version:", VERSION.unwrap_or("unknown"), false),
|
||||||
|
("Server count:", &guild_count, false),
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO: reduce the ammount of #[cfg(feature = "database")] here!!
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
let mut con = open_redis_connection().await?;
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
let execution_count: String = redis::cmd("GET")
|
||||||
|
.arg("status:commands-executed")
|
||||||
|
.clone()
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
let mut new_field = vec![(
|
||||||
|
"Total commands run since 2.0.18:",
|
||||||
|
execution_count.as_str(),
|
||||||
|
false,
|
||||||
|
)];
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
fields.append(&mut new_field);
|
||||||
|
|
||||||
|
// TODO: change to your own URLs
|
||||||
|
ctx.send(|f| {
|
||||||
|
f.embed(|e| {
|
||||||
|
e.title("About")
|
||||||
|
.url("https://fbtsecurity.fbtheaven.com/")
|
||||||
|
.author(|a| {
|
||||||
|
a.name("FBT Staff")
|
||||||
|
.url("https://fbtsecurity.fbtheaven.com/")
|
||||||
|
})
|
||||||
|
.fields(fields)
|
||||||
|
.footer(|foot| {
|
||||||
|
foot.text("Time mojo spent on V2.0+")
|
||||||
|
})
|
||||||
|
.image(format!("https://wakatime.com/badge/user/fd57ff6b-f3f1-4957-b9c6-7e09bc3f0559/project/d2f87f17-8c44-4835-b4f6-f0089e52515f.png?rand={}", rand::thread_rng().gen_range(0..1_000_000_000)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
event!(
|
||||||
|
Level::INFO,
|
||||||
|
"Total commands run since 2.0.18" = execution_count.parse::<u32>().unwrap() + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
6
src/commands/mod.rs
Normal file
6
src/commands/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod admin;
|
||||||
|
pub mod database;
|
||||||
|
pub mod fun;
|
||||||
|
pub mod info;
|
||||||
|
pub mod tickets;
|
||||||
|
pub mod tools;
|
225
src/commands/tickets.rs
Normal file
225
src/commands/tickets.rs
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
use poise::serenity_prelude::{self as serenity};
|
||||||
|
use poise::serenity_prelude::{ChannelId, RoleId};
|
||||||
|
use poise::serenity_prelude::{PermissionOverwrite, PermissionOverwriteType, Permissions};
|
||||||
|
use rusted_fbt_lib::enums::CloseTicketFail;
|
||||||
|
use rusted_fbt_lib::types::{Context, Error};
|
||||||
|
use rusted_fbt_lib::utils::verbose_mode;
|
||||||
|
use rusted_fbt_lib::vars::{CLOSED_TICKET_CATEGORY, FBT_GUILD_ID, TICKET_CATEGORY};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(slash_command, category = "Ticket", guild_only)]
|
||||||
|
/// Create new ticket (FBT discord only!)
|
||||||
|
pub async fn new_ticket(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "An optional topic to put on the ticket"] topic: Option<String>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
ctx.defer_ephemeral().await?;
|
||||||
|
|
||||||
|
if *ctx.guild_id().unwrap().as_u64() == FBT_GUILD_ID {
|
||||||
|
let mut channels = Vec::new();
|
||||||
|
|
||||||
|
for channel in ctx.guild().unwrap().channels {
|
||||||
|
let parent_id = match channel.1.clone() {
|
||||||
|
serenity::Channel::Guild(g) => g.parent_id,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(cat) = parent_id {
|
||||||
|
if *cat.as_u64() == TICKET_CATEGORY {
|
||||||
|
channels.push(channel.0.name(ctx).await.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut existing_ticket = false;
|
||||||
|
|
||||||
|
for ch in channels {
|
||||||
|
if ch.starts_with(&ctx.author().name) {
|
||||||
|
existing_ticket = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing_ticket {
|
||||||
|
ctx.send(|b| {
|
||||||
|
b.content("You already have a ticket open, you cannot open another!")
|
||||||
|
.ephemeral(true)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// TODO: change these IDs to be your own server roles
|
||||||
|
|
||||||
|
// Assuming a guild has already been bound.
|
||||||
|
let perms = vec![
|
||||||
|
PermissionOverwrite {
|
||||||
|
allow: Permissions::READ_MESSAGE_HISTORY
|
||||||
|
| Permissions::VIEW_CHANNEL
|
||||||
|
| Permissions::SEND_MESSAGES
|
||||||
|
| Permissions::ADD_REACTIONS
|
||||||
|
| Permissions::EMBED_LINKS
|
||||||
|
| Permissions::ATTACH_FILES
|
||||||
|
| Permissions::USE_EXTERNAL_EMOJIS,
|
||||||
|
deny: Permissions::empty(),
|
||||||
|
kind: PermissionOverwriteType::Member(ctx.author().id),
|
||||||
|
},
|
||||||
|
PermissionOverwrite {
|
||||||
|
allow: Permissions::all(),
|
||||||
|
deny: Permissions::SEND_TTS_MESSAGES,
|
||||||
|
kind: PermissionOverwriteType::Role(RoleId::from(737_168_134_569_590_888)), // Secretary (Probably not needed)
|
||||||
|
},
|
||||||
|
PermissionOverwrite {
|
||||||
|
allow: Permissions::all(),
|
||||||
|
deny: Permissions::SEND_TTS_MESSAGES,
|
||||||
|
kind: PermissionOverwriteType::Role(RoleId::from(820_914_502_220_513_330)), // Admin (Probably not needed)
|
||||||
|
},
|
||||||
|
PermissionOverwrite {
|
||||||
|
allow: Permissions::all(),
|
||||||
|
deny: Permissions::SEND_TTS_MESSAGES,
|
||||||
|
kind: PermissionOverwriteType::Role(RoleId::from(874_898_210_534_096_907)), // Mods
|
||||||
|
},
|
||||||
|
PermissionOverwrite {
|
||||||
|
allow: Permissions::all(),
|
||||||
|
deny: Permissions::SEND_TTS_MESSAGES,
|
||||||
|
kind: PermissionOverwriteType::Role(RoleId::from(1_005_994_060_416_294_942)), // World admin panel
|
||||||
|
},
|
||||||
|
PermissionOverwrite {
|
||||||
|
allow: Permissions::all(),
|
||||||
|
deny: Permissions::SEND_TTS_MESSAGES,
|
||||||
|
kind: PermissionOverwriteType::Role(RoleId::from(1_046_937_023_400_919_091)), // World admin panel trainee
|
||||||
|
},
|
||||||
|
PermissionOverwrite {
|
||||||
|
allow: Permissions::empty(),
|
||||||
|
deny: Permissions::all(),
|
||||||
|
kind: PermissionOverwriteType::Role(RoleId::from(737_168_134_502_350_849)), // @everyone
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
match ctx
|
||||||
|
.guild()
|
||||||
|
.expect("")
|
||||||
|
.create_channel(ctx, |c| {
|
||||||
|
c.category(ChannelId::from(TICKET_CATEGORY))
|
||||||
|
.name(format!(
|
||||||
|
"{}-{}",
|
||||||
|
ctx.author().name,
|
||||||
|
chrono::offset::Utc::now().format("%s")
|
||||||
|
))
|
||||||
|
.permissions(perms)
|
||||||
|
.topic(topic.unwrap_or_else(|| "A new ticket".to_string()))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(ch) => {
|
||||||
|
ctx.send(|b| {
|
||||||
|
b.content(format!(
|
||||||
|
"Ticket created! Find it here: <#{}>",
|
||||||
|
ch.id.as_u64()
|
||||||
|
))
|
||||||
|
.ephemeral(true)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
ch.say(
|
||||||
|
ctx,
|
||||||
|
format!("New ticket opened by <@{}>!", ctx.author().id.as_u64()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
let err_msg = if verbose_mode() {
|
||||||
|
format!("Failed to create ticket. Reason: {error:?}")
|
||||||
|
} else {
|
||||||
|
"Failed to create ticket".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.send(|b| b.content(err_msg).ephemeral(true)).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[poise::command(slash_command, category = "Ticket", guild_only)]
|
||||||
|
/// Closes the current ticket (FBT discord only!)
|
||||||
|
pub async fn close_ticket(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
ctx.defer_ephemeral().await?;
|
||||||
|
|
||||||
|
if *ctx.guild_id().unwrap().as_u64() == FBT_GUILD_ID {
|
||||||
|
let mut failed = CloseTicketFail::False;
|
||||||
|
|
||||||
|
let current_channel = ctx.channel_id().to_channel(ctx).await?;
|
||||||
|
let chnnl_name = ctx
|
||||||
|
.channel_id()
|
||||||
|
.name(ctx)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|| "Unkown Ticket".to_string());
|
||||||
|
|
||||||
|
let parent_id = match current_channel {
|
||||||
|
serenity::Channel::Guild(g) => g.parent_id,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match parent_id {
|
||||||
|
None => {
|
||||||
|
failed = CloseTicketFail::False;
|
||||||
|
}
|
||||||
|
Some(channel_category) => {
|
||||||
|
if *channel_category.as_u64() == TICKET_CATEGORY {
|
||||||
|
match ctx
|
||||||
|
.channel_id()
|
||||||
|
.edit(ctx, |c| {
|
||||||
|
c.category(Some(ChannelId::from(CLOSED_TICKET_CATEGORY)))
|
||||||
|
.name(format!(
|
||||||
|
"{}-{}",
|
||||||
|
chnnl_name,
|
||||||
|
chrono::offset::Utc::now().format("%s")
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(fail_reason) => {
|
||||||
|
failed = CloseTicketFail::SerenityError(fail_reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failed = CloseTicketFail::IncorrectCategory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match failed {
|
||||||
|
CloseTicketFail::False => {
|
||||||
|
ctx.say("Ticket closed!").await?;
|
||||||
|
}
|
||||||
|
CloseTicketFail::IncorrectCategory => {
|
||||||
|
ctx.send(|b| {
|
||||||
|
b.content(format!(
|
||||||
|
"This can only be ran inside of a channel under <#{}>!",
|
||||||
|
TICKET_CATEGORY
|
||||||
|
))
|
||||||
|
.ephemeral(true)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
CloseTicketFail::SerenityError(error) => {
|
||||||
|
ctx.send(|b| {
|
||||||
|
b.content(format!(
|
||||||
|
"Failed to close ticker because of following error:\n{}",
|
||||||
|
error
|
||||||
|
))
|
||||||
|
.ephemeral(true)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.say("This command must be ran inside of FBT's discord")
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
320
src/commands/tools.rs
Normal file
320
src/commands/tools.rs
Normal file
|
@ -0,0 +1,320 @@
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use poise::serenity_prelude::{self as serenity, AttachmentType, RichInvite};
|
||||||
|
use rusted_fbt_lib::{
|
||||||
|
checks::guild_auth_check,
|
||||||
|
types::{Context, Error},
|
||||||
|
utils::snowflake_to_unix,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::commands::database::check_username_against_db;
|
||||||
|
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(slash_command, track_edits, category = "Tools")]
|
||||||
|
/// Display your or another user's account creation date
|
||||||
|
pub async fn account_age(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Selected user"] user: Option<serenity::User>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let user = user.as_ref().unwrap_or_else(|| ctx.author());
|
||||||
|
|
||||||
|
let uid = *user.id.as_u64();
|
||||||
|
|
||||||
|
let unix_timecode = snowflake_to_unix(u128::from(uid));
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
// this shouldn't be able to break but just in case I'm making the `unwrap_or` output NaiveDateTime::MIN
|
||||||
|
let date_time_stamp =
|
||||||
|
NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0).unwrap_or(NaiveDateTime::MIN);
|
||||||
|
|
||||||
|
let age = chrono::Utc::now()
|
||||||
|
.naive_utc()
|
||||||
|
.signed_duration_since(date_time_stamp)
|
||||||
|
.num_days();
|
||||||
|
|
||||||
|
ctx.say(format!(
|
||||||
|
"{}'s account was created at {}.\nSo They are {} days old.",
|
||||||
|
user.name,
|
||||||
|
user.created_at(),
|
||||||
|
age
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the creation date or a Snowflake ID
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(prefix_command, slash_command, category = "Tools")]
|
||||||
|
pub async fn creation_date(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "ID of User/Message/Channel/ect"] snowflake_id: u128,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let unix_timecode = snowflake_to_unix(snowflake_id);
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
// this shouldn't be able to break but just in case I'm making the `unwrap_or` output NaiveDateTime::MIN
|
||||||
|
let date_time_stamp =
|
||||||
|
NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0).unwrap_or(NaiveDateTime::MIN);
|
||||||
|
|
||||||
|
ctx.say(format!("Created/Joined on {date_time_stamp}"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// qmit
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(owners_only, slash_command, hide_in_help)]
|
||||||
|
pub async fn bot_owner_tool_1(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
ctx.defer_ephemeral().await?;
|
||||||
|
|
||||||
|
let guild_list = ctx.serenity_context().cache.guilds();
|
||||||
|
|
||||||
|
let mut invites: Vec<RichInvite> = Vec::new();
|
||||||
|
|
||||||
|
for guild in guild_list {
|
||||||
|
let guild_invites: Option<Vec<RichInvite>> = (guild.invites(ctx).await).ok();
|
||||||
|
|
||||||
|
if guild_invites.clone().is_some() {
|
||||||
|
invites.append(&mut guild_invites.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// let shit_list = format!("All invites the bot can see:\n\n{:?}", invites);
|
||||||
|
|
||||||
|
let mut new_list: String = "Every invite the bot can see, grouped by guild:\n\n[\n".to_string();
|
||||||
|
|
||||||
|
for invite in invites {
|
||||||
|
new_list.push_str(format!("{},\n", serde_json::to_string(&invite)?).as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
new_list.push(']');
|
||||||
|
|
||||||
|
ctx.send(|b| {
|
||||||
|
b.content("All bot invites:".to_string())
|
||||||
|
.attachment(AttachmentType::Bytes {
|
||||||
|
data: std::borrow::Cow::Borrowed(new_list.as_bytes()),
|
||||||
|
filename: format!("{}_invites.txt", ctx.id()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get's all avaliable info from a Discord Invite
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(
|
||||||
|
prefix_command,
|
||||||
|
slash_command,
|
||||||
|
category = "Tools",
|
||||||
|
member_cooldown = 5,
|
||||||
|
check = "guild_auth_check",
|
||||||
|
guild_only
|
||||||
|
)]
|
||||||
|
pub async fn invite_info(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Invite URL"] invite_url: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
use linkify::LinkFinder;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
struct InviteObject {
|
||||||
|
#[serde(rename(deserialize = "type"))]
|
||||||
|
_type: u64,
|
||||||
|
code: String,
|
||||||
|
inviter: InviterObject,
|
||||||
|
expires_at: Option<String>,
|
||||||
|
guild: PartialGuild,
|
||||||
|
guild_id: String,
|
||||||
|
channel: PartialChannel,
|
||||||
|
approximate_member_count: u64,
|
||||||
|
approximate_presence_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
struct InviterObject {
|
||||||
|
id: String,
|
||||||
|
username: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
avatar: Option<String>,
|
||||||
|
discriminator: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
public_flags: u64,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
flags: u64,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
banner: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
accent_color: Option<u64>,
|
||||||
|
global_name: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
avatar_decoration_data: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
banner_color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
struct PartialGuild {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
splash: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
banner: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
icon: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
features: Vec<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
verification_level: u64,
|
||||||
|
vanity_url_code: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
nsfw_level: u64,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
nsfw: bool,
|
||||||
|
premium_subscription_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
struct PartialChannel {
|
||||||
|
id: String,
|
||||||
|
#[serde(rename(deserialize = "type"))]
|
||||||
|
_type: u64,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let finder = LinkFinder::new();
|
||||||
|
let links: Vec<_> = finder.links(&invite_url).collect();
|
||||||
|
|
||||||
|
if links.is_empty() {
|
||||||
|
ctx.say("No valid links found").await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let link_str = links[0].as_str().to_owned();
|
||||||
|
|
||||||
|
let (_link, invite_code) = link_str.split_at(19);
|
||||||
|
|
||||||
|
let response = reqwest::get(format!(
|
||||||
|
"https://discord.com/api/v10/invites/{invite_code}?with_counts=true"
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
let response_formatted: Option<InviteObject> = response.json().await?;
|
||||||
|
|
||||||
|
if response_formatted.is_none() {
|
||||||
|
ctx.say("Invite not found").await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let invite = response_formatted.unwrap();
|
||||||
|
|
||||||
|
let invite_info_fields = vec![
|
||||||
|
("Code:", invite.code, false),
|
||||||
|
("Expires:", invite.expires_at.unwrap_or_default(), false),
|
||||||
|
("Destination channel name:", invite.channel.name, false),
|
||||||
|
("Destination channel ID:", invite.channel.id, false),
|
||||||
|
];
|
||||||
|
|
||||||
|
let guild_info_fields = vec![
|
||||||
|
("Server name:", invite.guild.name, false),
|
||||||
|
("Server ID:", invite.guild_id, false),
|
||||||
|
(
|
||||||
|
"Server Description:",
|
||||||
|
invite.guild.description.unwrap_or_default(),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Vanity URL code:",
|
||||||
|
invite.guild.vanity_url_code.unwrap_or_default(),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Server boosts count:",
|
||||||
|
format!("{}", invite.guild.premium_subscription_count),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Approx member count:",
|
||||||
|
format!("{}", invite.approximate_member_count),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Approx online user count:",
|
||||||
|
format!("{}", invite.approximate_presence_count),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let inviter_info_fields = vec![
|
||||||
|
("Username:", invite.inviter.username, false),
|
||||||
|
(
|
||||||
|
"Global username:",
|
||||||
|
invite.inviter.global_name.unwrap_or_default(),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
("User ID:", invite.inviter.id.clone(), false),
|
||||||
|
(
|
||||||
|
"Discriminator(Eg: #0001):",
|
||||||
|
invite.inviter.discriminator.unwrap_or_default(),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
ctx.send(|f| {
|
||||||
|
f.embed(|e| e.title("Invite Info").fields(invite_info_fields))
|
||||||
|
.embed(|e| e.title("Guild Info").fields(guild_info_fields))
|
||||||
|
.embed(|e| e.title("Inviter Info").fields(inviter_info_fields))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let unix_timecode = snowflake_to_unix(u128::from(ctx.author().id.0));
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
// this shouldn't be able to break but just in case I'm making the `unwrap_or` output NaiveDateTime::MIN
|
||||||
|
let date_time_stamp =
|
||||||
|
NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0).unwrap_or(NaiveDateTime::MIN);
|
||||||
|
|
||||||
|
let age = chrono::Utc::now()
|
||||||
|
.naive_utc()
|
||||||
|
.signed_duration_since(date_time_stamp)
|
||||||
|
.num_days();
|
||||||
|
|
||||||
|
let is_user_in_db: Option<String> =
|
||||||
|
check_username_against_db(invite.inviter.id.parse::<u64>().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: set your own channel ID!
|
||||||
|
|
||||||
|
// log user name, id, guild name, id and url to channel
|
||||||
|
serenity::ChannelId()
|
||||||
|
.send_message(ctx, |f| {
|
||||||
|
f.embed(|e| {
|
||||||
|
e.title("User requested invite info")
|
||||||
|
.field("Username", ctx.author().name.clone(), true)
|
||||||
|
.field("User ID", ctx.author().id.0.to_string(), true)
|
||||||
|
.field("User Account age (days)", age, true)
|
||||||
|
.field("Source Server Name", ctx.guild().unwrap().name, true)
|
||||||
|
.field(
|
||||||
|
"Source Server ID",
|
||||||
|
ctx.guild().unwrap().id.0.to_string(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.field("Url provided", link_str, true)
|
||||||
|
.field(
|
||||||
|
"Is User in DB",
|
||||||
|
format!("{}", is_user_in_db.is_some()),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
36
src/lib/args.rs
Normal file
36
src/lib/args.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use crate::enums::{DebugLevel, LogDebugLevel};
|
||||||
|
use crate::vars::BOT_TOKEN;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
/// This is where CLI args are set
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
pub struct Args {
|
||||||
|
/// Discord bot token
|
||||||
|
#[clap(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
default_value = BOT_TOKEN
|
||||||
|
)]
|
||||||
|
pub token: String,
|
||||||
|
|
||||||
|
/// Command prefix for message commands
|
||||||
|
#[clap(short, long, default_value = "`")]
|
||||||
|
pub prefix: String,
|
||||||
|
|
||||||
|
/// Output extra information in discord reply errors
|
||||||
|
#[clap(short, long)]
|
||||||
|
pub verbose: bool,
|
||||||
|
|
||||||
|
/// Print out list of guilds in cache on startup
|
||||||
|
#[clap(long("gpc"))]
|
||||||
|
pub print_guild_cache: bool,
|
||||||
|
|
||||||
|
/// emit debug information to both stdout and a file
|
||||||
|
#[clap(value_enum, long, default_value = "most")]
|
||||||
|
pub debug: DebugLevel,
|
||||||
|
|
||||||
|
/// emit debug information to both stdout and a file
|
||||||
|
#[clap(value_enum, long, default_value = "most")]
|
||||||
|
pub debug_log: LogDebugLevel,
|
||||||
|
}
|
100
src/lib/checks.rs
Normal file
100
src/lib/checks.rs
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
use crate::types::{Context, Error};
|
||||||
|
use crate::vars::BOT_ADMINS;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
/// Check if command user is in the `BOT_ADMINS` list
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will never return an error.
|
||||||
|
#[allow(clippy::unused_async, clippy::missing_errors_doc)] // async is used by command checks but clippy can't tell
|
||||||
|
pub async fn bot_admin_check(ctx: Context<'_>) -> Result<bool, Error> {
|
||||||
|
// ? The bellow commented out code is for quick testing, automatic fails on my ID
|
||||||
|
// match ctx.author().id.as_u64() {
|
||||||
|
// 383507911160233985 => Ok(false),
|
||||||
|
// _ => {
|
||||||
|
// match BOT_ADMINS.contains(ctx.author().id.as_u64()) {
|
||||||
|
// true => Ok(true),
|
||||||
|
// false => Ok(false),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
if BOT_ADMINS.contains(ctx.author().id.as_u64()) {
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ? This might not be needed, I thinik it's a left over from before we dud guild based authing
|
||||||
|
// ! Remove the _ if put into use!
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.1.12",
|
||||||
|
note = "left over from before we dud guild based auth"
|
||||||
|
)]
|
||||||
|
#[allow(clippy::unused_async, clippy::missing_errors_doc)] // no need to lint dead code
|
||||||
|
pub async fn _bot_auth_check(_ctx: Context<'_>) -> Result<bool, Error> {
|
||||||
|
// if let Ok(res) = bot_admin_check(ctx).await {
|
||||||
|
// if res {
|
||||||
|
// return Ok(true);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let mut con = open_redis_connection().await?;
|
||||||
|
|
||||||
|
// let key_list: HashSet<u64> = redis::cmd("SMEMBERS")
|
||||||
|
// .arg("user-lists:authorised-users")
|
||||||
|
// .clone()
|
||||||
|
// .query_async(&mut con)
|
||||||
|
// .await?;
|
||||||
|
|
||||||
|
// if key_list.contains(ctx.author().id.as_u64()) {
|
||||||
|
// Ok(true)
|
||||||
|
// } else {
|
||||||
|
// ctx.say("You are not authorized to use this command! Please contact a bot admin or Azuki!")
|
||||||
|
// .await?;
|
||||||
|
// Ok(false)
|
||||||
|
// }
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a user is authorised to use the bot in the current server
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if unable to connet to or query DB.
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
pub async fn guild_auth_check(ctx: Context<'_>) -> Result<bool, Error> {
|
||||||
|
use crate::utils::open_redis_connection;
|
||||||
|
|
||||||
|
if let Ok(res) = bot_admin_check(ctx).await {
|
||||||
|
if res {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut con = open_redis_connection().await?;
|
||||||
|
|
||||||
|
let key_list: Option<HashSet<String>> = redis::cmd("SMEMBERS")
|
||||||
|
.arg(format!(
|
||||||
|
"authed-server-users:{}",
|
||||||
|
ctx.guild_id()
|
||||||
|
.unwrap_or(poise::serenity_prelude::GuildId(0))
|
||||||
|
.as_u64()
|
||||||
|
))
|
||||||
|
.clone()
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match key_list {
|
||||||
|
None => Ok(false),
|
||||||
|
Some(list) if !list.contains(&format!("{}", ctx.author().id.as_u64())) => {
|
||||||
|
Ok({
|
||||||
|
ctx.say("You are not authorized to use this command! Please contact a bot admin or Azuki!").await?;
|
||||||
|
false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Some(_list) => Ok(true),
|
||||||
|
}
|
||||||
|
}
|
57
src/lib/enums.rs
Normal file
57
src/lib/enums.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use poise::serenity_prelude::{self as serenity};
|
||||||
|
|
||||||
|
#[derive(Debug, poise::ChoiceParameter)]
|
||||||
|
pub enum WaifuTypes {
|
||||||
|
Neko,
|
||||||
|
Megumin,
|
||||||
|
Bully,
|
||||||
|
Cuddle,
|
||||||
|
Cry,
|
||||||
|
Kiss,
|
||||||
|
Lick,
|
||||||
|
Pat,
|
||||||
|
Smug,
|
||||||
|
Bonk,
|
||||||
|
Blush,
|
||||||
|
Smile,
|
||||||
|
Wave,
|
||||||
|
Highfive,
|
||||||
|
Handhold,
|
||||||
|
Nom,
|
||||||
|
Bite,
|
||||||
|
Glomp,
|
||||||
|
Slap,
|
||||||
|
Kill,
|
||||||
|
Happy,
|
||||||
|
Wink,
|
||||||
|
Poke,
|
||||||
|
Dance,
|
||||||
|
Cringe,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, clap::ValueEnum)]
|
||||||
|
pub enum DebugLevel {
|
||||||
|
Off,
|
||||||
|
Some,
|
||||||
|
Most,
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebugLevel {
|
||||||
|
#[must_use]
|
||||||
|
pub fn enabled(&self) -> bool {
|
||||||
|
*self != Self::Off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, clap::ValueEnum)]
|
||||||
|
pub enum LogDebugLevel {
|
||||||
|
Most,
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CloseTicketFail {
|
||||||
|
False,
|
||||||
|
IncorrectCategory,
|
||||||
|
SerenityError(serenity::Error),
|
||||||
|
}
|
574
src/lib/event_handlers.rs
Normal file
574
src/lib/event_handlers.rs
Normal file
|
@ -0,0 +1,574 @@
|
||||||
|
use crate::structs::{GuildSettings, UserInfo, WaybackResponse, WaybackStatus};
|
||||||
|
use crate::utils::snowflake_to_unix;
|
||||||
|
use crate::vars::FBT_GUILD_ID;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use chrono::Utc;
|
||||||
|
use chrono_tz::Australia::Melbourne;
|
||||||
|
use colored::Colorize;
|
||||||
|
use poise::serenity_prelude::{self as serenity, ChannelId, Colour, MessageUpdateEvent};
|
||||||
|
use rand::Rng;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tracing::{event, Level};
|
||||||
|
|
||||||
|
// TODO: Change to the ID of a channel you want all DMs sent to the bot to be relayed to
|
||||||
|
const DM_CHANNEL_ID: u64 = 0000000000000000000;
|
||||||
|
|
||||||
|
/// If enabled on a server it will warn them on black listed users joining
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if unable to parse channel ID from DB to u64.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if;
|
||||||
|
/// - Fails to contact redis DB.
|
||||||
|
/// - Fails to get guild settings from DB.
|
||||||
|
/// - Fails to ask Redis for coresponding DB entry for user.
|
||||||
|
/// - Fails to send message to channel.
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
pub async fn bl_warner(
|
||||||
|
ctx: &serenity::Context,
|
||||||
|
member: &serenity::Member,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
use crate::utils::open_redis_connection;
|
||||||
|
|
||||||
|
let mut con = open_redis_connection().await?;
|
||||||
|
|
||||||
|
let guild_settings_json_in: Option<String> = redis::cmd("JSON.GET")
|
||||||
|
.arg(format!("guild-settings:{}", member.guild_id.as_u64()))
|
||||||
|
.clone()
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let if_on_bl: Option<String> = redis::cmd("JSON.GET")
|
||||||
|
.arg(format!("user:{}", member.user.id.as_u64()))
|
||||||
|
.clone()
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match if_on_bl {
|
||||||
|
None => {}
|
||||||
|
Some(user_json) => {
|
||||||
|
match guild_settings_json_in {
|
||||||
|
None => {} // Do nothing
|
||||||
|
// Check guild settings
|
||||||
|
Some(server_json) => {
|
||||||
|
let settings: GuildSettings = serde_json::from_str(&server_json)?;
|
||||||
|
let user: UserInfo = serde_json::from_str(&user_json)?;
|
||||||
|
|
||||||
|
ChannelId::from(settings.channel_id.parse::<u64>().unwrap())
|
||||||
|
.say(
|
||||||
|
ctx,
|
||||||
|
format!(
|
||||||
|
"<@{}>/{0} Just joined your server with {} offenses on record",
|
||||||
|
user.discord_id.unwrap(),
|
||||||
|
user.offences.len()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if server has alt protection enabled and then kicks the new member if they are >90 days old
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if;
|
||||||
|
/// - Fails to connect to Redis DB.
|
||||||
|
/// - Fails to serde guild settings json to `GuildSettings` struct.
|
||||||
|
/// - Fails to send DM to user getting kicked.
|
||||||
|
/// - Fails to actually kick member.
|
||||||
|
///
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
pub async fn alt_kicker(
|
||||||
|
ctx: &serenity::Context,
|
||||||
|
member: &serenity::Member,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
use crate::utils::open_redis_connection;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
let mut con = open_redis_connection().await?;
|
||||||
|
|
||||||
|
let whitelist: HashSet<String> = redis::cmd("SMEMBERS")
|
||||||
|
.arg("kick-whitelist")
|
||||||
|
.clone()
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if whitelist.contains(&member.user.id.0.to_string()) {
|
||||||
|
return Ok(()); // Don't kick whitelisted users
|
||||||
|
}
|
||||||
|
|
||||||
|
let guild_settings_json_in: Option<String> = redis::cmd("JSON.GET")
|
||||||
|
.arg(format!("guild-settings:{}", member.guild_id.as_u64()))
|
||||||
|
.clone()
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match guild_settings_json_in {
|
||||||
|
None => {} // Do nothing
|
||||||
|
// Check guild settings
|
||||||
|
Some(json_in) => {
|
||||||
|
let settings: GuildSettings = serde_json::from_str(&json_in)?;
|
||||||
|
// Is kicking enabled?
|
||||||
|
if settings.kick {
|
||||||
|
let uid = *member.user.id.as_u64();
|
||||||
|
|
||||||
|
// Trying to handle the pfp here to see if it catches more or maybe most alts really do have the same pfp
|
||||||
|
let pfp = member
|
||||||
|
.avatar_url()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
"https://discord.com/assets/1f0bfc0865d324c2587920a7d80c609b.png"
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let unix_timecode = snowflake_to_unix(u128::from(uid));
|
||||||
|
|
||||||
|
#[allow(clippy::pedantic)]
|
||||||
|
// it literally only take's i64, no need to warn about truncation here.
|
||||||
|
let date_time_stamp = NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0)
|
||||||
|
.unwrap_or(NaiveDateTime::MIN);
|
||||||
|
|
||||||
|
let age = chrono::Utc::now()
|
||||||
|
.naive_utc()
|
||||||
|
.signed_duration_since(date_time_stamp)
|
||||||
|
.num_days();
|
||||||
|
|
||||||
|
// Compare user age
|
||||||
|
if !age.ge(&90_i64) {
|
||||||
|
member.user.direct_message(ctx.http.clone(), |f| {
|
||||||
|
f.content("It looks like your account is under 90 days old, or has been detected as a potential alt. You have been kick from the server!\nYou have not been banned, feel free to join back when your account is over 90 days old.\nRun the `about` slash command or send `help in this chat to find out more.")
|
||||||
|
}).await?;
|
||||||
|
member
|
||||||
|
.kick_with_reason(
|
||||||
|
ctx.http.clone(),
|
||||||
|
&format!("Potential alt detected, account was {age:.0} day(s) old"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let colour = &mut rand::thread_rng().gen_range(0..10_000_000);
|
||||||
|
|
||||||
|
ChannelId(settings.channel_id.parse::<u64>()?)
|
||||||
|
.send_message(ctx.http.clone(), |f| {
|
||||||
|
f.embed(|e| {
|
||||||
|
e.title("Alt kicked!")
|
||||||
|
.description(format!(
|
||||||
|
"Potential alt detected, account was {:.0} day(s) old",
|
||||||
|
age
|
||||||
|
))
|
||||||
|
.thumbnail(pfp)
|
||||||
|
.field("User ID", uid, true)
|
||||||
|
.field("Name", member.user.name.clone(), true)
|
||||||
|
.color(Colour::new(*colour))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends all recieved DMs into a specified channel
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if;
|
||||||
|
/// - Fails to handle message attachments.
|
||||||
|
/// - Fails to handle message stickers.
|
||||||
|
/// - Fails to send request to wayback machine.
|
||||||
|
/// - Fails to send message to DM channel.
|
||||||
|
// TODO: Handle attachments, list of links?
|
||||||
|
pub async fn handle_dms(
|
||||||
|
event: &serenity::Message,
|
||||||
|
ctx: &serenity::Context,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
if !event.author.bot {
|
||||||
|
let message = event.clone();
|
||||||
|
let uid = *message.author.id.as_u64();
|
||||||
|
|
||||||
|
let icon = message.author.avatar_url().map_or_else(
|
||||||
|
|| "https://discord.com/assets/1f0bfc0865d324c2587920a7d80c609b.png".to_string(),
|
||||||
|
|url| url,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cache = ctx.http.clone();
|
||||||
|
|
||||||
|
let colour = &mut rand::thread_rng().gen_range(0..10_000_000);
|
||||||
|
|
||||||
|
let now = Utc::now().with_timezone(&Melbourne);
|
||||||
|
|
||||||
|
let local_time = now.to_string();
|
||||||
|
|
||||||
|
let timestamp = local_time.to_string();
|
||||||
|
|
||||||
|
let mut wayback_job_ids = Vec::new();
|
||||||
|
|
||||||
|
let list_of_files = if message.attachments.is_empty() | message.sticker_items.is_empty() {
|
||||||
|
"N/A".to_string()
|
||||||
|
} else {
|
||||||
|
let mut urls = Vec::new();
|
||||||
|
|
||||||
|
handle_files(&message, &mut wayback_job_ids, &mut urls).await?;
|
||||||
|
|
||||||
|
// Duped code for stickers, could probably refactor into function
|
||||||
|
handle_stickers(&message, ctx, &mut wayback_job_ids, &mut urls).await?;
|
||||||
|
|
||||||
|
urls.join("\n \n")
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut msg = ChannelId(DM_CHANNEL_ID)
|
||||||
|
.send_message(cache, |f| {
|
||||||
|
f.embed(|e| {
|
||||||
|
e.title("New message:")
|
||||||
|
.description(message.content.clone())
|
||||||
|
.field("Attachments/Stickers:", list_of_files.clone(), false)
|
||||||
|
.field("User ID", uid, false)
|
||||||
|
.field("Recieved at:", timestamp.clone(), false)
|
||||||
|
.author(|a| a.icon_url(icon.clone()).name(message.author.name.clone()))
|
||||||
|
.color(Colour::new(*colour))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut wayback_urls: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
for job in wayback_job_ids {
|
||||||
|
let mut is_not_done = true;
|
||||||
|
while is_not_done {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
// TODO: Change to your own wayback machine authorization key
|
||||||
|
let response = client
|
||||||
|
.get(format!("https://web.archive.org/save/status/{job}"))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", "LOW asdgasdg:fasfaf") // auth key here!!
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let response_content = response.text().await?;
|
||||||
|
let wayback_status: WaybackStatus = serde_json::from_str(&response_content)?;
|
||||||
|
|
||||||
|
if wayback_status.status == *"success" {
|
||||||
|
wayback_urls.push(format!(
|
||||||
|
"https://web.archive.org/web/{}",
|
||||||
|
wayback_status.original_url.unwrap_or_else(|| {
|
||||||
|
"20220901093722/https://www.dafk.net/what/".to_string()
|
||||||
|
})
|
||||||
|
));
|
||||||
|
is_not_done = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !wayback_urls.is_empty() {
|
||||||
|
msg.edit(ctx, |f| {
|
||||||
|
f.embed(|e| {
|
||||||
|
e.title("New message:")
|
||||||
|
.description(message.content.clone())
|
||||||
|
.field("Attachments/Stickers:", list_of_files, false)
|
||||||
|
.field(
|
||||||
|
"Archived Attachments/Stickers:",
|
||||||
|
wayback_urls.join("\n \n"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.field("User ID", uid, false)
|
||||||
|
.field("Recieved at:", timestamp, false)
|
||||||
|
.author(|a| a.icon_url(icon).name(message.author.name.clone()))
|
||||||
|
.color(Colour::new(*colour))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles DM files.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if Failes to contact wayback machine.
|
||||||
|
async fn handle_files(
|
||||||
|
message: &serenity::Message,
|
||||||
|
wayback_job_ids: &mut Vec<String>,
|
||||||
|
urls: &mut Vec<String>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
for file in message.attachments.clone() {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let mut params = HashMap::new();
|
||||||
|
params.insert("url".to_string(), file.url.clone());
|
||||||
|
params.insert("skip_first_archive".to_string(), "1".to_string());
|
||||||
|
|
||||||
|
// TODO: Change to your own wayback machine authorization key
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post("https://web.archive.org/save")
|
||||||
|
.form(¶ms)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", "LOW asdgasdg:fasfaf")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response_content = response.text().await?;
|
||||||
|
let wayback_status: WaybackResponse = serde_json::from_str(&response_content)?;
|
||||||
|
|
||||||
|
if wayback_status.status.is_none() {
|
||||||
|
if let Some(jid) = wayback_status.job_id {
|
||||||
|
wayback_job_ids.push(jid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
urls.push(file.url);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles DM stickers.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if Failes to contact wayback machine.
|
||||||
|
async fn handle_stickers(
|
||||||
|
message: &serenity::Message,
|
||||||
|
ctx: &serenity::Context,
|
||||||
|
wayback_job_ids: &mut Vec<String>,
|
||||||
|
urls: &mut Vec<String>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
for file in message.sticker_items.clone() {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let mut params = HashMap::new();
|
||||||
|
params.insert(
|
||||||
|
"url".to_string(),
|
||||||
|
file.to_sticker(ctx)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.image_url()
|
||||||
|
.unwrap()
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
params.insert("skip_first_archive".to_string(), "1".to_string());
|
||||||
|
|
||||||
|
// TODO: Change to your own wayback machine authorization key
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post("https://web.archive.org/save")
|
||||||
|
.form(¶ms)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", "LOW asdgasdg:fasfaf")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response_content = response.text().await?;
|
||||||
|
let wayback_status: WaybackResponse = serde_json::from_str(&response_content)?;
|
||||||
|
|
||||||
|
match wayback_status.status {
|
||||||
|
None => {
|
||||||
|
if let Some(jid) = wayback_status.job_id {
|
||||||
|
wayback_job_ids.push(jid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
urls.push(file.to_sticker(ctx).await.unwrap().image_url().unwrap());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When a message is edited in FBT this function will send the new and old message to a specified channel.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if an author doesn't exist, should be unreachable.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if the message fails to send.
|
||||||
|
pub async fn handle_msg_edit(
|
||||||
|
event: MessageUpdateEvent,
|
||||||
|
old_if_available: &Option<serenity::Message>,
|
||||||
|
ctx: &serenity::Context,
|
||||||
|
new: &Option<serenity::Message>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
if event.guild_id.is_some() {
|
||||||
|
if let Some(author) = event.author.clone() {
|
||||||
|
if !author.bot {
|
||||||
|
let old_message = old_if_available.as_ref().map_or_else(
|
||||||
|
|| "Message not stored in cache :(".to_string(),
|
||||||
|
|msg| msg.content.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_message = new.as_ref().map_or_else(
|
||||||
|
|| "Message not stored in cache :(".to_string(),
|
||||||
|
|msg| msg.content.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let message_url = new.as_ref().map_or_else(
|
||||||
|
|| "URL stored in cache :(".to_string(),
|
||||||
|
poise::serenity_prelude::Message::link,
|
||||||
|
);
|
||||||
|
|
||||||
|
let current_time = Utc::now().with_timezone(&Melbourne);
|
||||||
|
|
||||||
|
let local_time = current_time.to_string();
|
||||||
|
|
||||||
|
let timestamp = local_time.to_string();
|
||||||
|
|
||||||
|
// TODO: channel to alert you that a message has been deleted
|
||||||
|
|
||||||
|
ChannelId(891_294_507_923_025_951)
|
||||||
|
.send_message(ctx.http.clone(), |f| {
|
||||||
|
f.embed(|e| {
|
||||||
|
e.title(format!(
|
||||||
|
"\"{}\" Edited a message",
|
||||||
|
event.author.clone().unwrap().tag()
|
||||||
|
))
|
||||||
|
.field("Old message content:", old_message, false)
|
||||||
|
.field("New message content:", new_message, false)
|
||||||
|
.field("Link:", message_url, false)
|
||||||
|
.field("Edited at:", timestamp, false)
|
||||||
|
.footer(|f| {
|
||||||
|
f.text(format!(
|
||||||
|
"User ID: {}",
|
||||||
|
event.author.clone().unwrap().id.as_u64()
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.color(Colour::new(0x00FA_A81A))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles messages that have been deleted
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if there is no message object in cache.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if unable to send message to channel.
|
||||||
|
pub async fn handle_msg_delete(
|
||||||
|
guild_id: &Option<serenity::GuildId>,
|
||||||
|
ctx: &serenity::Context,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
deleted_message_id: &serenity::MessageId,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
match guild_id {
|
||||||
|
None => {}
|
||||||
|
Some(gid) => {
|
||||||
|
// TODO: this logs any delted message in FBT specifically, change to your own server ID
|
||||||
|
if *gid.as_u64() == 737_168_134_502_350_849 {
|
||||||
|
match ctx.cache.message(channel_id, deleted_message_id) {
|
||||||
|
None => {}
|
||||||
|
Some(msg) => {
|
||||||
|
if !msg.author.bot {
|
||||||
|
let message = match ctx.cache.message(channel_id, deleted_message_id) {
|
||||||
|
None => "Message not stored in cache :(".to_string(),
|
||||||
|
Some(msg) => format!("{:?}", msg.content),
|
||||||
|
};
|
||||||
|
|
||||||
|
let author_id = match message.as_str() {
|
||||||
|
"Message not stored in cache :(" => 0_u64,
|
||||||
|
_ => *ctx
|
||||||
|
.cache
|
||||||
|
.message(channel_id, deleted_message_id)
|
||||||
|
.unwrap()
|
||||||
|
.author
|
||||||
|
.id
|
||||||
|
.as_u64(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let author_tag =
|
||||||
|
if message.clone().as_str() == "Message not stored in cache :(" {
|
||||||
|
"Not in cache#000".to_string()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{:?}",
|
||||||
|
match ctx.cache.message(channel_id, deleted_message_id) {
|
||||||
|
Some(msg) => {
|
||||||
|
msg.author.tag()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
String::new() // This just creates ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = Utc::now().with_timezone(&Melbourne);
|
||||||
|
|
||||||
|
let local_time = now.to_string();
|
||||||
|
|
||||||
|
let timestamp = local_time.to_string();
|
||||||
|
|
||||||
|
// TODO: This is the channel the deleted messages are sent to
|
||||||
|
|
||||||
|
ChannelId(891_294_507_923_025_951)
|
||||||
|
.send_message(ctx.http.clone(), |f| {
|
||||||
|
f.embed(|e| {
|
||||||
|
e.title(format!("{author_tag} deleted a message"))
|
||||||
|
.field("Message content:", message, false)
|
||||||
|
.field("Deleted at:", timestamp, false)
|
||||||
|
.field(
|
||||||
|
"Channel link:",
|
||||||
|
format!(
|
||||||
|
"https://discord.com/channels/{}/{}",
|
||||||
|
guild_id
|
||||||
|
.unwrap_or(serenity::GuildId::from(
|
||||||
|
FBT_GUILD_ID
|
||||||
|
))
|
||||||
|
.as_u64(),
|
||||||
|
channel_id.as_u64()
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.footer(|f| f.text(format!("User ID: {author_id}")))
|
||||||
|
.color(Colour::new(0x00ED_4245))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints message and outputs trace if in verbose mode
|
||||||
|
pub fn handle_resume(event: &serenity::ResumedEvent) {
|
||||||
|
event!(
|
||||||
|
Level::INFO,
|
||||||
|
"ResumedEvent" = format!(
|
||||||
|
"{}",
|
||||||
|
"Bot went offline but is online again".bright_red().italic()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Is this a good idea?
|
||||||
|
event!(
|
||||||
|
Level::TRACE,
|
||||||
|
"ResumedEvent" = format!(
|
||||||
|
"{}",
|
||||||
|
"Bot went offline but is online again".bright_red().italic()
|
||||||
|
),
|
||||||
|
"event" = ?event
|
||||||
|
);
|
||||||
|
}
|
9
src/lib/lib.rs
Normal file
9
src/lib/lib.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
pub mod args;
|
||||||
|
pub mod checks;
|
||||||
|
pub mod enums;
|
||||||
|
pub mod event_handlers;
|
||||||
|
pub mod memes;
|
||||||
|
pub mod structs;
|
||||||
|
pub mod types;
|
||||||
|
pub mod utils;
|
||||||
|
pub mod vars;
|
77
src/lib/memes.rs
Normal file
77
src/lib/memes.rs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
use crate::vars::FBT_GUILD_ID;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use poise::serenity_prelude::{self as serenity};
|
||||||
|
use regex::Regex;
|
||||||
|
use strip_markdown::strip_markdown;
|
||||||
|
|
||||||
|
// Some times maybe good sometimes maybe shit
|
||||||
|
pub static POG_RE: Lazy<Regex> = Lazy::new(|| {
|
||||||
|
Regex::new(
|
||||||
|
r"[pP𝓹⍴𝖕𝔭የ𝕡ק🅟🅿ⓟρᑭ𝙥քp̷þp͎₱ᵽ℘ア𝐩𝒑𝓅p̞̈͑̚͞℘p͓̽ք𝓹ᕶp̶p̳p̅][oO0øØ𝓸᥆𝖔𝔬ዐ𝕠๏🅞🅾ⓞσOoÒօoo̷ðo͎の𝗼ᴏᵒ🇴𝙤ѻⲟᓍӨo͓̽o͟o̲o̅o̳o̶🄾o̯̱̊͊͢Ꭷσℴ𝒐𝐨][gG9𝓰𝖌𝔤𝕘🅖🅶ⓖɠGg𝑔ցg̷gg͎g̲g͟ǥ₲ɢg͓̽Gɠ𝓰𝙜🇬Ꮆᵍɢ𝗴𝐠𝒈𝑔ᧁg𝚐₲Ꮆ𝑔ĝ̽̓̀͑𝘨ງ🄶𝔤ģ]\b",
|
||||||
|
).unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
/// This will read a message and check to see if the message contains the word `pog`
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if regex fails to compile, this should be unreachable unless I acidentally change something before compile time.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if .
|
||||||
|
pub async fn pog_be_gone(
|
||||||
|
new_message: &serenity::Message,
|
||||||
|
ctx: &serenity::Context,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
if !new_message.author.bot && !new_message.content.is_empty() {
|
||||||
|
match new_message.guild(ctx) {
|
||||||
|
None => {} // Probably a DM, do nothing
|
||||||
|
Some(guild) => {
|
||||||
|
if guild.id.as_u64() == &FBT_GUILD_ID {
|
||||||
|
let lowercase_message = new_message.content.to_lowercase();
|
||||||
|
let cleaned_message = strip_markdown(&lowercase_message);
|
||||||
|
|
||||||
|
let words: Vec<&str> = cleaned_message.split(' ').collect();
|
||||||
|
let mut hits: Vec<&str> = Vec::new();
|
||||||
|
|
||||||
|
for word in words {
|
||||||
|
POG_RE.find(word).map_or((), |pog| {
|
||||||
|
hits.push(pog.as_str());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hits.is_empty() {
|
||||||
|
// there is at least 1 pog found
|
||||||
|
if hits.capacity().gt(&10) {
|
||||||
|
new_message
|
||||||
|
.reply(
|
||||||
|
ctx,
|
||||||
|
format!(
|
||||||
|
"Jesus dude, why did you pog {} times?! stop it!",
|
||||||
|
hits.len()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
new_message.reply_mention(ctx, "please refer to the rules and use the term 'poi' instead of 'pog'!").await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod meme_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_regex() {
|
||||||
|
let pog_test = "pog";
|
||||||
|
|
||||||
|
assert!(POG_RE.is_match(pog_test));
|
||||||
|
}
|
||||||
|
}
|
157
src/lib/structs.rs
Normal file
157
src/lib/structs.rs
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
use merge::Merge;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::{As, FromInto};
|
||||||
|
|
||||||
|
// User data, which is stored and accessible in all command invocations
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Data {}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[derive(Debug, Deserialize, PartialEq, Hash, Eq)]
|
||||||
|
pub struct CsvEntry {
|
||||||
|
pub AuthorID: String,
|
||||||
|
pub Author: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Merge, Clone, Hash, Eq)]
|
||||||
|
pub struct UserInfo {
|
||||||
|
#[serde(with = "As::<FromInto<OptionalString2>>")]
|
||||||
|
pub vrc_id: Option<String>,
|
||||||
|
#[serde(with = "As::<FromInto<OptionalString>>")]
|
||||||
|
pub username: Option<String>,
|
||||||
|
#[serde(with = "As::<FromInto<OptionalString2>>")]
|
||||||
|
pub discord_id: Option<String>,
|
||||||
|
#[merge(strategy = merge::vec::append)]
|
||||||
|
pub offences: Vec<Offense>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)]
|
||||||
|
pub struct GuildSettings {
|
||||||
|
pub channel_id: String,
|
||||||
|
pub kick: bool,
|
||||||
|
pub server_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Merge, Hash, Eq)]
|
||||||
|
pub struct Offense {
|
||||||
|
#[merge(skip)]
|
||||||
|
pub guild_id: String,
|
||||||
|
#[merge(skip)]
|
||||||
|
pub reason: String,
|
||||||
|
#[serde(with = "As::<FromInto<OptionalString2>>")]
|
||||||
|
pub image: Option<String>,
|
||||||
|
#[serde(with = "As::<FromInto<OptionalString2>>")]
|
||||||
|
pub extra: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)]
|
||||||
|
pub struct GuildAuthList {
|
||||||
|
pub users: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)]
|
||||||
|
pub struct ClearedUser {
|
||||||
|
pub user_id: String,
|
||||||
|
pub username: String,
|
||||||
|
pub where_found: String,
|
||||||
|
pub reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)]
|
||||||
|
pub struct MonitoredGuildInfo {
|
||||||
|
pub guild_name: String,
|
||||||
|
pub guild_id: String,
|
||||||
|
pub invite_link: String,
|
||||||
|
pub updated: String,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)]
|
||||||
|
pub struct BlacklistHit {
|
||||||
|
pub user_id: String,
|
||||||
|
pub username: String,
|
||||||
|
pub guild_id: String,
|
||||||
|
pub reason: String,
|
||||||
|
pub image: String,
|
||||||
|
pub extra: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WaybackResponse {
|
||||||
|
pub url: Option<String>,
|
||||||
|
#[serde(rename = "job_id")]
|
||||||
|
pub job_id: Option<String>,
|
||||||
|
pub message: Option<String>,
|
||||||
|
pub status: Option<String>,
|
||||||
|
#[serde(rename = "status_ext")]
|
||||||
|
pub status_ext: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WaybackStatus {
|
||||||
|
#[serde(rename = "http_status")]
|
||||||
|
pub http_status: Option<i64>,
|
||||||
|
#[serde(default)]
|
||||||
|
outlinks: Vec<String>,
|
||||||
|
pub timestamp: Option<String>,
|
||||||
|
#[serde(rename = "original_url")]
|
||||||
|
pub original_url: Option<String>,
|
||||||
|
resources: Vec<String>,
|
||||||
|
#[serde(rename = "duration_sec")]
|
||||||
|
pub duration_sec: Option<f64>,
|
||||||
|
pub status: String,
|
||||||
|
#[serde(rename = "job_id")]
|
||||||
|
pub job_id: String,
|
||||||
|
pub counters: Option<Counters>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Counters {
|
||||||
|
pub outlinks: i64,
|
||||||
|
pub embeds: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct OptionalString(pub Option<String>);
|
||||||
|
|
||||||
|
impl From<OptionalString> for Option<String> {
|
||||||
|
fn from(val: OptionalString) -> Self {
|
||||||
|
val.0.map_or_else(|| Some("N/A".to_string()), Some)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<String>> for OptionalString {
|
||||||
|
fn from(val: Option<String>) -> Self {
|
||||||
|
val.map_or_else(|| Self(Some("N/A".to_string())), |s| Self(Some(s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct OptionalString2(pub Option<String>);
|
||||||
|
|
||||||
|
impl From<OptionalString2> for Option<String> {
|
||||||
|
fn from(val: OptionalString2) -> Self {
|
||||||
|
val.0.map_or_else(
|
||||||
|
|| Some("N/A".to_string()),
|
||||||
|
|s| match s.as_str() {
|
||||||
|
"0" => Some("N/A".to_string()),
|
||||||
|
x => Some(x.to_string()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<String>> for OptionalString2 {
|
||||||
|
fn from(val: Option<String>) -> Self {
|
||||||
|
val.map_or_else(|| Self(Some("N/A".to_string())), |s| Self(Some(s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PasteResponse {
|
||||||
|
pub key: String,
|
||||||
|
}
|
4
src/lib/types.rs
Normal file
4
src/lib/types.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
use crate::structs::Data;
|
||||||
|
|
||||||
|
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
pub type Context<'a> = poise::Context<'a, Data, Error>;
|
142
src/lib/utils.rs
Normal file
142
src/lib/utils.rs
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
use crate::args::Args;
|
||||||
|
use crate::structs::GuildSettings;
|
||||||
|
use crate::types::Context;
|
||||||
|
use crate::types::Error;
|
||||||
|
use crate::vars::REDIS_ADDR;
|
||||||
|
use clap::Parser;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
/// Converts a dsicord snowflake to a unix timecode
|
||||||
|
#[must_use]
|
||||||
|
pub const fn snowflake_to_unix(id: u128) -> u128 {
|
||||||
|
const DISCORD_EPOCH: u128 = 1_420_070_400_000;
|
||||||
|
|
||||||
|
((id >> 22) + DISCORD_EPOCH) / 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quickly checks if the verbose flag was used on launch
|
||||||
|
#[must_use]
|
||||||
|
pub fn verbose_mode() -> bool {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
args.verbose
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a tokio redis connection
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[instrument()]
|
||||||
|
pub async fn open_redis_connection() -> Result<redis::aio::Connection, anyhow::Error> {
|
||||||
|
let redis_connection = redis::Client::open(REDIS_ADDR)?
|
||||||
|
.get_tokio_connection()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(redis_connection)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes guild settings to DB
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[instrument(skip(con))]
|
||||||
|
pub async fn set_guild_settings(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
con: &mut redis::aio::Connection,
|
||||||
|
settings: GuildSettings,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let json = serde_json::to_string(&settings).unwrap();
|
||||||
|
|
||||||
|
let mut pipe = redis::pipe();
|
||||||
|
|
||||||
|
pipe.cmd("JSON.SET").arg(&[
|
||||||
|
format!(
|
||||||
|
"guild-settings:{}",
|
||||||
|
ctx.guild_id().expect("Not run inside guild")
|
||||||
|
),
|
||||||
|
"$".to_string(),
|
||||||
|
json,
|
||||||
|
]);
|
||||||
|
|
||||||
|
pipe.atomic().query_async(con).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the user to a server's auth list in the DB
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[instrument(skip(con))]
|
||||||
|
pub async fn auth(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
con: &mut redis::aio::Connection,
|
||||||
|
uid: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
redis::cmd("SADD")
|
||||||
|
.arg(&[
|
||||||
|
format!(
|
||||||
|
"authed-server-users:{}",
|
||||||
|
ctx.guild_id().expect("Not run inside guild")
|
||||||
|
),
|
||||||
|
uid,
|
||||||
|
])
|
||||||
|
.query_async(con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increases the total commands run count in the DB
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[instrument]
|
||||||
|
pub async fn inc_execution_count() -> Result<(), Error> {
|
||||||
|
let mut con = open_redis_connection().await?;
|
||||||
|
|
||||||
|
// increment status:commands-executed in redis DB
|
||||||
|
redis::cmd("INCR")
|
||||||
|
.arg("status:commands-executed")
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[instrument]
|
||||||
|
pub async fn is_uid_valid_user(uid: u64, ctx: &Context<'_>) -> anyhow::Result<bool> {
|
||||||
|
let u_opt: Option<poise::serenity_prelude::User> =
|
||||||
|
match poise::serenity_prelude::UserId::from(uid)
|
||||||
|
.to_user(ctx)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(user) => Some(user),
|
||||||
|
Err(error) => {
|
||||||
|
if verbose_mode() {
|
||||||
|
ctx.say(format!(
|
||||||
|
"ID must be a user ID, make sure you coppied the right one! Error: {:?}",
|
||||||
|
error
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
ctx.say("ID must be a user ID, make sure you coppied the right one!")
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(u_opt.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod utils_tests {
|
||||||
|
|
||||||
|
use crate::utils::{snowflake_to_unix, verbose_mode};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snowflake_unix_test() {
|
||||||
|
assert_eq!(snowflake_to_unix(383_507_911_160_233_985), 1_511_505_811);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verbose_mode_test() {
|
||||||
|
// Inverting output since verbose mode is disabled by default
|
||||||
|
assert!(!verbose_mode());
|
||||||
|
}
|
||||||
|
}
|
90
src/lib/vars.rs
Normal file
90
src/lib/vars.rs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
pub const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
pub const HELP_EXTRA_TEXT: &str = "Find the documentation website at https://fbtsecurity.fbtheaven.com/\nRun the About command to find out more (/about)";
|
||||||
|
|
||||||
|
// TODO: change this list to your own bot admin user IDs
|
||||||
|
|
||||||
|
// You need to increase the number in [u64; X] so rust knows the limit of the array
|
||||||
|
pub const BOT_ADMINS: [u64; 6] = [
|
||||||
|
212_132_817_017_110_528,
|
||||||
|
288_186_677_967_585_280,
|
||||||
|
211_027_317_068_136_448,
|
||||||
|
383_507_911_160_233_985,
|
||||||
|
168_600_506_233_651_201,
|
||||||
|
231_482_341_921_521_664,
|
||||||
|
]; // Azuki, Komi, Xeno, Mojo, Ellie, Wundie
|
||||||
|
|
||||||
|
// TODO: you can mass replace the name of this variable easily
|
||||||
|
// TODO: change to your own guild ID
|
||||||
|
|
||||||
|
pub const FBT_GUILD_ID: u64 = 737_168_134_502_350_849; // FBT's guild ID
|
||||||
|
|
||||||
|
// TODO: this is the channel wehre the feedback command sends it's response for you to read
|
||||||
|
pub const FEEDBACK_CHANNEL_ID: u64 = 925_599_477_283_311_636;
|
||||||
|
|
||||||
|
//pub const FBT_GUILD_ID: u64 = 838658675916275722; // My test server ID
|
||||||
|
|
||||||
|
// TODO: you need your own Redis DB, this is where you put in the login details and adress of the DB
|
||||||
|
// format: "redis://USERNAME:PASSWORD@ADDRESS:PORT/DB_INDEX"
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
pub const REDIS_ADDR: &str =
|
||||||
|
"redis://:ForSureARealRedisPassword@google.com:6379/0";
|
||||||
|
|
||||||
|
// TODO: change to your own Meilisearch address
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
pub const MEILISEARCH_HOST: &str = "http://google.com:7777";
|
||||||
|
|
||||||
|
// TODO: change to your own Meilisearch API key
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
pub const MEILISEARCH_API_KEY: &str = "why-so-strange";
|
||||||
|
|
||||||
|
// TODO: change to your own bot token
|
||||||
|
pub const BOT_TOKEN: &str =
|
||||||
|
"not touching this <3";
|
||||||
|
|
||||||
|
//TODO: these are popular discord bots, used to ignore their messages and stuff
|
||||||
|
// Part of blacklist for now but I should add it as a check to the excel command too
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
pub const BOT_IDS: [u64; 22] = [
|
||||||
|
134_133_271_750_639_616,
|
||||||
|
155_149_108_183_695_360,
|
||||||
|
159_985_870_458_322_944,
|
||||||
|
159_985_870_458_322_944,
|
||||||
|
184_405_311_681_986_560,
|
||||||
|
204_255_221_017_214_977,
|
||||||
|
216_437_513_709_944_832,
|
||||||
|
235_088_799_074_484_224,
|
||||||
|
235_148_962_103_951_360,
|
||||||
|
294_882_584_201_003_009,
|
||||||
|
351_227_880_153_546_754,
|
||||||
|
375_805_687_529_209_857,
|
||||||
|
537_429_661_139_861_504,
|
||||||
|
550_613_223_733_329_920,
|
||||||
|
559_426_966_151_757_824,
|
||||||
|
583_995_825_269_768_211,
|
||||||
|
625_588_618_525_802_507,
|
||||||
|
649_535_344_236_167_212,
|
||||||
|
743_269_383_438_073_856,
|
||||||
|
743_269_383_438_073_856,
|
||||||
|
887_914_294_988_140_565,
|
||||||
|
935_372_708_089_315_369,
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO: this is for the ticket system, change to your own ticket category ID.
|
||||||
|
// it creates new threads in TICKET_CATEGORY and moves them to CLOSED_TICKET_CATEGORY once closed
|
||||||
|
pub const TICKET_CATEGORY: u64 = 982_769_870_259_240_981;
|
||||||
|
pub const CLOSED_TICKET_CATEGORY: u64 = 983_228_142_107_918_336;
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
#[derive(Debug, poise::ChoiceParameter)]
|
||||||
|
pub enum BlacklistOutput {
|
||||||
|
#[name = "Chat - Output resulting @, ID and Reasons to chat"]
|
||||||
|
Chat,
|
||||||
|
#[name = "Compact Chat - Only send resulting @ and IDs"]
|
||||||
|
CompactChat,
|
||||||
|
#[name = "CSV - Output all relevant info as a single .csv file"]
|
||||||
|
Csv,
|
||||||
|
#[name = "Json - Output all relevant info as a single .json file"]
|
||||||
|
Json,
|
||||||
|
}
|
464
src/main.rs
Normal file
464
src/main.rs
Normal file
|
@ -0,0 +1,464 @@
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
//! Please increade the version in the Cargo.toml file by 0.0.1 for &&every minor commit or command and by 0.1.0 for any majoy function rewrite or implamentation
|
||||||
|
|
||||||
|
// TODO: Add ticket ssytem
|
||||||
|
// ? /close_ticket could check a DB list to see if it contains the channel ID and if it does then close?
|
||||||
|
// ? If we wan't more info we can store each ticket as a json file and then only close if an entry for the channel exists in DB
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use colored::Colorize;
|
||||||
|
use commands::database::remove_guild;
|
||||||
|
use poise::builtins::register_application_commands_buttons;
|
||||||
|
use poise::serenity_prelude::{self as serenity, ChannelId, Colour, UserId};
|
||||||
|
use serenity::model::gateway::Activity;
|
||||||
|
use serenity::model::user::OnlineStatus;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fs::File;
|
||||||
|
use tracing::instrument;
|
||||||
|
use tracing::metadata::LevelFilter;
|
||||||
|
use tracing::{event, Level};
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate maplit;
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
use rusted_fbt_lib::utils::open_redis_connection;
|
||||||
|
use rusted_fbt_lib::vars::FBT_GUILD_ID;
|
||||||
|
// Import everything from the commands folder
|
||||||
|
mod commands;
|
||||||
|
use commands::admin::{
|
||||||
|
announcement, authorize, ban, botmsg, request_setup, setup, shutdown, toggle_kick,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
use commands::database::{
|
||||||
|
add, excel, footprint_lookup, key, search, update_search_engine, whitelist,
|
||||||
|
};
|
||||||
|
use commands::fun::{cringe, owo, ping, pog, toxic, waifu};
|
||||||
|
use commands::info::{about, feedback, help};
|
||||||
|
use commands::tickets::{close_ticket, new_ticket};
|
||||||
|
use commands::tools::{account_age, bot_owner_tool_1, creation_date};
|
||||||
|
|
||||||
|
// New rust librabry to never leave this reposity :D
|
||||||
|
use rusted_fbt_lib::args::Args;
|
||||||
|
use rusted_fbt_lib::checks::bot_admin_check;
|
||||||
|
use rusted_fbt_lib::enums::{DebugLevel, LogDebugLevel};
|
||||||
|
use rusted_fbt_lib::event_handlers::{
|
||||||
|
alt_kicker, bl_warner, handle_dms, handle_msg_delete, handle_msg_edit, handle_resume,
|
||||||
|
};
|
||||||
|
use rusted_fbt_lib::memes::pog_be_gone;
|
||||||
|
use rusted_fbt_lib::structs::{Data, PasteResponse};
|
||||||
|
use rusted_fbt_lib::types::{Context, Error};
|
||||||
|
use rusted_fbt_lib::utils::inc_execution_count;
|
||||||
|
|
||||||
|
use crate::commands::tools::invite_info;
|
||||||
|
|
||||||
|
/// Register application commands in this guild or globally
|
||||||
|
///
|
||||||
|
/// Run with no arguments to register in guild, run with argument "global" to register globally.
|
||||||
|
#[instrument(skip(ctx))]
|
||||||
|
#[poise::command(prefix_command, slash_command, hide_in_help, owners_only)]
|
||||||
|
async fn register(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
register_application_commands_buttons(ctx).await?;
|
||||||
|
event!(Level::INFO, "Commandwhere registered");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Custom error handeling
|
||||||
|
#[instrument]
|
||||||
|
async fn on_error(error: poise::FrameworkError<'_, Data, Error>) {
|
||||||
|
// This is our custom error handler
|
||||||
|
// They are many errors that can occur, so we only handle the ones we want to customize
|
||||||
|
// and forward the rest to the default handler
|
||||||
|
match error {
|
||||||
|
// allow unused_variables because we don't use all the variables
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
poise::FrameworkError::Setup {
|
||||||
|
error,
|
||||||
|
framework,
|
||||||
|
data_about_bot,
|
||||||
|
ctx,
|
||||||
|
} => {
|
||||||
|
// Log failed bot setup
|
||||||
|
event!(Level::ERROR, "Bot setup failed" = ?error);
|
||||||
|
}
|
||||||
|
poise::FrameworkError::Command { error, ctx } => {
|
||||||
|
event!(Level::WARN, "Error in command" = ?ctx.command(), "error" = ?error);
|
||||||
|
}
|
||||||
|
poise::FrameworkError::CommandCheckFailed { error, ctx } => {
|
||||||
|
if ctx.command().name.as_str() == "setup" {
|
||||||
|
ctx.send(|m| {
|
||||||
|
m.content("If you can't run this because you don't have the correct permissions then please ask a local admin to run `/request_setup` or `/authorize`!\nAn admin will come and check out your server ASAP after `/request_setup` is executed.")
|
||||||
|
.ephemeral(true)
|
||||||
|
}).await
|
||||||
|
.expect("Failed to tell user about request_setup during error handeling");
|
||||||
|
}
|
||||||
|
|
||||||
|
event!(Level::INFO, "CommandCheckFailed" = ?ctx.command(), "error" = ?error);
|
||||||
|
}
|
||||||
|
poise::FrameworkError::MissingBotPermissions {
|
||||||
|
missing_permissions,
|
||||||
|
ctx,
|
||||||
|
} => {
|
||||||
|
ctx.say(format!("I'm currently missing the follow permission(s) required to execute this command:\n\n```{}```\n\nPlease ask a local server admin to fix this in my bot role!", missing_permissions.get_permission_names().join("\n"))).await
|
||||||
|
.expect("Unable to tell a server what permissions I am missing!");
|
||||||
|
}
|
||||||
|
poise::FrameworkError::CooldownHit {
|
||||||
|
remaining_cooldown,
|
||||||
|
ctx,
|
||||||
|
} => {
|
||||||
|
ctx.send(|m| {
|
||||||
|
m.content(format!(
|
||||||
|
"You are on cooldown try again in {} seconds, moron!",
|
||||||
|
remaining_cooldown.as_secs()
|
||||||
|
))
|
||||||
|
.ephemeral(true)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to meme on someone for running a command while on cooldown");
|
||||||
|
}
|
||||||
|
error => {
|
||||||
|
if let Err(e) = poise::builtins::on_error(error).await {
|
||||||
|
event!(Level::WARN, info = "Error while handling error (ironic)", error = ?e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle events here, should move anything that isn't just println! to a seperate function to avoid the mess that was `on_message` in the python version
|
||||||
|
#[instrument(skip(ctx, _framework, event, _user_data))]
|
||||||
|
async fn event_listener(
|
||||||
|
ctx: &serenity::Context,
|
||||||
|
event: &poise::Event<'_>,
|
||||||
|
_framework: poise::FrameworkContext<'_, Data, Error>,
|
||||||
|
_user_data: &Data,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
match event {
|
||||||
|
poise::Event::Ready { data_about_bot } => {
|
||||||
|
println!(
|
||||||
|
"{} {}{}",
|
||||||
|
"Bot is now online as".color("Purple"),
|
||||||
|
data_about_bot.user.name.bright_cyan().bold().underline(),
|
||||||
|
"!".color("Purple")
|
||||||
|
);
|
||||||
|
|
||||||
|
let activity = Activity::playing(
|
||||||
|
"use /help to see all commands. /request_setup to request extra admin features.",
|
||||||
|
);
|
||||||
|
let status = OnlineStatus::Online;
|
||||||
|
|
||||||
|
// TODO: Store and get from DB so we can change it later and keep it consistent between boots
|
||||||
|
ctx.set_presence(Some(activity), status).await;
|
||||||
|
}
|
||||||
|
poise::Event::Resume { event } => {
|
||||||
|
handle_resume(event);
|
||||||
|
}
|
||||||
|
poise::Event::Message { new_message } => {
|
||||||
|
if new_message.is_private() {
|
||||||
|
handle_dms(new_message, ctx).await?;
|
||||||
|
} else {
|
||||||
|
pog_be_gone(new_message, ctx).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
poise::Event::GuildMemberAddition { new_member } => {
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
alt_kicker(ctx, new_member).await?;
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
bl_warner(ctx, new_member).await?;
|
||||||
|
}
|
||||||
|
poise::Event::MessageDelete {
|
||||||
|
channel_id,
|
||||||
|
deleted_message_id,
|
||||||
|
guild_id,
|
||||||
|
} => {
|
||||||
|
handle_msg_delete(guild_id, ctx, channel_id, deleted_message_id).await?;
|
||||||
|
}
|
||||||
|
poise::Event::MessageUpdate {
|
||||||
|
old_if_available,
|
||||||
|
new,
|
||||||
|
event,
|
||||||
|
} => {
|
||||||
|
if let Some(n) = new.clone() {
|
||||||
|
// TODO: put your own guild ID here, this is for tracking message edits
|
||||||
|
if !n.is_private() && *event.guild_id.unwrap().as_u64() == 737_168_134_502_350_849 {
|
||||||
|
// I need to learn the overall benifit to using `if let`
|
||||||
|
handle_msg_edit(event.clone(), old_if_available, ctx, new).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
poise::Event::ChannelDelete { channel } => {
|
||||||
|
if *channel.guild_id.as_u64() == FBT_GUILD_ID {
|
||||||
|
let messages = ctx.cache.channel_messages_field(channel.id.0, |s| {
|
||||||
|
s.filter_map(|m| {
|
||||||
|
if m.channel_id.0 == channel.id.0 {
|
||||||
|
Some(m.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
// let messages = match channel.messages(ctx, |b| b.limit(100)).await {
|
||||||
|
// Ok(vec) => format!("{:?}", vec),
|
||||||
|
// Err(e) => format!("{:?}", e),
|
||||||
|
// };
|
||||||
|
|
||||||
|
let shit_list = format!(
|
||||||
|
"Stored channel info:\n\n{:?}\n\nLast 100 messages stored in cache:\n\n{:#?}",
|
||||||
|
channel, messages
|
||||||
|
);
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
// TODO: I setup a custom paste bin here uhh you can figure out how to reaplce it or just comment out the poise::Event::ChannelDelete event
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post("https://paste.buymymojo.net/documents")
|
||||||
|
.body(shit_list)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response_content = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| "{'key': 'nope<3'}".to_string());
|
||||||
|
|
||||||
|
let id: PasteResponse = serde_json::from_str(&response_content)?;
|
||||||
|
|
||||||
|
// TODO: channel to alert users that a channel has been deleted
|
||||||
|
|
||||||
|
ChannelId(891_294_507_923_025_951)
|
||||||
|
.send_message(ctx.http.clone(), |f| {
|
||||||
|
f.embed(|e| {
|
||||||
|
e.title("A channel has been deleted".to_string())
|
||||||
|
.field(
|
||||||
|
"The last cached info from the channel:",
|
||||||
|
format!("https://paste.buymymojo.net/{}", id.key),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.color(Colour::new(0x00ED_4245))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
poise::Event::CacheReady { guilds } => {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
if args.print_guild_cache {
|
||||||
|
event!(Level::INFO, "Cache is ready");
|
||||||
|
} else {
|
||||||
|
event!(Level::INFO, info = "Cache is ready", guilds = ?guilds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let console_level = match args.debug {
|
||||||
|
DebugLevel::Off => LevelFilter::ERROR,
|
||||||
|
DebugLevel::Some => LevelFilter::WARN,
|
||||||
|
DebugLevel::Most => LevelFilter::INFO,
|
||||||
|
DebugLevel::All => LevelFilter::TRACE,
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_level = match args.debug_log {
|
||||||
|
LogDebugLevel::Most => LevelFilter::DEBUG,
|
||||||
|
LogDebugLevel::All => LevelFilter::TRACE,
|
||||||
|
};
|
||||||
|
|
||||||
|
let console_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_ansi(true)
|
||||||
|
.with_thread_names(true)
|
||||||
|
.with_target(true)
|
||||||
|
.with_filter(console_level);
|
||||||
|
let file_layer = if args.debug.enabled() {
|
||||||
|
match File::create(
|
||||||
|
std::path::Path::new(&std::env::current_dir().unwrap()).join(format!(
|
||||||
|
"./{}_rusted-fbt.verbose.log",
|
||||||
|
chrono::offset::Local::now().timestamp()
|
||||||
|
)),
|
||||||
|
) {
|
||||||
|
Ok(handle) => {
|
||||||
|
let file_log = tracing_subscriber::fmt::layer()
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_thread_names(true)
|
||||||
|
.with_target(true)
|
||||||
|
.with_writer(handle)
|
||||||
|
.with_filter(file_level);
|
||||||
|
Some(file_log)
|
||||||
|
}
|
||||||
|
Err(why) => {
|
||||||
|
eprintln!("ERROR!: Unable to create log output file: {why:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let info_file_layer = if args.debug.enabled() {
|
||||||
|
match File::create(
|
||||||
|
std::path::Path::new(&std::env::current_dir().unwrap()).join(format!(
|
||||||
|
"./{}_rusted-fbt.info.log",
|
||||||
|
chrono::offset::Local::now().timestamp()
|
||||||
|
)),
|
||||||
|
) {
|
||||||
|
Ok(handle) => {
|
||||||
|
let file_log = tracing_subscriber::fmt::layer()
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_thread_names(true)
|
||||||
|
.with_target(true)
|
||||||
|
.with_writer(handle)
|
||||||
|
.with_filter(LevelFilter::INFO);
|
||||||
|
Some(file_log)
|
||||||
|
}
|
||||||
|
Err(why) => {
|
||||||
|
eprintln!("ERROR!: Unable to create log output file: {why:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(console_layer)
|
||||||
|
.with(file_layer)
|
||||||
|
.with(info_file_layer)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
// TODO: Like bot admins, put your own IDs here
|
||||||
|
|
||||||
|
let bot_owners: HashSet<UserId> = hashset! {
|
||||||
|
UserId::from(212_132_817_017_110_528), // Azuki
|
||||||
|
UserId::from(164_694_510_947_794_944), // Cross
|
||||||
|
UserId::from(383_507_911_160_233_985), // Mojo
|
||||||
|
};
|
||||||
|
|
||||||
|
// * This is where we put the functions that we want in discord
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut discord_commands = vec![
|
||||||
|
about(),
|
||||||
|
account_age(),
|
||||||
|
ban(),
|
||||||
|
botmsg(),
|
||||||
|
creation_date(),
|
||||||
|
cringe(),
|
||||||
|
help(),
|
||||||
|
owo(),
|
||||||
|
ping(),
|
||||||
|
pog(),
|
||||||
|
register(),
|
||||||
|
shutdown(),
|
||||||
|
toxic(),
|
||||||
|
waifu(),
|
||||||
|
new_ticket(),
|
||||||
|
close_ticket(),
|
||||||
|
bot_owner_tool_1(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// * Any command that requires the DB goes here
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
{
|
||||||
|
let mut db_vec = vec![
|
||||||
|
add(),
|
||||||
|
announcement(),
|
||||||
|
authorize(),
|
||||||
|
footprint_lookup(),
|
||||||
|
excel(),
|
||||||
|
feedback(),
|
||||||
|
remove_guild(),
|
||||||
|
whitelist(),
|
||||||
|
request_setup(),
|
||||||
|
search(),
|
||||||
|
setup(),
|
||||||
|
// sqlite_transfer(), // Deprecated
|
||||||
|
toggle_kick(),
|
||||||
|
update_search_engine(),
|
||||||
|
key(),
|
||||||
|
invite_info(),
|
||||||
|
];
|
||||||
|
|
||||||
|
discord_commands.append(&mut db_vec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Any command that are not complete/working here
|
||||||
|
#[cfg(feature = "beta")]
|
||||||
|
{
|
||||||
|
let mut beta_vec = vec![];
|
||||||
|
|
||||||
|
discord_commands.append(&mut beta_vec);
|
||||||
|
}
|
||||||
|
|
||||||
|
let framework = poise::Framework::builder()
|
||||||
|
.options(poise::FrameworkOptions {
|
||||||
|
commands: discord_commands,
|
||||||
|
prefix_options: poise::PrefixFrameworkOptions {
|
||||||
|
prefix: Some(args.prefix),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
// The global error handler for all error cases that may occur
|
||||||
|
on_error: |error| Box::pin(on_error(error)),
|
||||||
|
event_handler: |ctx, event, framework, user_data| {
|
||||||
|
Box::pin(event_listener(ctx, event, framework, user_data))
|
||||||
|
},
|
||||||
|
owners: bot_owners,
|
||||||
|
// Every command invocation must pass this check to continue execution
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
command_check: Some(|ctx| {
|
||||||
|
Box::pin(async move {
|
||||||
|
if bot_admin_check(ctx).await.unwrap() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut con = open_redis_connection().await?;
|
||||||
|
|
||||||
|
let key_list: HashSet<String> = redis::cmd("SMEMBERS")
|
||||||
|
.arg("user-lists:banned-from-bot")
|
||||||
|
.clone()
|
||||||
|
.query_async(&mut con)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if key_list.contains(&format!("{}", ctx.author().id.as_u64())) {
|
||||||
|
Ok({
|
||||||
|
println!(
|
||||||
|
"{}/{} was blocked from using the bot",
|
||||||
|
ctx.author().id,
|
||||||
|
ctx.author().name
|
||||||
|
);
|
||||||
|
false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
post_command: |_ctx| {
|
||||||
|
Box::pin(async move {
|
||||||
|
inc_execution_count().await.expect("");
|
||||||
|
})
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.token(args.token)
|
||||||
|
.intents(serenity::GatewayIntents::all())
|
||||||
|
.setup(move |_ctx, _ready, _framework| Box::pin(async move { Ok(Data {}) }))
|
||||||
|
.client_settings(|f| f.cache_settings(|cs| cs.max_messages(5_000)));
|
||||||
|
|
||||||
|
framework.run_autosharded().await.unwrap();
|
||||||
|
}
|
15
westmere.Dockerfile
Normal file
15
westmere.Dockerfile
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
FROM rust:latest as builder
|
||||||
|
WORKDIR /usr/src/rusted-fbt
|
||||||
|
COPY . .
|
||||||
|
RUN env RUSTFLAGS="-C target-cpu=westmere" cargo install --path .
|
||||||
|
|
||||||
|
FROM ubuntu:23.10
|
||||||
|
# RUN apt-get update && apt-get install -y extra-runtime-dependencies && rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/rusted-fbt /usr/local/bin/rusted-fbt
|
||||||
|
RUN apt update -y
|
||||||
|
RUN apt install ca-certificates -y
|
||||||
|
RUN apt update -y
|
||||||
|
# This is the only dependancie missing as far as I can tell which is great
|
||||||
|
RUN apt install libssl-dev -y
|
||||||
|
RUN apt autoremove -y
|
||||||
|
CMD ["rusted-fbt"]
|
14
znver3.Dockerfile
Normal file
14
znver3.Dockerfile
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
FROM rust:latest as builder
|
||||||
|
WORKDIR /usr/src/rusted-fbt
|
||||||
|
COPY . .
|
||||||
|
RUN env RUSTFLAGS="-C target-cpu=znver3" cargo install --path .
|
||||||
|
|
||||||
|
FROM debian:buster-slim
|
||||||
|
# RUN apt-get update && apt-get install -y extra-runtime-dependencies && rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/rusted-fbt /usr/local/bin/rusted-fbt
|
||||||
|
run apt update -y
|
||||||
|
run apt install ca-certificates -y
|
||||||
|
run apt update -y
|
||||||
|
# This is the only dependancie missing as far as I can tell which is great
|
||||||
|
run apt install libssl-dev -y
|
||||||
|
CMD ["rusted-fbt"]
|
Loading…
Add table
Add a link
Reference in a new issue