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

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,
}