This commit is contained in:
BuyMyMojo 2024-06-29 01:22:22 +10:00
commit ebdc182e86
47 changed files with 8090 additions and 0 deletions

26
.gitignore vendored Normal file
View 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

File diff suppressed because it is too large Load diff

74
Cargo.toml Normal file
View 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
View file

@ -0,0 +1 @@
# FBT Security - Rust

BIN
assets/DILLIGAF.ogg Normal file

Binary file not shown.

BIN
assets/Discord_meme.mp4 Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/PomfPomf.ogg Normal file

Binary file not shown.

BIN
assets/amogus.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

BIN
assets/astro.ogg Normal file

Binary file not shown.

BIN
assets/egirls.ogg Normal file

Binary file not shown.

BIN
assets/erp-tonight.mp3 Normal file

Binary file not shown.

BIN
assets/femboys.mp3 Normal file

Binary file not shown.

BIN
assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

110
assets/languages.txt Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
assets/toxic.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
assets/user.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

BIN
assets/white.mp4 Normal file

Binary file not shown.

BIN
assets/wrestle.mp4 Normal file

Binary file not shown.

1
build.ps1 Normal file
View file

@ -0,0 +1 @@
$Env:RUSTFLAGS='-C target-cpu=native'; cargo build --release

21
build.rs Normal file
View 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
View 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
View 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
View file

@ -0,0 +1 @@
$Env:RUSTFLAGS='-C target-cpu=native'; cargo build --profile=release-full-lto

224
src/commands/_deprecated.rs Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

176
src/commands/fun.rs Normal file
View 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
View 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![
("Help page:", "[https://fbtsecurity.fbtheaven.com/](https://fbtsecurity.fbtheaven.com/)", 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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(&params)
.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(&params)
.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
View 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
View 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øØ𝓸𝖔𝔬𝕠๏🅞🅾ⓞσOÒօoo̷ðo͎の𝗼ᵒ🇴𝙤ѻᓍӨo͓̽o͟o̲o̅o̳o̶🄾o̯̱̊͊͢Ꭷσ𝒐𝐨][gG9𝓰𝖌𝔤𝕘🅖🅶ⓖɠG𝑔ց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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"]