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