init
This commit is contained in:
commit
ebdc182e86
47 changed files with 8090 additions and 0 deletions
224
src/commands/_deprecated.rs
Normal file
224
src/commands/_deprecated.rs
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
// https://sqlitebrowser.org/
|
||||
/// Transfer from sqlite DB, get the json files using "File>Export>Table(s) to json" in sqlitebrowser
|
||||
#[cfg(feature = "database")]
|
||||
#[allow(non_snake_case, non_camel_case_types)] // Keeping these badly names variables since that's what they are called in the SQLite DB
|
||||
#[poise::command(slash_command, category = "Admin", owners_only, hide_in_help)]
|
||||
async fn sqlite_transfer(
|
||||
ctx: Context<'_>,
|
||||
#[description = "vrc_data.json"] vrc_data: Attachment,
|
||||
// #[description = "no_bot_perms.json"] no_bot_perms: Attachment,
|
||||
#[description = "guild_channels.json"] guild_channels: Attachment,
|
||||
#[description = "authorized_users.json"] authorized_users: Attachment,
|
||||
#[description = "Cleared_IDs.json"] cleard_ids: Attachment,
|
||||
#[description = "Monitored Guilds.json"] monitored_guilds: Attachment,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthorizedUser {
|
||||
user_id: u64,
|
||||
guild_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ClearedID {
|
||||
Discord_ID: u64,
|
||||
Name: String,
|
||||
Where_found: String,
|
||||
Cleared_Reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MonitoredGuild {
|
||||
Guild_Name: String,
|
||||
Guild_ID: u64,
|
||||
Invite_link: Option<String>,
|
||||
Updated: Option<String>,
|
||||
DMCA_Takedown_Nuked: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GuildChannel {
|
||||
guild: u64,
|
||||
channel_id: u64,
|
||||
kick_active: u64,
|
||||
Name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct vrc_data {
|
||||
vrc_id: Option<String>,
|
||||
guild_id: Option<u64>,
|
||||
name: Option<String>,
|
||||
discord_id: u64,
|
||||
reason: String,
|
||||
image: Option<String>,
|
||||
extra: Option<String>,
|
||||
}
|
||||
|
||||
let mut con = open_redis_connection().await?;
|
||||
let mut pipe = redis::pipe();
|
||||
|
||||
let msg = ctx.say("Downloading files").await?;
|
||||
|
||||
let authorized_users_json = authorized_users.download().await?;
|
||||
let guild_channel_json = guild_channels.download().await?;
|
||||
let cleard_ids_json = cleard_ids.download().await?;
|
||||
let monitored_guilds_json = monitored_guilds.download().await?;
|
||||
let vrc_data_json = vrc_data.download().await?;
|
||||
|
||||
msg.edit(ctx, |b| b.content("Converting json to structs"))
|
||||
.await?;
|
||||
|
||||
let auth_user_vec: Vec<AuthorizedUser> =
|
||||
serde_json::from_str(std::str::from_utf8(&authorized_users_json)?)?;
|
||||
let guild_channel_vec: Vec<GuildChannel> =
|
||||
serde_json::from_str(std::str::from_utf8(&guild_channel_json)?)?;
|
||||
let cleard_id_vec: Vec<ClearedID> =
|
||||
serde_json::from_str(std::str::from_utf8(&cleard_ids_json)?)?;
|
||||
let monitored_guilds_vec: Vec<MonitoredGuild> =
|
||||
serde_json::from_str(std::str::from_utf8(&monitored_guilds_json)?)?;
|
||||
let vrc_data_vec: Vec<vrc_data> = serde_json::from_str(std::str::from_utf8(&vrc_data_json)?)?;
|
||||
|
||||
msg.edit(ctx, |b| b.content("Preparing authorized_users data"))
|
||||
.await?;
|
||||
|
||||
for authed_user in auth_user_vec {
|
||||
pipe.cmd("SADD").arg(&[
|
||||
format!("authed-server-users:{}", authed_user.guild_id),
|
||||
format!("{}", authed_user.user_id),
|
||||
]);
|
||||
}
|
||||
|
||||
msg.edit(ctx, |b| b.content("Preparing guild_channels data"))
|
||||
.await?;
|
||||
|
||||
for guild_settings in guild_channel_vec {
|
||||
let formatted = GuildSettings {
|
||||
channel_id: format!("{}", guild_settings.channel_id),
|
||||
kick: match guild_settings.kick_active {
|
||||
1 => true,
|
||||
0 => false,
|
||||
_ => false,
|
||||
},
|
||||
server_name: guild_settings.Name,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&formatted).unwrap();
|
||||
pipe.cmd("JSON.SET").arg(&[
|
||||
format!("guild-settings:{}", guild_settings.guild),
|
||||
"$".to_string(),
|
||||
json,
|
||||
]);
|
||||
}
|
||||
|
||||
msg.edit(ctx, |b| b.content("Preparing cleard_ids data"))
|
||||
.await?;
|
||||
|
||||
for cleared_id in cleard_id_vec {
|
||||
let formatted: ClearedUser = ClearedUser {
|
||||
user_id: format!("{}", cleared_id.Discord_ID),
|
||||
username: cleared_id.Name,
|
||||
where_found: cleared_id.Where_found,
|
||||
reason: cleared_id.Cleared_Reason,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&formatted).unwrap();
|
||||
pipe.cmd("JSON.SET").arg(&[
|
||||
format!("cleared-user:{}", cleared_id.Discord_ID),
|
||||
"$".to_string(),
|
||||
json,
|
||||
]);
|
||||
}
|
||||
|
||||
msg.edit(ctx, |b| b.content("Preparing monitored_guilds data"))
|
||||
.await?;
|
||||
|
||||
for guild in monitored_guilds_vec {
|
||||
let formatted: MonitoredGuildInfo = MonitoredGuildInfo {
|
||||
guild_name: guild.Guild_Name.to_string(),
|
||||
guild_id: format!("{}", guild.Guild_ID),
|
||||
invite_link: match guild.Invite_link {
|
||||
None => "N/A".to_string(),
|
||||
Some(link) => link.to_string(),
|
||||
},
|
||||
updated: match guild.Updated {
|
||||
None => "Never".to_string(),
|
||||
Some(date) => date.to_string(),
|
||||
},
|
||||
status: match guild.DMCA_Takedown_Nuked {
|
||||
None => "Unknown".to_string(),
|
||||
Some(status) => status.to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&formatted).unwrap();
|
||||
pipe.cmd("JSON.SET").arg(&[
|
||||
format!("monitored-guild:{}", guild.Guild_ID),
|
||||
"$".to_string(),
|
||||
json,
|
||||
]);
|
||||
}
|
||||
|
||||
msg.edit(ctx, |b| b.content("Preparing vrc_data")).await?;
|
||||
|
||||
let mut parsed_ids: HashSet<String> = HashSet::new();
|
||||
|
||||
for user_data in vrc_data_vec {
|
||||
match parsed_ids.contains(&format!("{}", user_data.discord_id)) {
|
||||
false => {
|
||||
let mut new_user = UserInfo {
|
||||
vrc_id: user_data.vrc_id,
|
||||
username: user_data.name,
|
||||
discord_id: Some(format!("{}", user_data.discord_id)),
|
||||
offences: Vec::new(),
|
||||
};
|
||||
|
||||
let offense = vec![Offense {
|
||||
guild_id: match user_data.guild_id {
|
||||
None => "N/A".to_string(),
|
||||
Some(gid) => format!("{}", gid),
|
||||
},
|
||||
reason: user_data.reason,
|
||||
image: user_data.image,
|
||||
extra: user_data.extra,
|
||||
}];
|
||||
new_user.offences = offense;
|
||||
|
||||
let json = serde_json::to_string(&new_user).unwrap();
|
||||
pipe.cmd("JSON.SET").arg(&[
|
||||
format!("user:{}", user_data.discord_id),
|
||||
"$".to_string(),
|
||||
json,
|
||||
]);
|
||||
|
||||
parsed_ids.insert(format!("{}", user_data.discord_id));
|
||||
}
|
||||
true => {
|
||||
let offense = Offense {
|
||||
guild_id: match user_data.guild_id {
|
||||
None => "N/A".to_string(),
|
||||
Some(gid) => format!("{}", gid),
|
||||
},
|
||||
reason: user_data.reason,
|
||||
image: user_data.image,
|
||||
extra: user_data.extra,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&offense).unwrap();
|
||||
pipe.cmd("JSON.ARRAPPEND")
|
||||
.arg(format!("user:{}", user_data.discord_id))
|
||||
.arg("$.offences".to_string())
|
||||
.arg(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg.edit(ctx, |b| b.content("Uploading data to DB")).await?;
|
||||
|
||||
pipe.query_async(&mut con).await?;
|
||||
|
||||
msg.edit(ctx, |b| b.content("All done!")).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
516
src/commands/admin.rs
Normal file
516
src/commands/admin.rs
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
use clap::Parser;
|
||||
use core::time;
|
||||
use poise::serenity_prelude::Attachment;
|
||||
use poise::serenity_prelude::{self as serenity, Activity, Member, OnlineStatus};
|
||||
use poise::serenity_prelude::{ChannelId, Colour};
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::Rng;
|
||||
use rusted_fbt_lib::checks::guild_auth_check;
|
||||
use rusted_fbt_lib::structs::GuildSettings;
|
||||
use rusted_fbt_lib::utils::{auth, open_redis_connection, set_guild_settings};
|
||||
use rusted_fbt_lib::{
|
||||
args::Args,
|
||||
checks::bot_admin_check,
|
||||
types::{Context, Error},
|
||||
utils::{inc_execution_count, verbose_mode},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Add;
|
||||
use std::process::exit;
|
||||
use tokio::time::sleep;
|
||||
use tracing::instrument;
|
||||
use tracing::{event, Level};
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
category = "Admin",
|
||||
check = "bot_admin_check",
|
||||
hide_in_help
|
||||
)]
|
||||
/// Sends message to specified user ID
|
||||
pub async fn botmsg(
|
||||
ctx: Context<'_>,
|
||||
#[description = "User ID"] user: serenity::User,
|
||||
#[description = "Message"] msg: String,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
|
||||
user.direct_message(ctx, |f| f.content(&msg)).await?;
|
||||
|
||||
ctx.say(format!("Sent message to: {}", user.name)).await?;
|
||||
ctx.say(format!("Message: {}", &msg)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(
|
||||
prefix_command,
|
||||
slash_command,
|
||||
category = "Admin",
|
||||
required_permissions = "BAN_MEMBERS",
|
||||
required_bot_permissions = "BAN_MEMBERS",
|
||||
guild_only,
|
||||
ephemeral
|
||||
)]
|
||||
/// Explains how to ban a list of users with `ban
|
||||
pub async fn ban_help(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let args = Args::parse();
|
||||
|
||||
ctx.say("To ban a single user the easiest way is with the slash command `/ban ban_user @USER/ID` since you don't need to provide message deletion numbers or a reason.").await?;
|
||||
ctx.say(format!("In order to ban multiple people please use this command as a a prefix command like so:\n```\n{}ban ban_user \"Reason in qutoation marks\" 0(A number from 0 to 7, how many days worth of messages you want to delet) userID1 userID2 userID3\n```", args.prefix)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(
|
||||
prefix_command,
|
||||
slash_command,
|
||||
category = "Admin",
|
||||
required_permissions = "BAN_MEMBERS",
|
||||
required_bot_permissions = "BAN_MEMBERS",
|
||||
guild_only,
|
||||
ephemeral
|
||||
)]
|
||||
/// Explains how to ban a list of users with `ban
|
||||
pub async fn ban_user(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Ban reason"] reason: Option<String>,
|
||||
#[description = "How many days of messages to purge (Max of 7)"]
|
||||
#[min = 0]
|
||||
#[max = 7]
|
||||
dmd: Option<u8>,
|
||||
#[description = "Member(s) to ban"] members: Vec<Member>,
|
||||
) -> Result<(), Error> {
|
||||
let delete_count: u8 = dmd.map_or(0u8, |num| {
|
||||
let num_check = num;
|
||||
if num_check.le(&7u8) {
|
||||
num
|
||||
} else {
|
||||
7u8
|
||||
}
|
||||
});
|
||||
|
||||
let reason_sanitised = reason.map_or_else(|| "Banned via bot ban command".to_string(), |r| r);
|
||||
|
||||
// TODO: Change to your own emojis!
|
||||
|
||||
// Mojo test server emoji version
|
||||
// let phrase_list = vec!["has been ejected", "banned quietly", "terminated", "thrown out", "<a:Banned1:1000474420864880831><a:Banned2:1000474423683452998><a:Banned3:1000474426447503441>"];
|
||||
|
||||
// FBT Emoji version
|
||||
let phrase_list = ["has been ejected", "banned quietly", "terminated", "thrown out", "<a:Banned1:1000474106929631433><a:Banned2:1000474109802725457><a:Banned3:1000474112734531715>"];
|
||||
|
||||
match ctx.guild() {
|
||||
Some(guild) => match members.len() {
|
||||
0 => {
|
||||
let args = Args::parse();
|
||||
|
||||
ctx.say("You must provide at least one user to ban!")
|
||||
.await?;
|
||||
ctx.say(format!("In order to ban multiple people please use this command as a a prefix command like so:\n```\n{}ban userID1 userID2 userID3\n```", args.prefix)).await?;
|
||||
}
|
||||
1 => {
|
||||
let member = guild.member(ctx, members[0].user.id).await?;
|
||||
if let Err(error) = member
|
||||
.ban_with_reason(ctx, delete_count, reason_sanitised.clone())
|
||||
.await
|
||||
{
|
||||
if verbose_mode() {
|
||||
ctx.say(format!(
|
||||
"Failed to ban {} because of {:?}",
|
||||
member.display_name(),
|
||||
error
|
||||
))
|
||||
.await?;
|
||||
} else {
|
||||
ctx.say(format!("Failed to ban {}", member.display_name()))
|
||||
.await?;
|
||||
}
|
||||
// 0u8
|
||||
} else {
|
||||
let phrase = phrase_list
|
||||
.choose(&mut rand::thread_rng())
|
||||
.expect("Unable to get meme phrase for ban");
|
||||
ctx.say(format!("{} has been {phrase}", member.display_name()))
|
||||
.await?;
|
||||
// 0u8
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
for member in members {
|
||||
if let Err(error) = member
|
||||
.ban_with_reason(ctx, delete_count, reason_sanitised.clone())
|
||||
.await
|
||||
{
|
||||
if verbose_mode() {
|
||||
ctx.say(format!(
|
||||
"Failed to ban {} because of {:?}",
|
||||
member.display_name(),
|
||||
error
|
||||
))
|
||||
.await?;
|
||||
} else {
|
||||
ctx.say(format!("Failed to ban {}", member.display_name()))
|
||||
.await?;
|
||||
}
|
||||
// 0u8
|
||||
} else {
|
||||
let phrase = phrase_list
|
||||
.choose(&mut rand::thread_rng())
|
||||
.expect("Unable to get meme phrase for ban");
|
||||
ctx.say(format!("{} has been {phrase}", member.display_name()))
|
||||
.await?;
|
||||
// 0u8
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
ctx.say("This must be ran from inside a guild").await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(
|
||||
prefix_command,
|
||||
slash_command,
|
||||
category = "Admin",
|
||||
required_permissions = "BAN_MEMBERS",
|
||||
required_bot_permissions = "BAN_MEMBERS",
|
||||
guild_only,
|
||||
subcommands("ban_help", "ban_user")
|
||||
)]
|
||||
/// Ban a member or list of members by ID or Mention
|
||||
pub async fn ban(ctx: Context<'_>) -> Result<(), Error> {
|
||||
ctx.say("Run `/ban ban_help` to learn how to use this command and then use ")
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Change to your own emojis!
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(prefix_command, slash_command, category = "Admin", owners_only)]
|
||||
/// Literally just shoot the bot!
|
||||
pub async fn shutdown(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let pewpew = ctx
|
||||
.say("<:GunPoint:908506214915276851> <:FBT:795660945627676712>")
|
||||
.await?;
|
||||
sleep(time::Duration::from_secs(1)).await;
|
||||
pewpew
|
||||
.edit(ctx, |b| {
|
||||
b.content("<:GunPoint:908506214915276851> 💥 <:FBT:795660945627676712>")
|
||||
})
|
||||
.await?;
|
||||
sleep(time::Duration::from_secs(1)).await;
|
||||
pewpew
|
||||
.edit(ctx, |b| {
|
||||
b.content("<:GunPoint:908506214915276851> <:FBT:795660945627676712>")
|
||||
})
|
||||
.await?;
|
||||
sleep(time::Duration::from_secs(1)).await;
|
||||
pewpew
|
||||
.edit(ctx, |b| {
|
||||
b.content("<:GunPoint:908506214915276851> 🩸 <:FBT:795660945627676712> 🩸")
|
||||
})
|
||||
.await?;
|
||||
sleep(time::Duration::from_secs(1)).await;
|
||||
ctx.say("Exiting now!").await?;
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
inc_execution_count().await?;
|
||||
|
||||
let activity = Activity::playing("Sleeping");
|
||||
let status = OnlineStatus::Offline;
|
||||
|
||||
ctx.serenity_context()
|
||||
.set_presence(Some(activity), status)
|
||||
.await;
|
||||
|
||||
exit(0)
|
||||
}
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
/// Authorize someone in this guild
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(
|
||||
prefix_command,
|
||||
slash_command,
|
||||
category = "Admin",
|
||||
check = "guild_auth_check",
|
||||
guild_only
|
||||
)]
|
||||
pub async fn authorize(
|
||||
ctx: Context<'_>,
|
||||
#[description = "User to authorise in this server"] user: Member,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer_or_broadcast().await?;
|
||||
|
||||
let uid = format!("{}", user.user.id.as_u64());
|
||||
|
||||
let mut con = open_redis_connection().await?;
|
||||
|
||||
// * json format: {users:[ID1, ID2, IDect]}
|
||||
let key_list: Option<HashSet<String>> = redis::cmd("SMEMBERS")
|
||||
.arg(format!(
|
||||
"authed-server-users:{}",
|
||||
ctx.guild_id().unwrap().as_u64()
|
||||
))
|
||||
.clone()
|
||||
.query_async(&mut con)
|
||||
.await?;
|
||||
|
||||
if let Some(list) = key_list {
|
||||
if list.contains(&uid) {
|
||||
ctx.say("User already authorised in this server!").await?;
|
||||
} else {
|
||||
match auth(ctx, &mut con, uid).await {
|
||||
Ok(()) => {
|
||||
ctx.say(format!(
|
||||
"{} is now authorized to use commands in this server!",
|
||||
user.display_name()
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
Err(_error) if !verbose_mode() => {
|
||||
ctx.say(format!("Failed to auth {}!", user.display_name()))
|
||||
.await?;
|
||||
}
|
||||
Err(error) => {
|
||||
ctx.say(format!(
|
||||
"Failed to auth {}! Caused by {:?}",
|
||||
user.display_name(),
|
||||
error
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auth(ctx, &mut con, uid).await?;
|
||||
|
||||
ctx.say(format!(
|
||||
"{} is now authorized to use commands in this server!",
|
||||
user.display_name()
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(
|
||||
prefix_command,
|
||||
slash_command,
|
||||
category = "Admin",
|
||||
check = "bot_admin_check"
|
||||
)]
|
||||
/// Send annoucement to any server that has been setup
|
||||
pub async fn announcement(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Title of announcement embed"] title: String,
|
||||
#[description = "Message to send to all servers (As a .txt file!)"] message_file: Attachment,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer_or_broadcast().await?;
|
||||
|
||||
let message_content = message_file.download().await?;
|
||||
let message = std::str::from_utf8(&message_content)?;
|
||||
|
||||
let mut con = open_redis_connection().await?;
|
||||
|
||||
let key_list: Vec<String> = redis::cmd("KEYS")
|
||||
.arg("guild-settings:*")
|
||||
.clone()
|
||||
.query_async(&mut con)
|
||||
.await?;
|
||||
|
||||
let mut key_pipe = redis::pipe();
|
||||
|
||||
for key in key_list {
|
||||
key_pipe.cmd("JSON.GET").arg(key);
|
||||
}
|
||||
|
||||
let setting_entries: Vec<String> = key_pipe.atomic().query_async(&mut con).await?;
|
||||
|
||||
let mut guild_settings_collection = Vec::new();
|
||||
for settings in setting_entries {
|
||||
let gs: GuildSettings = serde_json::from_str(&settings)?;
|
||||
|
||||
guild_settings_collection.push(gs);
|
||||
}
|
||||
|
||||
// TODO: Change to custom announcement message!
|
||||
|
||||
let mut count: u64 = 0;
|
||||
for guild in guild_settings_collection.clone() {
|
||||
let colour = &mut rand::thread_rng().gen_range(0..10_000_000);
|
||||
match ChannelId(guild.channel_id.parse::<u64>()?).send_message(ctx, |f| {
|
||||
f.embed(|e| {
|
||||
e.title(format!("New announcement from FBT Security: {}", title.clone()))
|
||||
.description(message)
|
||||
.color(Colour::new(*colour))
|
||||
.author(|a| {
|
||||
a.icon_url("https://cdn.discordapp.com/avatars/743269383438073856/959512463b1559b14818590d8c8a9d2a.webp?size=4096")
|
||||
.name("FBT Security")
|
||||
})
|
||||
.thumbnail("https://media.giphy.com/media/U4sfHXAALLYBQzPcWk/giphy.gif")
|
||||
})
|
||||
}).await {
|
||||
Err(e)=>{
|
||||
event!(
|
||||
Level::INFO,
|
||||
"Failed to send announcement to a server because of" = ?e
|
||||
);
|
||||
},
|
||||
Ok(msg) => {
|
||||
count = count.add(1);
|
||||
println!("Sent to: {}", msg.link());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ctx.say(format!(
|
||||
"Sent annoucement to {}/{} servers!",
|
||||
count,
|
||||
guild_settings_collection.len()
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(
|
||||
prefix_command,
|
||||
slash_command,
|
||||
category = "Admin",
|
||||
required_permissions = "ADMINISTRATOR",
|
||||
guild_only
|
||||
)]
|
||||
/// Request an FBT staff member to come and auth your server
|
||||
pub async fn request_setup(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Do you want to kick accounts that are under 90 days old"] alt_protection: bool,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
let link = ChannelId(*ctx.channel_id().as_u64())
|
||||
.create_invite(ctx, |f| f.temporary(false).max_age(0).unique(false))
|
||||
.await?;
|
||||
|
||||
|
||||
// TODO: this channel is where the bot alterts you when a server is requesting use of the bot's moderation stuff
|
||||
ChannelId(953_435_498_318_286_898).send_message(ctx, |f| {
|
||||
f.content(format!("{0} is requesting authentication! {1}\n They requested for alt protection to be: `{alt_protection}`", ctx.guild().unwrap().name, link.url()))
|
||||
}).await?;
|
||||
|
||||
ctx.send(|b| b.content("Request sent, sit tight!\nOnce an administrator joins make sure to give them permissions to acess the channel so they can set it up!").ephemeral(true)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
category = "Admin",
|
||||
check = "guild_auth_check",
|
||||
guild_only
|
||||
)]
|
||||
/// Setup your server's settings
|
||||
pub async fn setup(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Do you want to kick accounts that are under 90 days old when they join"]
|
||||
alt_protection: bool,
|
||||
) -> Result<(), Error> {
|
||||
let mut con = open_redis_connection().await?;
|
||||
|
||||
let guild_settings_json_in: Option<String> = redis::cmd("JSON.GET")
|
||||
.arg(format!(
|
||||
"guild-settings:{}",
|
||||
ctx.guild_id().unwrap().as_u64()
|
||||
))
|
||||
.clone()
|
||||
.query_async(&mut con)
|
||||
.await?;
|
||||
|
||||
let ch_id = format!("{}", ctx.channel_id().as_u64());
|
||||
let g_name = ctx
|
||||
.partial_guild()
|
||||
.await
|
||||
.expect("Unable to get Guild info")
|
||||
.name;
|
||||
|
||||
if let Some(json_in) = guild_settings_json_in {
|
||||
let mut settings: GuildSettings = serde_json::from_str(&json_in)?;
|
||||
settings.channel_id = ch_id.clone();
|
||||
settings.kick = alt_protection;
|
||||
settings.server_name = g_name;
|
||||
|
||||
set_guild_settings(ctx, &mut con, settings).await?;
|
||||
ctx.say(format!("Settings have been updated for your server!\nChannel for kick messages and bot announcements: <#{0}>.\nAlt protection: {alt_protection:?}.", ch_id.clone())).await?;
|
||||
} else {
|
||||
let settings = GuildSettings {
|
||||
channel_id: ch_id.clone(),
|
||||
kick: alt_protection,
|
||||
server_name: g_name,
|
||||
};
|
||||
|
||||
set_guild_settings(ctx, &mut con, settings).await?;
|
||||
ctx.say(format!("Settings have been created for your server!\nChannel for kick messages and bot announcements: <#{0}>.\nAlt protection: {alt_protection:?}.", ch_id.clone())).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(
|
||||
prefix_command,
|
||||
slash_command,
|
||||
category = "Admin",
|
||||
check = "guild_auth_check",
|
||||
guild_only
|
||||
)]
|
||||
/// Set your server's alt protection policy
|
||||
pub async fn toggle_kick(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let mut con = open_redis_connection().await?;
|
||||
|
||||
let guild_settings_json_in: Option<String> = redis::cmd("JSON.GET")
|
||||
.arg(format!(
|
||||
"guild-settings:{}",
|
||||
ctx.guild_id().unwrap().as_u64()
|
||||
))
|
||||
.clone()
|
||||
.query_async(&mut con)
|
||||
.await?;
|
||||
|
||||
match guild_settings_json_in {
|
||||
// Update settings
|
||||
Some(json_in) => {
|
||||
let mut settings: GuildSettings = serde_json::from_str(&json_in)?;
|
||||
settings.kick = !settings.kick;
|
||||
|
||||
set_guild_settings(ctx, &mut con, settings.clone()).await?;
|
||||
ctx.say(format!(
|
||||
"Settings have been updated for your server!\nAlt protection: {:?}.",
|
||||
settings.kick
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
// TODO: change to custom message
|
||||
// This should not be able to trigger because of the auth check but better safe than sorry
|
||||
None => {
|
||||
ctx.say("Your server has not been setup by a bot admin yet! Please context a bot admin or azuki to get authorised.").await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
1216
src/commands/database.rs
Normal file
1216
src/commands/database.rs
Normal file
File diff suppressed because it is too large
Load diff
176
src/commands/fun.rs
Normal file
176
src/commands/fun.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
use core::time;
|
||||
|
||||
use poise::serenity_prelude::{self as serenity, AttachmentType};
|
||||
use rusted_fbt_lib::enums::WaifuTypes;
|
||||
use rusted_fbt_lib::types::{Context, Error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::time::sleep;
|
||||
use tracing::instrument;
|
||||
use uwuifier::uwuify_str_sse;
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(prefix_command, slash_command, category = "Fun", member_cooldown = 15)]
|
||||
/// This user is cringe
|
||||
pub async fn cringe(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Optionally call this user cringe"] user: Option<serenity::User>,
|
||||
) -> Result<(), Error> {
|
||||
let camera_message = ctx.say("<a:camera:870459823907553352>").await?;
|
||||
|
||||
sleep(time::Duration::from_secs(1)).await;
|
||||
|
||||
camera_message
|
||||
.edit(ctx, |b| {
|
||||
b.content("<a:camera_with_flash:870458599325986898>")
|
||||
})
|
||||
.await?;
|
||||
|
||||
sleep(time::Duration::from_secs(1)).await;
|
||||
|
||||
camera_message
|
||||
.edit(ctx, |b| b.content("<a:camera:870459823907553352>"))
|
||||
.await?;
|
||||
|
||||
match user {
|
||||
None => {
|
||||
ctx.say("Yep, that's going in my cringe compilation")
|
||||
.await?;
|
||||
}
|
||||
Some(user) => {
|
||||
ctx.send(|m| {
|
||||
m.content(format!(
|
||||
"Yep <@{}>, that's going in my cringe compilation",
|
||||
user.id
|
||||
))
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// OwOifys your message
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(prefix_command, slash_command, category = "Fun")]
|
||||
pub async fn owo(ctx: Context<'_>, #[description = "Message"] msg: String) -> Result<(), Error> {
|
||||
ctx.say(uwuify_str_sse(msg.as_str())).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replies with pog pog pog and pog frog!
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(prefix_command, slash_command, category = "Fun", member_cooldown = 10)]
|
||||
pub async fn pog(ctx: Context<'_>) -> Result<(), Error> {
|
||||
ctx.send(|f| {
|
||||
f.content("Pog pog pog!")
|
||||
.ephemeral(false)
|
||||
.attachment(AttachmentType::Bytes {
|
||||
data: std::borrow::Cow::Borrowed(include_bytes!("../../assets/pog-frog.gif")),
|
||||
filename: String::from("pog-frog.gif"),
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a random waifu (SFW)
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(slash_command, category = "Fun", member_cooldown = 5)]
|
||||
pub async fn waifu(
|
||||
ctx: Context<'_>,
|
||||
#[description = "What waifu do you want?"] waifu_type: Option<WaifuTypes>,
|
||||
) -> Result<(), Error> {
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
struct Waifu {
|
||||
url: String,
|
||||
}
|
||||
|
||||
let choice: String = match waifu_type {
|
||||
None => "waifu".to_string(),
|
||||
Some(WaifuTypes::Neko) => "neko".to_string(),
|
||||
Some(WaifuTypes::Megumin) => "megumin".to_string(),
|
||||
Some(WaifuTypes::Bully) => "bully".to_string(),
|
||||
Some(WaifuTypes::Cuddle) => "cuddle".to_string(),
|
||||
Some(WaifuTypes::Cry) => "cry".to_string(),
|
||||
Some(WaifuTypes::Kiss) => "kiss".to_string(),
|
||||
Some(WaifuTypes::Lick) => "lick".to_string(),
|
||||
Some(WaifuTypes::Pat) => "pat".to_string(),
|
||||
Some(WaifuTypes::Smug) => "smug".to_string(),
|
||||
Some(WaifuTypes::Bonk) => "bonk".to_string(),
|
||||
Some(WaifuTypes::Blush) => "blush".to_string(),
|
||||
Some(WaifuTypes::Smile) => "smile".to_string(),
|
||||
Some(WaifuTypes::Wave) => "wave".to_string(),
|
||||
Some(WaifuTypes::Highfive) => "highfive".to_string(),
|
||||
Some(WaifuTypes::Handhold) => "handhold".to_string(),
|
||||
Some(WaifuTypes::Nom) => "nom".to_string(),
|
||||
Some(WaifuTypes::Bite) => "bite".to_string(),
|
||||
Some(WaifuTypes::Glomp) => "glomp".to_string(),
|
||||
Some(WaifuTypes::Slap) => "slap".to_string(),
|
||||
Some(WaifuTypes::Kill) => "kill".to_string(),
|
||||
Some(WaifuTypes::Happy) => "happy".to_string(),
|
||||
Some(WaifuTypes::Wink) => "wink".to_string(),
|
||||
Some(WaifuTypes::Poke) => "poke".to_string(),
|
||||
Some(WaifuTypes::Dance) => "dance".to_string(),
|
||||
Some(WaifuTypes::Cringe) => "cringe".to_string(),
|
||||
};
|
||||
|
||||
let response = reqwest::get(format!("https://api.waifu.pics/sfw/{choice}")).await?;
|
||||
let waifu: Waifu = response.json().await?;
|
||||
|
||||
// let waifu: Waifu = serde_json::from_str(json_content.as_str())?;
|
||||
|
||||
ctx.send(|b| b.embed(|e| e.title("Your waifu:").image(waifu.url)))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replies with pong!
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(prefix_command, slash_command, category = "Fun", member_cooldown = 15)]
|
||||
pub async fn ping(ctx: Context<'_>) -> Result<(), Error> {
|
||||
ctx.say("Pong! 🏓").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// That's toxic
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(slash_command, category = "Fun", member_cooldown = 15)]
|
||||
pub async fn toxic(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Optionally call this user cringe"] user: Option<serenity::User>,
|
||||
) -> Result<(), Error> {
|
||||
const GIF_NAME: &str = "toxic.gif";
|
||||
let toxic_gif = include_bytes!("../../assets/toxic.gif");
|
||||
|
||||
ctx.defer().await?;
|
||||
|
||||
match user {
|
||||
None => {
|
||||
ctx.send(|m| {
|
||||
m.content("That's toxic!")
|
||||
.attachment(AttachmentType::Bytes {
|
||||
data: std::borrow::Cow::Borrowed(toxic_gif), // include_bytes! directly embeds the gif file into the executable at compile time.
|
||||
filename: GIF_NAME.to_string(),
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
Some(user) => {
|
||||
ctx.send(|m| {
|
||||
m.content(format!("<@{}>, That's toxic!", user.id.as_u64()))
|
||||
.attachment(AttachmentType::Bytes {
|
||||
data: std::borrow::Cow::Borrowed(toxic_gif), // include_bytes! directly embeds the gif file into the executable at compile time.
|
||||
filename: GIF_NAME.to_string(),
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
147
src/commands/info.rs
Normal file
147
src/commands/info.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
use poise::serenity_prelude::{ChannelId, Colour};
|
||||
use rand::Rng;
|
||||
use rusted_fbt_lib::{
|
||||
types::{Context, Error},
|
||||
utils::open_redis_connection,
|
||||
vars::{FEEDBACK_CHANNEL_ID, HELP_EXTRA_TEXT, VERSION},
|
||||
};
|
||||
#[cfg(feature = "database")]
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tracing::instrument;
|
||||
use tracing::{event, Level};
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(prefix_command, track_edits, slash_command, category = "Info")]
|
||||
/// Show this help menu
|
||||
pub async fn help(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Specific command to show help about"]
|
||||
#[autocomplete = "poise::builtins::autocomplete_command"]
|
||||
command: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
poise::builtins::help(
|
||||
ctx,
|
||||
command.as_deref(),
|
||||
poise::builtins::HelpConfiguration {
|
||||
extra_text_at_bottom: HELP_EXTRA_TEXT,
|
||||
show_context_menu_commands: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(slash_command, category = "Info", member_cooldown = 10, ephemeral)]
|
||||
/// Provide feedback for the bot team to look at!
|
||||
pub async fn feedback(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Feedback you want to provide"] feedback: String,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer_ephemeral().await?;
|
||||
let mut con = open_redis_connection().await?;
|
||||
|
||||
redis::cmd("SET")
|
||||
.arg(format!(
|
||||
"feedback:{}-{}-{}",
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
ctx.author().id.as_u64(),
|
||||
ctx.author().tag()
|
||||
))
|
||||
.arg(feedback.clone())
|
||||
.clone()
|
||||
.query_async(&mut con)
|
||||
.await?;
|
||||
|
||||
let colour = &mut rand::thread_rng().gen_range(0..10_000_000);
|
||||
ChannelId(FEEDBACK_CHANNEL_ID)
|
||||
.send_message(ctx, |f| {
|
||||
f.embed(|e| {
|
||||
e.title("New feedback!".to_string())
|
||||
.description(feedback)
|
||||
.color(Colour::new(*colour))
|
||||
.author(|a| a.icon_url(ctx.author().face()).name(ctx.author().tag()))
|
||||
.thumbnail("https://media.giphy.com/media/U4sfHXAALLYBQzPcWk/giphy.gif")
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
ctx.say("Thank you for the feedback! It has been sent directly to our developers.")
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(prefix_command, slash_command, category = "Info", member_cooldown = 10)]
|
||||
/// Have some info about the bot
|
||||
pub async fn about(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let guild_count = ctx
|
||||
.serenity_context()
|
||||
.cache
|
||||
.guilds()
|
||||
.len()
|
||||
.clone()
|
||||
.to_string();
|
||||
|
||||
// TODO: change to your own URLs
|
||||
let mut fields = vec", false),
|
||||
("Remove any of your info from the bot:", "[Delete your data](https://fbtsecurity.fbtheaven.com/data-and-privacy-policy#delete-your-data)", false),
|
||||
("Bot version:", VERSION.unwrap_or("unknown"), false),
|
||||
("Server count:", &guild_count, false),
|
||||
];
|
||||
|
||||
// TODO: reduce the ammount of #[cfg(feature = "database")] here!!
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
let mut con = open_redis_connection().await?;
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
let execution_count: String = redis::cmd("GET")
|
||||
.arg("status:commands-executed")
|
||||
.clone()
|
||||
.query_async(&mut con)
|
||||
.await?;
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
let mut new_field = vec![(
|
||||
"Total commands run since 2.0.18:",
|
||||
execution_count.as_str(),
|
||||
false,
|
||||
)];
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
fields.append(&mut new_field);
|
||||
|
||||
// TODO: change to your own URLs
|
||||
ctx.send(|f| {
|
||||
f.embed(|e| {
|
||||
e.title("About")
|
||||
.url("https://fbtsecurity.fbtheaven.com/")
|
||||
.author(|a| {
|
||||
a.name("FBT Staff")
|
||||
.url("https://fbtsecurity.fbtheaven.com/")
|
||||
})
|
||||
.fields(fields)
|
||||
.footer(|foot| {
|
||||
foot.text("Time mojo spent on V2.0+")
|
||||
})
|
||||
.image(format!("https://wakatime.com/badge/user/fd57ff6b-f3f1-4957-b9c6-7e09bc3f0559/project/d2f87f17-8c44-4835-b4f6-f0089e52515f.png?rand={}", rand::thread_rng().gen_range(0..1_000_000_000)))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
event!(
|
||||
Level::INFO,
|
||||
"Total commands run since 2.0.18" = execution_count.parse::<u32>().unwrap() + 1
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
6
src/commands/mod.rs
Normal file
6
src/commands/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod admin;
|
||||
pub mod database;
|
||||
pub mod fun;
|
||||
pub mod info;
|
||||
pub mod tickets;
|
||||
pub mod tools;
|
||||
225
src/commands/tickets.rs
Normal file
225
src/commands/tickets.rs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
use poise::serenity_prelude::{self as serenity};
|
||||
use poise::serenity_prelude::{ChannelId, RoleId};
|
||||
use poise::serenity_prelude::{PermissionOverwrite, PermissionOverwriteType, Permissions};
|
||||
use rusted_fbt_lib::enums::CloseTicketFail;
|
||||
use rusted_fbt_lib::types::{Context, Error};
|
||||
use rusted_fbt_lib::utils::verbose_mode;
|
||||
use rusted_fbt_lib::vars::{CLOSED_TICKET_CATEGORY, FBT_GUILD_ID, TICKET_CATEGORY};
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(slash_command, category = "Ticket", guild_only)]
|
||||
/// Create new ticket (FBT discord only!)
|
||||
pub async fn new_ticket(
|
||||
ctx: Context<'_>,
|
||||
#[description = "An optional topic to put on the ticket"] topic: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer_ephemeral().await?;
|
||||
|
||||
if *ctx.guild_id().unwrap().as_u64() == FBT_GUILD_ID {
|
||||
let mut channels = Vec::new();
|
||||
|
||||
for channel in ctx.guild().unwrap().channels {
|
||||
let parent_id = match channel.1.clone() {
|
||||
serenity::Channel::Guild(g) => g.parent_id,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(cat) = parent_id {
|
||||
if *cat.as_u64() == TICKET_CATEGORY {
|
||||
channels.push(channel.0.name(ctx).await.unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut existing_ticket = false;
|
||||
|
||||
for ch in channels {
|
||||
if ch.starts_with(&ctx.author().name) {
|
||||
existing_ticket = true;
|
||||
}
|
||||
}
|
||||
|
||||
if existing_ticket {
|
||||
ctx.send(|b| {
|
||||
b.content("You already have a ticket open, you cannot open another!")
|
||||
.ephemeral(true)
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
|
||||
// TODO: change these IDs to be your own server roles
|
||||
|
||||
// Assuming a guild has already been bound.
|
||||
let perms = vec![
|
||||
PermissionOverwrite {
|
||||
allow: Permissions::READ_MESSAGE_HISTORY
|
||||
| Permissions::VIEW_CHANNEL
|
||||
| Permissions::SEND_MESSAGES
|
||||
| Permissions::ADD_REACTIONS
|
||||
| Permissions::EMBED_LINKS
|
||||
| Permissions::ATTACH_FILES
|
||||
| Permissions::USE_EXTERNAL_EMOJIS,
|
||||
deny: Permissions::empty(),
|
||||
kind: PermissionOverwriteType::Member(ctx.author().id),
|
||||
},
|
||||
PermissionOverwrite {
|
||||
allow: Permissions::all(),
|
||||
deny: Permissions::SEND_TTS_MESSAGES,
|
||||
kind: PermissionOverwriteType::Role(RoleId::from(737_168_134_569_590_888)), // Secretary (Probably not needed)
|
||||
},
|
||||
PermissionOverwrite {
|
||||
allow: Permissions::all(),
|
||||
deny: Permissions::SEND_TTS_MESSAGES,
|
||||
kind: PermissionOverwriteType::Role(RoleId::from(820_914_502_220_513_330)), // Admin (Probably not needed)
|
||||
},
|
||||
PermissionOverwrite {
|
||||
allow: Permissions::all(),
|
||||
deny: Permissions::SEND_TTS_MESSAGES,
|
||||
kind: PermissionOverwriteType::Role(RoleId::from(874_898_210_534_096_907)), // Mods
|
||||
},
|
||||
PermissionOverwrite {
|
||||
allow: Permissions::all(),
|
||||
deny: Permissions::SEND_TTS_MESSAGES,
|
||||
kind: PermissionOverwriteType::Role(RoleId::from(1_005_994_060_416_294_942)), // World admin panel
|
||||
},
|
||||
PermissionOverwrite {
|
||||
allow: Permissions::all(),
|
||||
deny: Permissions::SEND_TTS_MESSAGES,
|
||||
kind: PermissionOverwriteType::Role(RoleId::from(1_046_937_023_400_919_091)), // World admin panel trainee
|
||||
},
|
||||
PermissionOverwrite {
|
||||
allow: Permissions::empty(),
|
||||
deny: Permissions::all(),
|
||||
kind: PermissionOverwriteType::Role(RoleId::from(737_168_134_502_350_849)), // @everyone
|
||||
},
|
||||
];
|
||||
|
||||
match ctx
|
||||
.guild()
|
||||
.expect("")
|
||||
.create_channel(ctx, |c| {
|
||||
c.category(ChannelId::from(TICKET_CATEGORY))
|
||||
.name(format!(
|
||||
"{}-{}",
|
||||
ctx.author().name,
|
||||
chrono::offset::Utc::now().format("%s")
|
||||
))
|
||||
.permissions(perms)
|
||||
.topic(topic.unwrap_or_else(|| "A new ticket".to_string()))
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(ch) => {
|
||||
ctx.send(|b| {
|
||||
b.content(format!(
|
||||
"Ticket created! Find it here: <#{}>",
|
||||
ch.id.as_u64()
|
||||
))
|
||||
.ephemeral(true)
|
||||
})
|
||||
.await?;
|
||||
|
||||
ch.say(
|
||||
ctx,
|
||||
format!("New ticket opened by <@{}>!", ctx.author().id.as_u64()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Err(error) => {
|
||||
let err_msg = if verbose_mode() {
|
||||
format!("Failed to create ticket. Reason: {error:?}")
|
||||
} else {
|
||||
"Failed to create ticket".to_string()
|
||||
};
|
||||
|
||||
ctx.send(|b| b.content(err_msg).ephemeral(true)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command, category = "Ticket", guild_only)]
|
||||
/// Closes the current ticket (FBT discord only!)
|
||||
pub async fn close_ticket(ctx: Context<'_>) -> Result<(), Error> {
|
||||
ctx.defer_ephemeral().await?;
|
||||
|
||||
if *ctx.guild_id().unwrap().as_u64() == FBT_GUILD_ID {
|
||||
let mut failed = CloseTicketFail::False;
|
||||
|
||||
let current_channel = ctx.channel_id().to_channel(ctx).await?;
|
||||
let chnnl_name = ctx
|
||||
.channel_id()
|
||||
.name(ctx)
|
||||
.await
|
||||
.unwrap_or_else(|| "Unkown Ticket".to_string());
|
||||
|
||||
let parent_id = match current_channel {
|
||||
serenity::Channel::Guild(g) => g.parent_id,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
match parent_id {
|
||||
None => {
|
||||
failed = CloseTicketFail::False;
|
||||
}
|
||||
Some(channel_category) => {
|
||||
if *channel_category.as_u64() == TICKET_CATEGORY {
|
||||
match ctx
|
||||
.channel_id()
|
||||
.edit(ctx, |c| {
|
||||
c.category(Some(ChannelId::from(CLOSED_TICKET_CATEGORY)))
|
||||
.name(format!(
|
||||
"{}-{}",
|
||||
chnnl_name,
|
||||
chrono::offset::Utc::now().format("%s")
|
||||
))
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(fail_reason) => {
|
||||
failed = CloseTicketFail::SerenityError(fail_reason);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
failed = CloseTicketFail::IncorrectCategory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match failed {
|
||||
CloseTicketFail::False => {
|
||||
ctx.say("Ticket closed!").await?;
|
||||
}
|
||||
CloseTicketFail::IncorrectCategory => {
|
||||
ctx.send(|b| {
|
||||
b.content(format!(
|
||||
"This can only be ran inside of a channel under <#{}>!",
|
||||
TICKET_CATEGORY
|
||||
))
|
||||
.ephemeral(true)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
CloseTicketFail::SerenityError(error) => {
|
||||
ctx.send(|b| {
|
||||
b.content(format!(
|
||||
"Failed to close ticker because of following error:\n{}",
|
||||
error
|
||||
))
|
||||
.ephemeral(true)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.say("This command must be ran inside of FBT's discord")
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
320
src/commands/tools.rs
Normal file
320
src/commands/tools.rs
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use poise::serenity_prelude::{self as serenity, AttachmentType, RichInvite};
|
||||
use rusted_fbt_lib::{
|
||||
checks::guild_auth_check,
|
||||
types::{Context, Error},
|
||||
utils::snowflake_to_unix,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::commands::database::check_username_against_db;
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(slash_command, track_edits, category = "Tools")]
|
||||
/// Display your or another user's account creation date
|
||||
pub async fn account_age(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Selected user"] user: Option<serenity::User>,
|
||||
) -> Result<(), Error> {
|
||||
let user = user.as_ref().unwrap_or_else(|| ctx.author());
|
||||
|
||||
let uid = *user.id.as_u64();
|
||||
|
||||
let unix_timecode = snowflake_to_unix(u128::from(uid));
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
// this shouldn't be able to break but just in case I'm making the `unwrap_or` output NaiveDateTime::MIN
|
||||
let date_time_stamp =
|
||||
NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0).unwrap_or(NaiveDateTime::MIN);
|
||||
|
||||
let age = chrono::Utc::now()
|
||||
.naive_utc()
|
||||
.signed_duration_since(date_time_stamp)
|
||||
.num_days();
|
||||
|
||||
ctx.say(format!(
|
||||
"{}'s account was created at {}.\nSo They are {} days old.",
|
||||
user.name,
|
||||
user.created_at(),
|
||||
age
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the creation date or a Snowflake ID
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(prefix_command, slash_command, category = "Tools")]
|
||||
pub async fn creation_date(
|
||||
ctx: Context<'_>,
|
||||
#[description = "ID of User/Message/Channel/ect"] snowflake_id: u128,
|
||||
) -> Result<(), Error> {
|
||||
let unix_timecode = snowflake_to_unix(snowflake_id);
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
// this shouldn't be able to break but just in case I'm making the `unwrap_or` output NaiveDateTime::MIN
|
||||
let date_time_stamp =
|
||||
NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0).unwrap_or(NaiveDateTime::MIN);
|
||||
|
||||
ctx.say(format!("Created/Joined on {date_time_stamp}"))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// qmit
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(owners_only, slash_command, hide_in_help)]
|
||||
pub async fn bot_owner_tool_1(ctx: Context<'_>) -> Result<(), Error> {
|
||||
ctx.defer_ephemeral().await?;
|
||||
|
||||
let guild_list = ctx.serenity_context().cache.guilds();
|
||||
|
||||
let mut invites: Vec<RichInvite> = Vec::new();
|
||||
|
||||
for guild in guild_list {
|
||||
let guild_invites: Option<Vec<RichInvite>> = (guild.invites(ctx).await).ok();
|
||||
|
||||
if guild_invites.clone().is_some() {
|
||||
invites.append(&mut guild_invites.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
// let shit_list = format!("All invites the bot can see:\n\n{:?}", invites);
|
||||
|
||||
let mut new_list: String = "Every invite the bot can see, grouped by guild:\n\n[\n".to_string();
|
||||
|
||||
for invite in invites {
|
||||
new_list.push_str(format!("{},\n", serde_json::to_string(&invite)?).as_str());
|
||||
}
|
||||
|
||||
new_list.push(']');
|
||||
|
||||
ctx.send(|b| {
|
||||
b.content("All bot invites:".to_string())
|
||||
.attachment(AttachmentType::Bytes {
|
||||
data: std::borrow::Cow::Borrowed(new_list.as_bytes()),
|
||||
filename: format!("{}_invites.txt", ctx.id()),
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get's all avaliable info from a Discord Invite
|
||||
#[instrument(skip(ctx))]
|
||||
#[poise::command(
|
||||
prefix_command,
|
||||
slash_command,
|
||||
category = "Tools",
|
||||
member_cooldown = 5,
|
||||
check = "guild_auth_check",
|
||||
guild_only
|
||||
)]
|
||||
pub async fn invite_info(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Invite URL"] invite_url: String,
|
||||
) -> Result<(), Error> {
|
||||
use linkify::LinkFinder;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct InviteObject {
|
||||
#[serde(rename(deserialize = "type"))]
|
||||
_type: u64,
|
||||
code: String,
|
||||
inviter: InviterObject,
|
||||
expires_at: Option<String>,
|
||||
guild: PartialGuild,
|
||||
guild_id: String,
|
||||
channel: PartialChannel,
|
||||
approximate_member_count: u64,
|
||||
approximate_presence_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct InviterObject {
|
||||
id: String,
|
||||
username: String,
|
||||
#[allow(dead_code)]
|
||||
avatar: Option<String>,
|
||||
discriminator: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
public_flags: u64,
|
||||
#[allow(dead_code)]
|
||||
flags: u64,
|
||||
#[allow(dead_code)]
|
||||
banner: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
accent_color: Option<u64>,
|
||||
global_name: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
avatar_decoration_data: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
banner_color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct PartialGuild {
|
||||
#[allow(dead_code)]
|
||||
id: String,
|
||||
name: String,
|
||||
#[allow(dead_code)]
|
||||
splash: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
banner: Option<String>,
|
||||
description: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
icon: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
features: Vec<String>,
|
||||
#[allow(dead_code)]
|
||||
verification_level: u64,
|
||||
vanity_url_code: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
nsfw_level: u64,
|
||||
#[allow(dead_code)]
|
||||
nsfw: bool,
|
||||
premium_subscription_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct PartialChannel {
|
||||
id: String,
|
||||
#[serde(rename(deserialize = "type"))]
|
||||
_type: u64,
|
||||
name: String,
|
||||
}
|
||||
|
||||
let finder = LinkFinder::new();
|
||||
let links: Vec<_> = finder.links(&invite_url).collect();
|
||||
|
||||
if links.is_empty() {
|
||||
ctx.say("No valid links found").await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let link_str = links[0].as_str().to_owned();
|
||||
|
||||
let (_link, invite_code) = link_str.split_at(19);
|
||||
|
||||
let response = reqwest::get(format!(
|
||||
"https://discord.com/api/v10/invites/{invite_code}?with_counts=true"
|
||||
))
|
||||
.await?;
|
||||
let response_formatted: Option<InviteObject> = response.json().await?;
|
||||
|
||||
if response_formatted.is_none() {
|
||||
ctx.say("Invite not found").await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let invite = response_formatted.unwrap();
|
||||
|
||||
let invite_info_fields = vec![
|
||||
("Code:", invite.code, false),
|
||||
("Expires:", invite.expires_at.unwrap_or_default(), false),
|
||||
("Destination channel name:", invite.channel.name, false),
|
||||
("Destination channel ID:", invite.channel.id, false),
|
||||
];
|
||||
|
||||
let guild_info_fields = vec![
|
||||
("Server name:", invite.guild.name, false),
|
||||
("Server ID:", invite.guild_id, false),
|
||||
(
|
||||
"Server Description:",
|
||||
invite.guild.description.unwrap_or_default(),
|
||||
true,
|
||||
),
|
||||
(
|
||||
"Vanity URL code:",
|
||||
invite.guild.vanity_url_code.unwrap_or_default(),
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Server boosts count:",
|
||||
format!("{}", invite.guild.premium_subscription_count),
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Approx member count:",
|
||||
format!("{}", invite.approximate_member_count),
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Approx online user count:",
|
||||
format!("{}", invite.approximate_presence_count),
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
let inviter_info_fields = vec![
|
||||
("Username:", invite.inviter.username, false),
|
||||
(
|
||||
"Global username:",
|
||||
invite.inviter.global_name.unwrap_or_default(),
|
||||
false,
|
||||
),
|
||||
("User ID:", invite.inviter.id.clone(), false),
|
||||
(
|
||||
"Discriminator(Eg: #0001):",
|
||||
invite.inviter.discriminator.unwrap_or_default(),
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
ctx.send(|f| {
|
||||
f.embed(|e| e.title("Invite Info").fields(invite_info_fields))
|
||||
.embed(|e| e.title("Guild Info").fields(guild_info_fields))
|
||||
.embed(|e| e.title("Inviter Info").fields(inviter_info_fields))
|
||||
})
|
||||
.await?;
|
||||
|
||||
let unix_timecode = snowflake_to_unix(u128::from(ctx.author().id.0));
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
// this shouldn't be able to break but just in case I'm making the `unwrap_or` output NaiveDateTime::MIN
|
||||
let date_time_stamp =
|
||||
NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0).unwrap_or(NaiveDateTime::MIN);
|
||||
|
||||
let age = chrono::Utc::now()
|
||||
.naive_utc()
|
||||
.signed_duration_since(date_time_stamp)
|
||||
.num_days();
|
||||
|
||||
let is_user_in_db: Option<String> =
|
||||
check_username_against_db(invite.inviter.id.parse::<u64>().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
// TODO: set your own channel ID!
|
||||
|
||||
// log user name, id, guild name, id and url to channel
|
||||
serenity::ChannelId()
|
||||
.send_message(ctx, |f| {
|
||||
f.embed(|e| {
|
||||
e.title("User requested invite info")
|
||||
.field("Username", ctx.author().name.clone(), true)
|
||||
.field("User ID", ctx.author().id.0.to_string(), true)
|
||||
.field("User Account age (days)", age, true)
|
||||
.field("Source Server Name", ctx.guild().unwrap().name, true)
|
||||
.field(
|
||||
"Source Server ID",
|
||||
ctx.guild().unwrap().id.0.to_string(),
|
||||
true,
|
||||
)
|
||||
.field("Url provided", link_str, true)
|
||||
.field(
|
||||
"Is User in DB",
|
||||
format!("{}", is_user_in_db.is_some()),
|
||||
false,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue