init
This commit is contained in:
commit
ebdc182e86
47 changed files with 8090 additions and 0 deletions
36
src/lib/args.rs
Normal file
36
src/lib/args.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use crate::enums::{DebugLevel, LogDebugLevel};
|
||||
use crate::vars::BOT_TOKEN;
|
||||
use clap::Parser;
|
||||
|
||||
/// This is where CLI args are set
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
/// Discord bot token
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
default_value = BOT_TOKEN
|
||||
)]
|
||||
pub token: String,
|
||||
|
||||
/// Command prefix for message commands
|
||||
#[clap(short, long, default_value = "`")]
|
||||
pub prefix: String,
|
||||
|
||||
/// Output extra information in discord reply errors
|
||||
#[clap(short, long)]
|
||||
pub verbose: bool,
|
||||
|
||||
/// Print out list of guilds in cache on startup
|
||||
#[clap(long("gpc"))]
|
||||
pub print_guild_cache: bool,
|
||||
|
||||
/// emit debug information to both stdout and a file
|
||||
#[clap(value_enum, long, default_value = "most")]
|
||||
pub debug: DebugLevel,
|
||||
|
||||
/// emit debug information to both stdout and a file
|
||||
#[clap(value_enum, long, default_value = "most")]
|
||||
pub debug_log: LogDebugLevel,
|
||||
}
|
100
src/lib/checks.rs
Normal file
100
src/lib/checks.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
use crate::types::{Context, Error};
|
||||
use crate::vars::BOT_ADMINS;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Check if command user is in the `BOT_ADMINS` list
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will never return an error.
|
||||
#[allow(clippy::unused_async, clippy::missing_errors_doc)] // async is used by command checks but clippy can't tell
|
||||
pub async fn bot_admin_check(ctx: Context<'_>) -> Result<bool, Error> {
|
||||
// ? The bellow commented out code is for quick testing, automatic fails on my ID
|
||||
// match ctx.author().id.as_u64() {
|
||||
// 383507911160233985 => Ok(false),
|
||||
// _ => {
|
||||
// match BOT_ADMINS.contains(ctx.author().id.as_u64()) {
|
||||
// true => Ok(true),
|
||||
// false => Ok(false),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if BOT_ADMINS.contains(ctx.author().id.as_u64()) {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
// ? This might not be needed, I thinik it's a left over from before we dud guild based authing
|
||||
// ! Remove the _ if put into use!
|
||||
#[cfg(feature = "database")]
|
||||
#[deprecated(
|
||||
since = "0.1.12",
|
||||
note = "left over from before we dud guild based auth"
|
||||
)]
|
||||
#[allow(clippy::unused_async, clippy::missing_errors_doc)] // no need to lint dead code
|
||||
pub async fn _bot_auth_check(_ctx: Context<'_>) -> Result<bool, Error> {
|
||||
// if let Ok(res) = bot_admin_check(ctx).await {
|
||||
// if res {
|
||||
// return Ok(true);
|
||||
// }
|
||||
// }
|
||||
|
||||
// let mut con = open_redis_connection().await?;
|
||||
|
||||
// let key_list: HashSet<u64> = redis::cmd("SMEMBERS")
|
||||
// .arg("user-lists:authorised-users")
|
||||
// .clone()
|
||||
// .query_async(&mut con)
|
||||
// .await?;
|
||||
|
||||
// if key_list.contains(ctx.author().id.as_u64()) {
|
||||
// Ok(true)
|
||||
// } else {
|
||||
// ctx.say("You are not authorized to use this command! Please contact a bot admin or Azuki!")
|
||||
// .await?;
|
||||
// Ok(false)
|
||||
// }
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Checks if a user is authorised to use the bot in the current server
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if unable to connet to or query DB.
|
||||
#[cfg(feature = "database")]
|
||||
pub async fn guild_auth_check(ctx: Context<'_>) -> Result<bool, Error> {
|
||||
use crate::utils::open_redis_connection;
|
||||
|
||||
if let Ok(res) = bot_admin_check(ctx).await {
|
||||
if res {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
let mut con = open_redis_connection().await?;
|
||||
|
||||
let key_list: Option<HashSet<String>> = redis::cmd("SMEMBERS")
|
||||
.arg(format!(
|
||||
"authed-server-users:{}",
|
||||
ctx.guild_id()
|
||||
.unwrap_or(poise::serenity_prelude::GuildId(0))
|
||||
.as_u64()
|
||||
))
|
||||
.clone()
|
||||
.query_async(&mut con)
|
||||
.await?;
|
||||
|
||||
match key_list {
|
||||
None => Ok(false),
|
||||
Some(list) if !list.contains(&format!("{}", ctx.author().id.as_u64())) => {
|
||||
Ok({
|
||||
ctx.say("You are not authorized to use this command! Please contact a bot admin or Azuki!").await?;
|
||||
false
|
||||
})
|
||||
}
|
||||
Some(_list) => Ok(true),
|
||||
}
|
||||
}
|
57
src/lib/enums.rs
Normal file
57
src/lib/enums.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use poise::serenity_prelude::{self as serenity};
|
||||
|
||||
#[derive(Debug, poise::ChoiceParameter)]
|
||||
pub enum WaifuTypes {
|
||||
Neko,
|
||||
Megumin,
|
||||
Bully,
|
||||
Cuddle,
|
||||
Cry,
|
||||
Kiss,
|
||||
Lick,
|
||||
Pat,
|
||||
Smug,
|
||||
Bonk,
|
||||
Blush,
|
||||
Smile,
|
||||
Wave,
|
||||
Highfive,
|
||||
Handhold,
|
||||
Nom,
|
||||
Bite,
|
||||
Glomp,
|
||||
Slap,
|
||||
Kill,
|
||||
Happy,
|
||||
Wink,
|
||||
Poke,
|
||||
Dance,
|
||||
Cringe,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, clap::ValueEnum)]
|
||||
pub enum DebugLevel {
|
||||
Off,
|
||||
Some,
|
||||
Most,
|
||||
All,
|
||||
}
|
||||
|
||||
impl DebugLevel {
|
||||
#[must_use]
|
||||
pub fn enabled(&self) -> bool {
|
||||
*self != Self::Off
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, clap::ValueEnum)]
|
||||
pub enum LogDebugLevel {
|
||||
Most,
|
||||
All,
|
||||
}
|
||||
|
||||
pub enum CloseTicketFail {
|
||||
False,
|
||||
IncorrectCategory,
|
||||
SerenityError(serenity::Error),
|
||||
}
|
574
src/lib/event_handlers.rs
Normal file
574
src/lib/event_handlers.rs
Normal file
|
@ -0,0 +1,574 @@
|
|||
use crate::structs::{GuildSettings, UserInfo, WaybackResponse, WaybackStatus};
|
||||
use crate::utils::snowflake_to_unix;
|
||||
use crate::vars::FBT_GUILD_ID;
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::Utc;
|
||||
use chrono_tz::Australia::Melbourne;
|
||||
use colored::Colorize;
|
||||
use poise::serenity_prelude::{self as serenity, ChannelId, Colour, MessageUpdateEvent};
|
||||
use rand::Rng;
|
||||
use std::collections::HashMap;
|
||||
use tracing::{event, Level};
|
||||
|
||||
// TODO: Change to the ID of a channel you want all DMs sent to the bot to be relayed to
|
||||
const DM_CHANNEL_ID: u64 = 0000000000000000000;
|
||||
|
||||
/// If enabled on a server it will warn them on black listed users joining
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if unable to parse channel ID from DB to u64.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if;
|
||||
/// - Fails to contact redis DB.
|
||||
/// - Fails to get guild settings from DB.
|
||||
/// - Fails to ask Redis for coresponding DB entry for user.
|
||||
/// - Fails to send message to channel.
|
||||
#[cfg(feature = "database")]
|
||||
pub async fn bl_warner(
|
||||
ctx: &serenity::Context,
|
||||
member: &serenity::Member,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
use crate::utils::open_redis_connection;
|
||||
|
||||
let mut con = open_redis_connection().await?;
|
||||
|
||||
let guild_settings_json_in: Option<String> = redis::cmd("JSON.GET")
|
||||
.arg(format!("guild-settings:{}", member.guild_id.as_u64()))
|
||||
.clone()
|
||||
.query_async(&mut con)
|
||||
.await?;
|
||||
|
||||
let if_on_bl: Option<String> = redis::cmd("JSON.GET")
|
||||
.arg(format!("user:{}", member.user.id.as_u64()))
|
||||
.clone()
|
||||
.query_async(&mut con)
|
||||
.await?;
|
||||
|
||||
match if_on_bl {
|
||||
None => {}
|
||||
Some(user_json) => {
|
||||
match guild_settings_json_in {
|
||||
None => {} // Do nothing
|
||||
// Check guild settings
|
||||
Some(server_json) => {
|
||||
let settings: GuildSettings = serde_json::from_str(&server_json)?;
|
||||
let user: UserInfo = serde_json::from_str(&user_json)?;
|
||||
|
||||
ChannelId::from(settings.channel_id.parse::<u64>().unwrap())
|
||||
.say(
|
||||
ctx,
|
||||
format!(
|
||||
"<@{}>/{0} Just joined your server with {} offenses on record",
|
||||
user.discord_id.unwrap(),
|
||||
user.offences.len()
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if server has alt protection enabled and then kicks the new member if they are >90 days old
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if;
|
||||
/// - Fails to connect to Redis DB.
|
||||
/// - Fails to serde guild settings json to `GuildSettings` struct.
|
||||
/// - Fails to send DM to user getting kicked.
|
||||
/// - Fails to actually kick member.
|
||||
///
|
||||
#[cfg(feature = "database")]
|
||||
pub async fn alt_kicker(
|
||||
ctx: &serenity::Context,
|
||||
member: &serenity::Member,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
use crate::utils::open_redis_connection;
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut con = open_redis_connection().await?;
|
||||
|
||||
let whitelist: HashSet<String> = redis::cmd("SMEMBERS")
|
||||
.arg("kick-whitelist")
|
||||
.clone()
|
||||
.query_async(&mut con)
|
||||
.await?;
|
||||
|
||||
if whitelist.contains(&member.user.id.0.to_string()) {
|
||||
return Ok(()); // Don't kick whitelisted users
|
||||
}
|
||||
|
||||
let guild_settings_json_in: Option<String> = redis::cmd("JSON.GET")
|
||||
.arg(format!("guild-settings:{}", member.guild_id.as_u64()))
|
||||
.clone()
|
||||
.query_async(&mut con)
|
||||
.await?;
|
||||
|
||||
match guild_settings_json_in {
|
||||
None => {} // Do nothing
|
||||
// Check guild settings
|
||||
Some(json_in) => {
|
||||
let settings: GuildSettings = serde_json::from_str(&json_in)?;
|
||||
// Is kicking enabled?
|
||||
if settings.kick {
|
||||
let uid = *member.user.id.as_u64();
|
||||
|
||||
// Trying to handle the pfp here to see if it catches more or maybe most alts really do have the same pfp
|
||||
let pfp = member
|
||||
.avatar_url()
|
||||
.unwrap_or_else(|| {
|
||||
"https://discord.com/assets/1f0bfc0865d324c2587920a7d80c609b.png"
|
||||
.to_string()
|
||||
})
|
||||
.clone();
|
||||
|
||||
let unix_timecode = snowflake_to_unix(u128::from(uid));
|
||||
|
||||
#[allow(clippy::pedantic)]
|
||||
// it literally only take's i64, no need to warn about truncation here.
|
||||
let date_time_stamp = NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0)
|
||||
.unwrap_or(NaiveDateTime::MIN);
|
||||
|
||||
let age = chrono::Utc::now()
|
||||
.naive_utc()
|
||||
.signed_duration_since(date_time_stamp)
|
||||
.num_days();
|
||||
|
||||
// Compare user age
|
||||
if !age.ge(&90_i64) {
|
||||
member.user.direct_message(ctx.http.clone(), |f| {
|
||||
f.content("It looks like your account is under 90 days old, or has been detected as a potential alt. You have been kick from the server!\nYou have not been banned, feel free to join back when your account is over 90 days old.\nRun the `about` slash command or send `help in this chat to find out more.")
|
||||
}).await?;
|
||||
member
|
||||
.kick_with_reason(
|
||||
ctx.http.clone(),
|
||||
&format!("Potential alt detected, account was {age:.0} day(s) old"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let colour = &mut rand::thread_rng().gen_range(0..10_000_000);
|
||||
|
||||
ChannelId(settings.channel_id.parse::<u64>()?)
|
||||
.send_message(ctx.http.clone(), |f| {
|
||||
f.embed(|e| {
|
||||
e.title("Alt kicked!")
|
||||
.description(format!(
|
||||
"Potential alt detected, account was {:.0} day(s) old",
|
||||
age
|
||||
))
|
||||
.thumbnail(pfp)
|
||||
.field("User ID", uid, true)
|
||||
.field("Name", member.user.name.clone(), true)
|
||||
.color(Colour::new(*colour))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends all recieved DMs into a specified channel
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if;
|
||||
/// - Fails to handle message attachments.
|
||||
/// - Fails to handle message stickers.
|
||||
/// - Fails to send request to wayback machine.
|
||||
/// - Fails to send message to DM channel.
|
||||
// TODO: Handle attachments, list of links?
|
||||
pub async fn handle_dms(
|
||||
event: &serenity::Message,
|
||||
ctx: &serenity::Context,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if !event.author.bot {
|
||||
let message = event.clone();
|
||||
let uid = *message.author.id.as_u64();
|
||||
|
||||
let icon = message.author.avatar_url().map_or_else(
|
||||
|| "https://discord.com/assets/1f0bfc0865d324c2587920a7d80c609b.png".to_string(),
|
||||
|url| url,
|
||||
);
|
||||
|
||||
let cache = ctx.http.clone();
|
||||
|
||||
let colour = &mut rand::thread_rng().gen_range(0..10_000_000);
|
||||
|
||||
let now = Utc::now().with_timezone(&Melbourne);
|
||||
|
||||
let local_time = now.to_string();
|
||||
|
||||
let timestamp = local_time.to_string();
|
||||
|
||||
let mut wayback_job_ids = Vec::new();
|
||||
|
||||
let list_of_files = if message.attachments.is_empty() | message.sticker_items.is_empty() {
|
||||
"N/A".to_string()
|
||||
} else {
|
||||
let mut urls = Vec::new();
|
||||
|
||||
handle_files(&message, &mut wayback_job_ids, &mut urls).await?;
|
||||
|
||||
// Duped code for stickers, could probably refactor into function
|
||||
handle_stickers(&message, ctx, &mut wayback_job_ids, &mut urls).await?;
|
||||
|
||||
urls.join("\n \n")
|
||||
};
|
||||
|
||||
let mut msg = ChannelId(DM_CHANNEL_ID)
|
||||
.send_message(cache, |f| {
|
||||
f.embed(|e| {
|
||||
e.title("New message:")
|
||||
.description(message.content.clone())
|
||||
.field("Attachments/Stickers:", list_of_files.clone(), false)
|
||||
.field("User ID", uid, false)
|
||||
.field("Recieved at:", timestamp.clone(), false)
|
||||
.author(|a| a.icon_url(icon.clone()).name(message.author.name.clone()))
|
||||
.color(Colour::new(*colour))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut wayback_urls: Vec<String> = Vec::new();
|
||||
|
||||
for job in wayback_job_ids {
|
||||
let mut is_not_done = true;
|
||||
while is_not_done {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// TODO: Change to your own wayback machine authorization key
|
||||
let response = client
|
||||
.get(format!("https://web.archive.org/save/status/{job}"))
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", "LOW asdgasdg:fasfaf") // auth key here!!
|
||||
.send()
|
||||
.await?;
|
||||
let response_content = response.text().await?;
|
||||
let wayback_status: WaybackStatus = serde_json::from_str(&response_content)?;
|
||||
|
||||
if wayback_status.status == *"success" {
|
||||
wayback_urls.push(format!(
|
||||
"https://web.archive.org/web/{}",
|
||||
wayback_status.original_url.unwrap_or_else(|| {
|
||||
"20220901093722/https://www.dafk.net/what/".to_string()
|
||||
})
|
||||
));
|
||||
is_not_done = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !wayback_urls.is_empty() {
|
||||
msg.edit(ctx, |f| {
|
||||
f.embed(|e| {
|
||||
e.title("New message:")
|
||||
.description(message.content.clone())
|
||||
.field("Attachments/Stickers:", list_of_files, false)
|
||||
.field(
|
||||
"Archived Attachments/Stickers:",
|
||||
wayback_urls.join("\n \n"),
|
||||
false,
|
||||
)
|
||||
.field("User ID", uid, false)
|
||||
.field("Recieved at:", timestamp, false)
|
||||
.author(|a| a.icon_url(icon).name(message.author.name.clone()))
|
||||
.color(Colour::new(*colour))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handles DM files.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if Failes to contact wayback machine.
|
||||
async fn handle_files(
|
||||
message: &serenity::Message,
|
||||
wayback_job_ids: &mut Vec<String>,
|
||||
urls: &mut Vec<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
for file in message.attachments.clone() {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut params = HashMap::new();
|
||||
params.insert("url".to_string(), file.url.clone());
|
||||
params.insert("skip_first_archive".to_string(), "1".to_string());
|
||||
|
||||
// TODO: Change to your own wayback machine authorization key
|
||||
|
||||
let response = client
|
||||
.post("https://web.archive.org/save")
|
||||
.form(¶ms)
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", "LOW asdgasdg:fasfaf")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response_content = response.text().await?;
|
||||
let wayback_status: WaybackResponse = serde_json::from_str(&response_content)?;
|
||||
|
||||
if wayback_status.status.is_none() {
|
||||
if let Some(jid) = wayback_status.job_id {
|
||||
wayback_job_ids.push(jid);
|
||||
}
|
||||
}
|
||||
|
||||
urls.push(file.url);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handles DM stickers.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if Failes to contact wayback machine.
|
||||
async fn handle_stickers(
|
||||
message: &serenity::Message,
|
||||
ctx: &serenity::Context,
|
||||
wayback_job_ids: &mut Vec<String>,
|
||||
urls: &mut Vec<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
for file in message.sticker_items.clone() {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut params = HashMap::new();
|
||||
params.insert(
|
||||
"url".to_string(),
|
||||
file.to_sticker(ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.image_url()
|
||||
.unwrap()
|
||||
.clone(),
|
||||
);
|
||||
params.insert("skip_first_archive".to_string(), "1".to_string());
|
||||
|
||||
// TODO: Change to your own wayback machine authorization key
|
||||
|
||||
let response = client
|
||||
.post("https://web.archive.org/save")
|
||||
.form(¶ms)
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", "LOW asdgasdg:fasfaf")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response_content = response.text().await?;
|
||||
let wayback_status: WaybackResponse = serde_json::from_str(&response_content)?;
|
||||
|
||||
match wayback_status.status {
|
||||
None => {
|
||||
if let Some(jid) = wayback_status.job_id {
|
||||
wayback_job_ids.push(jid);
|
||||
}
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
|
||||
urls.push(file.to_sticker(ctx).await.unwrap().image_url().unwrap());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// When a message is edited in FBT this function will send the new and old message to a specified channel.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if an author doesn't exist, should be unreachable.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the message fails to send.
|
||||
pub async fn handle_msg_edit(
|
||||
event: MessageUpdateEvent,
|
||||
old_if_available: &Option<serenity::Message>,
|
||||
ctx: &serenity::Context,
|
||||
new: &Option<serenity::Message>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if event.guild_id.is_some() {
|
||||
if let Some(author) = event.author.clone() {
|
||||
if !author.bot {
|
||||
let old_message = old_if_available.as_ref().map_or_else(
|
||||
|| "Message not stored in cache :(".to_string(),
|
||||
|msg| msg.content.to_string(),
|
||||
);
|
||||
|
||||
let new_message = new.as_ref().map_or_else(
|
||||
|| "Message not stored in cache :(".to_string(),
|
||||
|msg| msg.content.to_string(),
|
||||
);
|
||||
|
||||
let message_url = new.as_ref().map_or_else(
|
||||
|| "URL stored in cache :(".to_string(),
|
||||
poise::serenity_prelude::Message::link,
|
||||
);
|
||||
|
||||
let current_time = Utc::now().with_timezone(&Melbourne);
|
||||
|
||||
let local_time = current_time.to_string();
|
||||
|
||||
let timestamp = local_time.to_string();
|
||||
|
||||
// TODO: channel to alert you that a message has been deleted
|
||||
|
||||
ChannelId(891_294_507_923_025_951)
|
||||
.send_message(ctx.http.clone(), |f| {
|
||||
f.embed(|e| {
|
||||
e.title(format!(
|
||||
"\"{}\" Edited a message",
|
||||
event.author.clone().unwrap().tag()
|
||||
))
|
||||
.field("Old message content:", old_message, false)
|
||||
.field("New message content:", new_message, false)
|
||||
.field("Link:", message_url, false)
|
||||
.field("Edited at:", timestamp, false)
|
||||
.footer(|f| {
|
||||
f.text(format!(
|
||||
"User ID: {}",
|
||||
event.author.clone().unwrap().id.as_u64()
|
||||
))
|
||||
})
|
||||
.color(Colour::new(0x00FA_A81A))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handles messages that have been deleted
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if there is no message object in cache.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if unable to send message to channel.
|
||||
pub async fn handle_msg_delete(
|
||||
guild_id: &Option<serenity::GuildId>,
|
||||
ctx: &serenity::Context,
|
||||
channel_id: &ChannelId,
|
||||
deleted_message_id: &serenity::MessageId,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
match guild_id {
|
||||
None => {}
|
||||
Some(gid) => {
|
||||
// TODO: this logs any delted message in FBT specifically, change to your own server ID
|
||||
if *gid.as_u64() == 737_168_134_502_350_849 {
|
||||
match ctx.cache.message(channel_id, deleted_message_id) {
|
||||
None => {}
|
||||
Some(msg) => {
|
||||
if !msg.author.bot {
|
||||
let message = match ctx.cache.message(channel_id, deleted_message_id) {
|
||||
None => "Message not stored in cache :(".to_string(),
|
||||
Some(msg) => format!("{:?}", msg.content),
|
||||
};
|
||||
|
||||
let author_id = match message.as_str() {
|
||||
"Message not stored in cache :(" => 0_u64,
|
||||
_ => *ctx
|
||||
.cache
|
||||
.message(channel_id, deleted_message_id)
|
||||
.unwrap()
|
||||
.author
|
||||
.id
|
||||
.as_u64(),
|
||||
};
|
||||
|
||||
let author_tag =
|
||||
if message.clone().as_str() == "Message not stored in cache :(" {
|
||||
"Not in cache#000".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{:?}",
|
||||
match ctx.cache.message(channel_id, deleted_message_id) {
|
||||
Some(msg) => {
|
||||
msg.author.tag()
|
||||
}
|
||||
None => {
|
||||
String::new() // This just creates ""
|
||||
}
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
let now = Utc::now().with_timezone(&Melbourne);
|
||||
|
||||
let local_time = now.to_string();
|
||||
|
||||
let timestamp = local_time.to_string();
|
||||
|
||||
// TODO: This is the channel the deleted messages are sent to
|
||||
|
||||
ChannelId(891_294_507_923_025_951)
|
||||
.send_message(ctx.http.clone(), |f| {
|
||||
f.embed(|e| {
|
||||
e.title(format!("{author_tag} deleted a message"))
|
||||
.field("Message content:", message, false)
|
||||
.field("Deleted at:", timestamp, false)
|
||||
.field(
|
||||
"Channel link:",
|
||||
format!(
|
||||
"https://discord.com/channels/{}/{}",
|
||||
guild_id
|
||||
.unwrap_or(serenity::GuildId::from(
|
||||
FBT_GUILD_ID
|
||||
))
|
||||
.as_u64(),
|
||||
channel_id.as_u64()
|
||||
),
|
||||
false,
|
||||
)
|
||||
.footer(|f| f.text(format!("User ID: {author_id}")))
|
||||
.color(Colour::new(0x00ED_4245))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prints message and outputs trace if in verbose mode
|
||||
pub fn handle_resume(event: &serenity::ResumedEvent) {
|
||||
event!(
|
||||
Level::INFO,
|
||||
"ResumedEvent" = format!(
|
||||
"{}",
|
||||
"Bot went offline but is online again".bright_red().italic()
|
||||
)
|
||||
);
|
||||
|
||||
// Is this a good idea?
|
||||
event!(
|
||||
Level::TRACE,
|
||||
"ResumedEvent" = format!(
|
||||
"{}",
|
||||
"Bot went offline but is online again".bright_red().italic()
|
||||
),
|
||||
"event" = ?event
|
||||
);
|
||||
}
|
9
src/lib/lib.rs
Normal file
9
src/lib/lib.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
pub mod args;
|
||||
pub mod checks;
|
||||
pub mod enums;
|
||||
pub mod event_handlers;
|
||||
pub mod memes;
|
||||
pub mod structs;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
pub mod vars;
|
77
src/lib/memes.rs
Normal file
77
src/lib/memes.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use crate::vars::FBT_GUILD_ID;
|
||||
use once_cell::sync::Lazy;
|
||||
use poise::serenity_prelude::{self as serenity};
|
||||
use regex::Regex;
|
||||
use strip_markdown::strip_markdown;
|
||||
|
||||
// Some times maybe good sometimes maybe shit
|
||||
pub static POG_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r"[pP𝓹⍴𝖕𝔭የ𝕡ק🅟🅿ⓟρᑭ𝙥քp̷þp͎₱ᵽ℘ア𝐩𝒑𝓅p̞̈͑̚͞℘p͓̽ք𝓹ᕶp̶p̳p̅][oO0øØ𝓸᥆𝖔𝔬ዐ𝕠๏🅞🅾ⓞσOoÒօoo̷ðo͎の𝗼ᴏᵒ🇴𝙤ѻⲟᓍӨo͓̽o͟o̲o̅o̳o̶🄾o̯̱̊͊͢Ꭷσℴ𝒐𝐨][gG9𝓰𝖌𝔤𝕘🅖🅶ⓖɠGg𝑔ցg̷gg͎g̲g͟ǥ₲ɢg͓̽Gɠ𝓰𝙜🇬Ꮆᵍɢ𝗴𝐠𝒈𝑔ᧁg𝚐₲Ꮆ𝑔ĝ̽̓̀͑𝘨ງ🄶𝔤ģ]\b",
|
||||
).unwrap()
|
||||
});
|
||||
|
||||
/// This will read a message and check to see if the message contains the word `pog`
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if regex fails to compile, this should be unreachable unless I acidentally change something before compile time.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if .
|
||||
pub async fn pog_be_gone(
|
||||
new_message: &serenity::Message,
|
||||
ctx: &serenity::Context,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if !new_message.author.bot && !new_message.content.is_empty() {
|
||||
match new_message.guild(ctx) {
|
||||
None => {} // Probably a DM, do nothing
|
||||
Some(guild) => {
|
||||
if guild.id.as_u64() == &FBT_GUILD_ID {
|
||||
let lowercase_message = new_message.content.to_lowercase();
|
||||
let cleaned_message = strip_markdown(&lowercase_message);
|
||||
|
||||
let words: Vec<&str> = cleaned_message.split(' ').collect();
|
||||
let mut hits: Vec<&str> = Vec::new();
|
||||
|
||||
for word in words {
|
||||
POG_RE.find(word).map_or((), |pog| {
|
||||
hits.push(pog.as_str());
|
||||
});
|
||||
}
|
||||
|
||||
if !hits.is_empty() {
|
||||
// there is at least 1 pog found
|
||||
if hits.capacity().gt(&10) {
|
||||
new_message
|
||||
.reply(
|
||||
ctx,
|
||||
format!(
|
||||
"Jesus dude, why did you pog {} times?! stop it!",
|
||||
hits.len()
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
new_message.reply_mention(ctx, "please refer to the rules and use the term 'poi' instead of 'pog'!").await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod meme_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_regex() {
|
||||
let pog_test = "pog";
|
||||
|
||||
assert!(POG_RE.is_match(pog_test));
|
||||
}
|
||||
}
|
157
src/lib/structs.rs
Normal file
157
src/lib/structs.rs
Normal file
|
@ -0,0 +1,157 @@
|
|||
use merge::Merge;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{As, FromInto};
|
||||
|
||||
// User data, which is stored and accessible in all command invocations
|
||||
#[derive(Debug)]
|
||||
pub struct Data {}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Deserialize, PartialEq, Hash, Eq)]
|
||||
pub struct CsvEntry {
|
||||
pub AuthorID: String,
|
||||
pub Author: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Merge, Clone, Hash, Eq)]
|
||||
pub struct UserInfo {
|
||||
#[serde(with = "As::<FromInto<OptionalString2>>")]
|
||||
pub vrc_id: Option<String>,
|
||||
#[serde(with = "As::<FromInto<OptionalString>>")]
|
||||
pub username: Option<String>,
|
||||
#[serde(with = "As::<FromInto<OptionalString2>>")]
|
||||
pub discord_id: Option<String>,
|
||||
#[merge(strategy = merge::vec::append)]
|
||||
pub offences: Vec<Offense>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)]
|
||||
pub struct GuildSettings {
|
||||
pub channel_id: String,
|
||||
pub kick: bool,
|
||||
pub server_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Merge, Hash, Eq)]
|
||||
pub struct Offense {
|
||||
#[merge(skip)]
|
||||
pub guild_id: String,
|
||||
#[merge(skip)]
|
||||
pub reason: String,
|
||||
#[serde(with = "As::<FromInto<OptionalString2>>")]
|
||||
pub image: Option<String>,
|
||||
#[serde(with = "As::<FromInto<OptionalString2>>")]
|
||||
pub extra: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)]
|
||||
pub struct GuildAuthList {
|
||||
pub users: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)]
|
||||
pub struct ClearedUser {
|
||||
pub user_id: String,
|
||||
pub username: String,
|
||||
pub where_found: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)]
|
||||
pub struct MonitoredGuildInfo {
|
||||
pub guild_name: String,
|
||||
pub guild_id: String,
|
||||
pub invite_link: String,
|
||||
pub updated: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)]
|
||||
pub struct BlacklistHit {
|
||||
pub user_id: String,
|
||||
pub username: String,
|
||||
pub guild_id: String,
|
||||
pub reason: String,
|
||||
pub image: String,
|
||||
pub extra: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WaybackResponse {
|
||||
pub url: Option<String>,
|
||||
#[serde(rename = "job_id")]
|
||||
pub job_id: Option<String>,
|
||||
pub message: Option<String>,
|
||||
pub status: Option<String>,
|
||||
#[serde(rename = "status_ext")]
|
||||
pub status_ext: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WaybackStatus {
|
||||
#[serde(rename = "http_status")]
|
||||
pub http_status: Option<i64>,
|
||||
#[serde(default)]
|
||||
outlinks: Vec<String>,
|
||||
pub timestamp: Option<String>,
|
||||
#[serde(rename = "original_url")]
|
||||
pub original_url: Option<String>,
|
||||
resources: Vec<String>,
|
||||
#[serde(rename = "duration_sec")]
|
||||
pub duration_sec: Option<f64>,
|
||||
pub status: String,
|
||||
#[serde(rename = "job_id")]
|
||||
pub job_id: String,
|
||||
pub counters: Option<Counters>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Counters {
|
||||
pub outlinks: i64,
|
||||
pub embeds: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct OptionalString(pub Option<String>);
|
||||
|
||||
impl From<OptionalString> for Option<String> {
|
||||
fn from(val: OptionalString) -> Self {
|
||||
val.0.map_or_else(|| Some("N/A".to_string()), Some)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<String>> for OptionalString {
|
||||
fn from(val: Option<String>) -> Self {
|
||||
val.map_or_else(|| Self(Some("N/A".to_string())), |s| Self(Some(s)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct OptionalString2(pub Option<String>);
|
||||
|
||||
impl From<OptionalString2> for Option<String> {
|
||||
fn from(val: OptionalString2) -> Self {
|
||||
val.0.map_or_else(
|
||||
|| Some("N/A".to_string()),
|
||||
|s| match s.as_str() {
|
||||
"0" => Some("N/A".to_string()),
|
||||
x => Some(x.to_string()),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<String>> for OptionalString2 {
|
||||
fn from(val: Option<String>) -> Self {
|
||||
val.map_or_else(|| Self(Some("N/A".to_string())), |s| Self(Some(s)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasteResponse {
|
||||
pub key: String,
|
||||
}
|
4
src/lib/types.rs
Normal file
4
src/lib/types.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
use crate::structs::Data;
|
||||
|
||||
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
pub type Context<'a> = poise::Context<'a, Data, Error>;
|
142
src/lib/utils.rs
Normal file
142
src/lib/utils.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use crate::args::Args;
|
||||
use crate::structs::GuildSettings;
|
||||
use crate::types::Context;
|
||||
use crate::types::Error;
|
||||
use crate::vars::REDIS_ADDR;
|
||||
use clap::Parser;
|
||||
use tracing::instrument;
|
||||
|
||||
/// Converts a dsicord snowflake to a unix timecode
|
||||
#[must_use]
|
||||
pub const fn snowflake_to_unix(id: u128) -> u128 {
|
||||
const DISCORD_EPOCH: u128 = 1_420_070_400_000;
|
||||
|
||||
((id >> 22) + DISCORD_EPOCH) / 1000
|
||||
}
|
||||
|
||||
/// Quickly checks if the verbose flag was used on launch
|
||||
#[must_use]
|
||||
pub fn verbose_mode() -> bool {
|
||||
let args = Args::parse();
|
||||
|
||||
args.verbose
|
||||
}
|
||||
|
||||
/// Open a tokio redis connection
|
||||
#[cfg(feature = "database")]
|
||||
#[instrument()]
|
||||
pub async fn open_redis_connection() -> Result<redis::aio::Connection, anyhow::Error> {
|
||||
let redis_connection = redis::Client::open(REDIS_ADDR)?
|
||||
.get_tokio_connection()
|
||||
.await?;
|
||||
|
||||
Ok(redis_connection)
|
||||
}
|
||||
|
||||
/// Pushes guild settings to DB
|
||||
#[cfg(feature = "database")]
|
||||
#[instrument(skip(con))]
|
||||
pub async fn set_guild_settings(
|
||||
ctx: Context<'_>,
|
||||
con: &mut redis::aio::Connection,
|
||||
settings: GuildSettings,
|
||||
) -> Result<(), Error> {
|
||||
let json = serde_json::to_string(&settings).unwrap();
|
||||
|
||||
let mut pipe = redis::pipe();
|
||||
|
||||
pipe.cmd("JSON.SET").arg(&[
|
||||
format!(
|
||||
"guild-settings:{}",
|
||||
ctx.guild_id().expect("Not run inside guild")
|
||||
),
|
||||
"$".to_string(),
|
||||
json,
|
||||
]);
|
||||
|
||||
pipe.atomic().query_async(con).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds the user to a server's auth list in the DB
|
||||
#[cfg(feature = "database")]
|
||||
#[instrument(skip(con))]
|
||||
pub async fn auth(
|
||||
ctx: Context<'_>,
|
||||
con: &mut redis::aio::Connection,
|
||||
uid: String,
|
||||
) -> Result<(), Error> {
|
||||
redis::cmd("SADD")
|
||||
.arg(&[
|
||||
format!(
|
||||
"authed-server-users:{}",
|
||||
ctx.guild_id().expect("Not run inside guild")
|
||||
),
|
||||
uid,
|
||||
])
|
||||
.query_async(con)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Increases the total commands run count in the DB
|
||||
#[cfg(feature = "database")]
|
||||
#[instrument]
|
||||
pub async fn inc_execution_count() -> Result<(), Error> {
|
||||
let mut con = open_redis_connection().await?;
|
||||
|
||||
// increment status:commands-executed in redis DB
|
||||
redis::cmd("INCR")
|
||||
.arg("status:commands-executed")
|
||||
.query_async(&mut con)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
#[instrument]
|
||||
pub async fn is_uid_valid_user(uid: u64, ctx: &Context<'_>) -> anyhow::Result<bool> {
|
||||
let u_opt: Option<poise::serenity_prelude::User> =
|
||||
match poise::serenity_prelude::UserId::from(uid)
|
||||
.to_user(ctx)
|
||||
.await
|
||||
{
|
||||
Ok(user) => Some(user),
|
||||
Err(error) => {
|
||||
if verbose_mode() {
|
||||
ctx.say(format!(
|
||||
"ID must be a user ID, make sure you coppied the right one! Error: {:?}",
|
||||
error
|
||||
))
|
||||
.await?;
|
||||
} else {
|
||||
ctx.say("ID must be a user ID, make sure you coppied the right one!")
|
||||
.await?;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Ok(u_opt.is_some())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod utils_tests {
|
||||
|
||||
use crate::utils::{snowflake_to_unix, verbose_mode};
|
||||
|
||||
#[test]
|
||||
fn snowflake_unix_test() {
|
||||
assert_eq!(snowflake_to_unix(383_507_911_160_233_985), 1_511_505_811);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verbose_mode_test() {
|
||||
// Inverting output since verbose mode is disabled by default
|
||||
assert!(!verbose_mode());
|
||||
}
|
||||
}
|
90
src/lib/vars.rs
Normal file
90
src/lib/vars.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
pub const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub const HELP_EXTRA_TEXT: &str = "Find the documentation website at https://fbtsecurity.fbtheaven.com/\nRun the About command to find out more (/about)";
|
||||
|
||||
// TODO: change this list to your own bot admin user IDs
|
||||
|
||||
// You need to increase the number in [u64; X] so rust knows the limit of the array
|
||||
pub const BOT_ADMINS: [u64; 6] = [
|
||||
212_132_817_017_110_528,
|
||||
288_186_677_967_585_280,
|
||||
211_027_317_068_136_448,
|
||||
383_507_911_160_233_985,
|
||||
168_600_506_233_651_201,
|
||||
231_482_341_921_521_664,
|
||||
]; // Azuki, Komi, Xeno, Mojo, Ellie, Wundie
|
||||
|
||||
// TODO: you can mass replace the name of this variable easily
|
||||
// TODO: change to your own guild ID
|
||||
|
||||
pub const FBT_GUILD_ID: u64 = 737_168_134_502_350_849; // FBT's guild ID
|
||||
|
||||
// TODO: this is the channel wehre the feedback command sends it's response for you to read
|
||||
pub const FEEDBACK_CHANNEL_ID: u64 = 925_599_477_283_311_636;
|
||||
|
||||
//pub const FBT_GUILD_ID: u64 = 838658675916275722; // My test server ID
|
||||
|
||||
// TODO: you need your own Redis DB, this is where you put in the login details and adress of the DB
|
||||
// format: "redis://USERNAME:PASSWORD@ADDRESS:PORT/DB_INDEX"
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
pub const REDIS_ADDR: &str =
|
||||
"redis://:ForSureARealRedisPassword@google.com:6379/0";
|
||||
|
||||
// TODO: change to your own Meilisearch address
|
||||
#[cfg(feature = "database")]
|
||||
pub const MEILISEARCH_HOST: &str = "http://google.com:7777";
|
||||
|
||||
// TODO: change to your own Meilisearch API key
|
||||
#[cfg(feature = "database")]
|
||||
pub const MEILISEARCH_API_KEY: &str = "why-so-strange";
|
||||
|
||||
// TODO: change to your own bot token
|
||||
pub const BOT_TOKEN: &str =
|
||||
"not touching this <3";
|
||||
|
||||
//TODO: these are popular discord bots, used to ignore their messages and stuff
|
||||
// Part of blacklist for now but I should add it as a check to the excel command too
|
||||
#[cfg(feature = "database")]
|
||||
pub const BOT_IDS: [u64; 22] = [
|
||||
134_133_271_750_639_616,
|
||||
155_149_108_183_695_360,
|
||||
159_985_870_458_322_944,
|
||||
159_985_870_458_322_944,
|
||||
184_405_311_681_986_560,
|
||||
204_255_221_017_214_977,
|
||||
216_437_513_709_944_832,
|
||||
235_088_799_074_484_224,
|
||||
235_148_962_103_951_360,
|
||||
294_882_584_201_003_009,
|
||||
351_227_880_153_546_754,
|
||||
375_805_687_529_209_857,
|
||||
537_429_661_139_861_504,
|
||||
550_613_223_733_329_920,
|
||||
559_426_966_151_757_824,
|
||||
583_995_825_269_768_211,
|
||||
625_588_618_525_802_507,
|
||||
649_535_344_236_167_212,
|
||||
743_269_383_438_073_856,
|
||||
743_269_383_438_073_856,
|
||||
887_914_294_988_140_565,
|
||||
935_372_708_089_315_369,
|
||||
];
|
||||
|
||||
// TODO: this is for the ticket system, change to your own ticket category ID.
|
||||
// it creates new threads in TICKET_CATEGORY and moves them to CLOSED_TICKET_CATEGORY once closed
|
||||
pub const TICKET_CATEGORY: u64 = 982_769_870_259_240_981;
|
||||
pub const CLOSED_TICKET_CATEGORY: u64 = 983_228_142_107_918_336;
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
#[derive(Debug, poise::ChoiceParameter)]
|
||||
pub enum BlacklistOutput {
|
||||
#[name = "Chat - Output resulting @, ID and Reasons to chat"]
|
||||
Chat,
|
||||
#[name = "Compact Chat - Only send resulting @ and IDs"]
|
||||
CompactChat,
|
||||
#[name = "CSV - Output all relevant info as a single .csv file"]
|
||||
Csv,
|
||||
#[name = "Json - Output all relevant info as a single .json file"]
|
||||
Json,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue