v1.0
This commit is contained in:
159
src/commands/action.rs
Normal file
159
src/commands/action.rs
Normal file
@ -0,0 +1,159 @@
|
||||
use crate::{utils, Context, Error};
|
||||
use poise::CreateReply;
|
||||
use serenity::all::{CreateAllowedMentions, CreateAttachment, Message};
|
||||
use serenity::builder::CreateMessage;
|
||||
use serenity::model::prelude::UserId;
|
||||
use serenity::prelude::Mentionable;
|
||||
|
||||
async fn check_if_allowed(ctx: Context<'_>, message: Message) -> Result<bool, Error> {
|
||||
let user = ctx
|
||||
.data()
|
||||
.database_controller
|
||||
.get_user_by_discord_id(message.author.id.into())
|
||||
.await?;
|
||||
|
||||
if user.is_none() {
|
||||
ctx.data()
|
||||
.database_controller
|
||||
.create_user(message.author.id.into())
|
||||
.await?;
|
||||
}
|
||||
|
||||
let user = ctx
|
||||
.data()
|
||||
.database_controller
|
||||
.get_user_by_discord_id(message.author.id.into())
|
||||
.await?;
|
||||
|
||||
Ok(user.unwrap().actions_allowed)
|
||||
}
|
||||
|
||||
#[poise::command(context_menu_command = "Hug User")]
|
||||
pub async fn use_action_hug(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The target message to use the action with"] message: Message,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
|
||||
let allowed = check_if_allowed(ctx, message.clone()).await?;
|
||||
|
||||
if !allowed {
|
||||
ctx.send(CreateReply::default().content(
|
||||
":x: Sorry, either that user has disabled actions or has no profile to check against",
|
||||
).ephemeral(true))
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let action_img = utils::get_random_action_image("hug".to_string()).await;
|
||||
|
||||
// Match the result of the action image
|
||||
match action_img {
|
||||
Ok(img) => {
|
||||
let user = UserId::new(ctx.author().id.into());
|
||||
let target = UserId::new(message.clone().author.id.into());
|
||||
let builder = CreateReply::default()
|
||||
.content(format!("{} hugs {}", user.mention(), target.mention()))
|
||||
.attachment(CreateAttachment::url(ctx.http(), &img).await?)
|
||||
.allowed_mentions(CreateAllowedMentions::default().users(vec![user, target]));
|
||||
|
||||
ctx.send(builder).await?;
|
||||
}
|
||||
Err(_) => {
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.content(":x: Something went wrong while fetching the action image")
|
||||
.ephemeral(true),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(context_menu_command = "Kiss User")]
|
||||
pub async fn use_action_kiss(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The target message to use the action with"] message: Message,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
let allowed = check_if_allowed(ctx, message.clone()).await?;
|
||||
|
||||
if !allowed {
|
||||
ctx.send(CreateReply::default().content(
|
||||
":x: Sorry, either that user has disabled actions or has no profile to check against",
|
||||
).ephemeral(true))
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let action_img = utils::get_random_action_image("kiss".to_string()).await;
|
||||
|
||||
// Match the result of the action image
|
||||
match action_img {
|
||||
Ok(img) => {
|
||||
let user = UserId::new(ctx.author().id.into());
|
||||
let target = UserId::new(message.author.id.into());
|
||||
let builder = CreateReply::default()
|
||||
.content(format!("{} kisses {}", user.mention(), target.mention()))
|
||||
.attachment(CreateAttachment::url(ctx.http(), &img).await?)
|
||||
.allowed_mentions(CreateAllowedMentions::default().users(vec![user, target]));
|
||||
|
||||
ctx.send(builder).await?;
|
||||
}
|
||||
Err(_) => {
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.content(":x: Something went wrong while fetching the action image")
|
||||
.ephemeral(true),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(context_menu_command = "Pat User")]
|
||||
pub async fn use_action_pat(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The target message to use the action with"] message: Message,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
let allowed = check_if_allowed(ctx, message.clone()).await?;
|
||||
|
||||
if !allowed {
|
||||
ctx.send(CreateReply::default().content(
|
||||
":x: Sorry, either that user has disabled actions or has no profile to check against",
|
||||
).ephemeral(true))
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let action_img = utils::get_random_action_image("pat".to_string()).await;
|
||||
|
||||
// Match the result of the action image
|
||||
match action_img {
|
||||
Ok(img) => {
|
||||
let user = UserId::new(ctx.author().id.into());
|
||||
let target = UserId::new(message.author.id.into());
|
||||
let builder = CreateReply::default()
|
||||
.content(format!("{} pats {}", user.mention(), target.mention()))
|
||||
.attachment(CreateAttachment::url(ctx.http(), &img).await?)
|
||||
.allowed_mentions(CreateAllowedMentions::default().users(vec![user, target]));
|
||||
|
||||
ctx.send(builder).await?;
|
||||
}
|
||||
Err(_) => {
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.content(":x: Something went wrong while fetching the action image")
|
||||
.ephemeral(true),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
34
src/commands/cta.rs
Normal file
34
src/commands/cta.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use poise::CreateReply;
|
||||
use serenity::{all::CreateEmbed, model::colour};
|
||||
use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
use crate::{Context, Error};
|
||||
|
||||
/// Returns a random cat image, along with a random cat fact.
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn cta(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let cat_fact_res = Client::new()
|
||||
.get("https://catfact.ninja/fact")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let cat_fact: Value = serde_json::from_str(&cat_fact_res.text().await?)?;
|
||||
let cat_fact = cat_fact["fact"].as_str().unwrap();
|
||||
|
||||
let cat_image_res = Client::new()
|
||||
.get("https://api.thecatapi.com/v1/images/search")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let cat_image: Value = serde_json::from_str(&cat_image_res.text().await?)?;
|
||||
let cat_image = cat_image[0]["url"].as_str().unwrap();
|
||||
|
||||
let embed = CreateEmbed::default()
|
||||
.title("Random Cat")
|
||||
.description(cat_fact)
|
||||
.image(cat_image)
|
||||
.color(colour::Colour::from_rgb(0, 255, 255));
|
||||
|
||||
ctx.send(CreateReply::default().embed(embed)).await?;
|
||||
Ok(())
|
||||
}
|
25
src/commands/dog.rs
Normal file
25
src/commands/dog.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use crate::{Context, Error};
|
||||
use poise::CreateReply;
|
||||
use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
use serenity::{all::CreateEmbed, model::colour};
|
||||
|
||||
/// Returns a random dog image.
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn dog(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let dog_res = Client::new()
|
||||
.get("https://dog.ceo/api/breeds/image/random")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let dog: Value = serde_json::from_str(&dog_res.text().await?)?;
|
||||
let dog = dog["message"].as_str().unwrap();
|
||||
|
||||
let embed = CreateEmbed::default()
|
||||
.title("Random Dog")
|
||||
.image(dog)
|
||||
.color(colour::Colour::from_rgb(0, 255, 255));
|
||||
|
||||
ctx.send(CreateReply::default().embed(embed)).await?;
|
||||
Ok(())
|
||||
}
|
@ -1 +1,6 @@
|
||||
pub mod ping;
|
||||
pub mod vouch;
|
||||
pub mod cta;
|
||||
pub mod dog;
|
||||
pub mod profile;
|
||||
pub mod action;
|
@ -1,16 +1,32 @@
|
||||
use poise::CreateReply;
|
||||
use serenity::{all::CreateEmbed, model::colour};
|
||||
|
||||
use crate::{Context, Error};
|
||||
|
||||
/// Pong? Returns the ping of the bot and process uptime
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn ping(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let start = std::time::Instant::now();
|
||||
let msg = ctx.say("Pong?").await?;
|
||||
|
||||
let latency = start.elapsed();
|
||||
msg.edit(
|
||||
ctx,
|
||||
poise::CreateReply::default().content(format!("Pong! Latency: {:?}", latency)),
|
||||
)
|
||||
.await?;
|
||||
// Create pining embed
|
||||
let mut embed = CreateEmbed::default()
|
||||
.title("Ping?")
|
||||
.description("Pinging...")
|
||||
.color(colour::Colour::PURPLE);
|
||||
|
||||
let msg = ctx.send(CreateReply::default().embed(embed)).await?;
|
||||
|
||||
let ping = start.elapsed().as_millis();
|
||||
embed = CreateEmbed::default()
|
||||
.title("Pong!")
|
||||
.description(format!(
|
||||
"Pong! Took {}ms!\nProcess uptime: {:?}",
|
||||
ping,
|
||||
// Round to 2 decimal places
|
||||
(ctx.data().uptime.elapsed().as_secs_f64() * 100.0).floor() / 100.0
|
||||
))
|
||||
.color(colour::Colour::DARK_GREEN);
|
||||
|
||||
msg.edit(ctx, CreateReply::default().embed(embed)).await?; // Edit the message with the ping
|
||||
Ok(())
|
||||
}
|
||||
|
142
src/commands/profile.rs
Normal file
142
src/commands/profile.rs
Normal file
@ -0,0 +1,142 @@
|
||||
use crate::structs::user::User as UserStruct;
|
||||
use crate::{Context, Error};
|
||||
use poise::CreateReply;
|
||||
use serenity::all::{Colour, CreateEmbed, User};
|
||||
|
||||
/// Commands related to profiles in the bot
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
subcommands("view", "edit"),
|
||||
check = "ensure_profile_is_setup"
|
||||
)]
|
||||
pub async fn profiles(_ctx: Context<'_>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure the user has a profile setup
|
||||
/// If the user doesn't have a profile, create one
|
||||
async fn ensure_profile_is_setup(ctx: Context<'_>) -> Result<bool, Error> {
|
||||
let profile = ctx
|
||||
.data()
|
||||
.database_controller
|
||||
.get_user_by_discord_id(ctx.author().id.into())
|
||||
.await?;
|
||||
|
||||
if profile.is_none() {
|
||||
ctx.data()
|
||||
.database_controller
|
||||
.create_user(ctx.author().id.into())
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// View a user's profile
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn view(
|
||||
_ctx: Context<'_>,
|
||||
#[description = "The user to view the profile of, defaults to yourself"] user: Option<User>,
|
||||
) -> Result<(), Error> {
|
||||
// Get the profile of the provided user, or the user who ran the command
|
||||
let target_user = user.unwrap_or_else(|| _ctx.author().clone());
|
||||
|
||||
// Get the profile of the target user
|
||||
let profile = _ctx
|
||||
.data()
|
||||
.database_controller
|
||||
.get_user_by_discord_id(target_user.id.into())
|
||||
.await?;
|
||||
|
||||
match profile {
|
||||
Some(profile) => {
|
||||
let profile_embed = CreateEmbed::default()
|
||||
.title(format!("Profile of {}", target_user.tag()))
|
||||
.description(format!(
|
||||
"About: {}\nPronouns: {}\nActions Allowed: {}",
|
||||
profile.about.unwrap_or("No about section".to_string()),
|
||||
profile.pronouns.unwrap_or("No pronouns set".to_string()),
|
||||
if profile.actions_allowed { "Yes" } else { "No" }
|
||||
))
|
||||
.color(Colour::FABLED_PINK);
|
||||
|
||||
_ctx.send(CreateReply::default().embed(profile_embed))
|
||||
.await?;
|
||||
}
|
||||
None => {
|
||||
// Send a emphul messagrespond(formate if the user doesn't have a profile
|
||||
_ctx.send(
|
||||
CreateReply::default()
|
||||
.content(":x: User has no profile")
|
||||
.ephemeral(true),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Edit your profile
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn edit(
|
||||
_ctx: Context<'_>,
|
||||
#[description = "Your about section"] about: Option<String>,
|
||||
#[description = "Your pronouns"] pronouns: Option<String>,
|
||||
#[description = "Whether you want to allow actions to be performed on you"]
|
||||
actions_allowed: Option<bool>,
|
||||
) -> Result<(), Error> {
|
||||
// If no options are provided, send a help message
|
||||
if about.is_none() && pronouns.is_none() && actions_allowed.is_none() {
|
||||
_ctx.send(
|
||||
CreateReply::default()
|
||||
.content("Please provide at least one option to edit")
|
||||
.ephemeral(true),
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Get the profile of the user who ran the command
|
||||
let profile = _ctx
|
||||
.data()
|
||||
.database_controller
|
||||
.get_user_by_discord_id(_ctx.author().id.into())
|
||||
.await?;
|
||||
|
||||
// If the user doesn't have a profile, create one
|
||||
let profile = match profile {
|
||||
Some(profile) => profile,
|
||||
None => {
|
||||
_ctx.data()
|
||||
.database_controller
|
||||
.create_user(_ctx.author().id.into())
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
// Generate a user struct with the updated values
|
||||
let updated_profile = UserStruct {
|
||||
id: profile.id,
|
||||
discord_id: profile.discord_id,
|
||||
about,
|
||||
pronouns,
|
||||
actions_allowed: actions_allowed.unwrap_or(profile.actions_allowed),
|
||||
};
|
||||
|
||||
// Update the user's profile
|
||||
_ctx.data()
|
||||
.database_controller
|
||||
.update_user(updated_profile)
|
||||
.await?;
|
||||
|
||||
// Send a success message
|
||||
_ctx.send(
|
||||
CreateReply::default()
|
||||
.content(":white_check_mark: Profile updated successfully!")
|
||||
.ephemeral(true),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
303
src/commands/vouch.rs
Normal file
303
src/commands/vouch.rs
Normal file
@ -0,0 +1,303 @@
|
||||
use crate::{structs::vouch::Vouch, Context, Error};
|
||||
use serenity::all::{
|
||||
Colour, CreateAllowedMentions, CreateEmbed, CreateEmbedFooter, CreateMessage, Mentionable, User,
|
||||
};
|
||||
|
||||
/// Commands related to vouching for new users
|
||||
#[poise::command(slash_command, subcommands("submit", "approve", "deny"))]
|
||||
pub async fn vouch(_ctx: Context<'_>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Vouch for a new user
|
||||
#[poise::command(slash_command, guild_only)]
|
||||
pub async fn submit(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The user to vouch for"] user: User,
|
||||
) -> Result<(), Error> {
|
||||
// Defer emphemeral response
|
||||
ctx.defer_ephemeral().await?;
|
||||
|
||||
// Do we have a vouch for this user already?
|
||||
if ctx
|
||||
.data()
|
||||
.vouch_store
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.any(|vouch| vouch.user.id == user.id)
|
||||
{
|
||||
ctx.say(":x: This user already has a vouch pending!")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let guild_id = ctx.guild_id().ok_or("Must be run in a guild")?;
|
||||
let member = guild_id.member(ctx.serenity_context(), user.id).await?;
|
||||
|
||||
// Does the user have a silly role?
|
||||
if member
|
||||
.roles
|
||||
.iter()
|
||||
.any(|role_id| *role_id == ctx.data().config.roles.silly_role)
|
||||
{
|
||||
ctx.say(":x: This user already is a vouched member!")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create a new vouch
|
||||
let vouch = Vouch::new(user.clone(), ctx.author().clone());
|
||||
|
||||
// Add the vouch to the store, we need to create a mutable reference to .data().vouch_store,
|
||||
// because we want to modify the store - and poise context is immutable by default
|
||||
ctx.data().vouch_store.lock().await.push(vouch);
|
||||
|
||||
// Send a messasge to the mod-logs channel with a ping that a new vouch has been submitted
|
||||
let log_msg = format!(
|
||||
"{} {}\n:notepad_spiral: A new vouch has been submitted for {} by {}, please either approve or deny this vouch.",
|
||||
serenity::model::id::RoleId::new(ctx.data().config.roles.mod_role).mention(),
|
||||
serenity::model::id::RoleId::new(ctx.data().config.roles.admin).mention(),
|
||||
user.clone().mention(),
|
||||
ctx.author().mention()
|
||||
);
|
||||
|
||||
let log_channel_id = ctx.data().config.channels.logs_mod;
|
||||
let channel_hash = guild_id.channels(ctx.serenity_context()).await?;
|
||||
let channel = channel_hash.get(&log_channel_id.into()).unwrap();
|
||||
|
||||
channel
|
||||
.send_message(
|
||||
ctx.serenity_context(),
|
||||
CreateMessage::new().content(log_msg).allowed_mentions(
|
||||
CreateAllowedMentions::new().roles(vec![
|
||||
ctx.data().config.roles.mod_role,
|
||||
ctx.data().config.roles.admin,
|
||||
]),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
ctx.say(":white_check_mark: Vouch submitted! An admin will review the vouch when able.")
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Approve a vouch for a user
|
||||
#[poise::command(slash_command, guild_only)]
|
||||
pub async fn approve(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The user to approve the vouch for"] user: User,
|
||||
) -> Result<(), Error> {
|
||||
// Defer emphemeral response
|
||||
ctx.defer_ephemeral().await?;
|
||||
|
||||
// Grab user
|
||||
let author_user = ctx.author_member().await.clone();
|
||||
match author_user {
|
||||
Some(author_user) => {
|
||||
// Check if the author is an admin
|
||||
if !author_user.roles.iter().any(|role_id| {
|
||||
*role_id == ctx.data().config.roles.admin
|
||||
|| *role_id == ctx.data().config.roles.mod_role
|
||||
}) {
|
||||
ctx.say(":x: You must be an admin to approve vouches!")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ctx.say(":x: You must be an admin to approve vouches!")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Do we have a vouch for this user?
|
||||
let mut vouch_store = ctx.data().vouch_store.lock().await;
|
||||
|
||||
// Find the vouch for the user and its index
|
||||
if let Some((vouch_index, vouch)) = vouch_store
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, v)| v.user.id == user.id)
|
||||
.map(|(index, vouch)| (index, vouch.clone()))
|
||||
{
|
||||
// Add the silly role to the user
|
||||
let guild_id = ctx.guild_id().ok_or("Must be run in a guild")?;
|
||||
let member = guild_id.member(ctx.serenity_context(), user.id).await?;
|
||||
|
||||
member
|
||||
.add_role(ctx.serenity_context(), ctx.data().config.roles.silly_role)
|
||||
.await?;
|
||||
|
||||
// Remove the vouch from the store
|
||||
vouch_store.remove(vouch_index);
|
||||
|
||||
// Send a message to the logs channel (mod)
|
||||
let log_msg = format!(
|
||||
":white_check_mark: Vouch approved for {} by {} at {} CDT, vouched by {}",
|
||||
user.mention(),
|
||||
ctx.author().mention(),
|
||||
vouch.get_vouch_time(),
|
||||
vouch.vouched_by.mention()
|
||||
);
|
||||
|
||||
let public_msg = CreateEmbed::default()
|
||||
.title(format!("Welcome to sillycord, {}!", user.tag()))
|
||||
.description("Enjoy your stay in our silly little community!")
|
||||
.footer(CreateEmbedFooter::new(format!(
|
||||
"Vouched by {} - Approved by {}",
|
||||
vouch.vouched_by.tag(),
|
||||
ctx.author().tag()
|
||||
)))
|
||||
.color(Colour::DARK_PURPLE);
|
||||
|
||||
let log_channel_id = ctx.data().config.channels.logs_mod;
|
||||
let public_channel_id = ctx.data().config.channels.main;
|
||||
|
||||
let channels = guild_id.channels(ctx.serenity_context()).await?;
|
||||
let channel = channels.get(&log_channel_id.into()).unwrap();
|
||||
let public_channel = channels.get(&public_channel_id.into()).unwrap();
|
||||
|
||||
channel
|
||||
.send_message(
|
||||
ctx.serenity_context(),
|
||||
CreateMessage::new().content(log_msg),
|
||||
)
|
||||
.await?;
|
||||
|
||||
public_channel
|
||||
.send_message(
|
||||
ctx.serenity_context(),
|
||||
CreateMessage::new()
|
||||
.embed(public_msg)
|
||||
.content(user.mention().to_string())
|
||||
.allowed_mentions(CreateAllowedMentions::new().users(vec![user.id])),
|
||||
)
|
||||
.await?;
|
||||
|
||||
ctx.data()
|
||||
.database_controller
|
||||
.create_user(user.id.into())
|
||||
.await?;
|
||||
ctx.say(":white_check_mark: Vouch approved!").await?;
|
||||
} else {
|
||||
ctx.say(":x: No vouch found for this user!").await?;
|
||||
}
|
||||
|
||||
drop(vouch_store);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deny a vouch for a user
|
||||
#[poise::command(slash_command, guild_only)]
|
||||
pub async fn deny(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The user to deny the vouch for"] user: User,
|
||||
#[flag] kick: bool,
|
||||
#[description = "The reason for denying the vouch"] _reason: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
// Defer emphemeral response
|
||||
ctx.defer_ephemeral().await?;
|
||||
|
||||
// Grab user
|
||||
let author_user = ctx.author_member().await.clone();
|
||||
match author_user {
|
||||
Some(author_user) => {
|
||||
// Check if the author is an admin
|
||||
if !author_user.roles.iter().any(|role_id| {
|
||||
*role_id == ctx.data().config.roles.admin
|
||||
|| *role_id == ctx.data().config.roles.mod_role
|
||||
}) {
|
||||
ctx.say(":x: You must be an admin to deny vouches!").await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ctx.say(":x: You must be an admin to deny vouches!").await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Do we have a vouch for this user?
|
||||
// Do we have a vouch for this user?
|
||||
let mut vouch_store = ctx.data().vouch_store.lock().await;
|
||||
|
||||
// Find the vouch for the user and its index
|
||||
if let Some((vouch_index, vouch)) = vouch_store
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, v)| v.user.id == user.id)
|
||||
.map(|(index, vouch)| (index, vouch.clone()))
|
||||
{
|
||||
// Remove the vouch from the store
|
||||
vouch_store.remove(vouch_index);
|
||||
|
||||
// Send a message to the logs channel (mod)
|
||||
let log_msg = format!(
|
||||
":x: Vouch denied for {} by {} at {} CDT, vouched by {} with a reason of '{}'",
|
||||
user.mention(),
|
||||
ctx.author().mention(),
|
||||
vouch.get_vouch_time(),
|
||||
vouch.vouched_by.mention(),
|
||||
_reason.clone().unwrap_or("No reason provided".to_string())
|
||||
);
|
||||
|
||||
let log_channel_id = ctx.data().config.channels.logs_mod;
|
||||
let guild = ctx.guild_id().unwrap().clone();
|
||||
let channels = guild.channels(ctx.serenity_context()).await?;
|
||||
let channel = channels.get(&log_channel_id.into()).unwrap();
|
||||
|
||||
channel
|
||||
.send_message(
|
||||
ctx.serenity_context(),
|
||||
CreateMessage::new().content(log_msg),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Then kick the user
|
||||
let member = guild.member(ctx.serenity_context(), user.id).await?;
|
||||
|
||||
// Attempt to notify then kick the user
|
||||
let dm_msg = format!(
|
||||
":warning: Your vouch has been denied by {} with a reason of '{}'. If you have any questions, feel free to ask a moderator or admin.",
|
||||
ctx.author().mention(),
|
||||
_reason.unwrap_or("No reason provided".to_string())
|
||||
);
|
||||
|
||||
let notify_result = vouch
|
||||
.user
|
||||
.direct_message(ctx.serenity_context(), CreateMessage::new().content(dm_msg))
|
||||
.await;
|
||||
|
||||
match notify_result {
|
||||
Ok(_) => {
|
||||
if kick {
|
||||
member.kick(ctx.serenity_context()).await?;
|
||||
ctx.say(":white_check_mark: Vouch denied and user kicked! User notified.")
|
||||
.await?;
|
||||
} else {
|
||||
ctx.say(":white_check_mark: Vouch denied! User notified.")
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
if kick {
|
||||
member.kick(ctx.serenity_context()).await?;
|
||||
ctx.say(":white_check_mark: Vouch denied and user kicked! User not notified.")
|
||||
.await?;
|
||||
} else {
|
||||
ctx.say(":white_check_mark: Vouch denied! User not notified.")
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.say(":x: No vouch found for this user!").await?;
|
||||
}
|
||||
|
||||
drop(vouch_store);
|
||||
Ok(())
|
||||
}
|
@ -1,13 +1,16 @@
|
||||
use crate::{Data, Error};
|
||||
use poise::serenity_prelude::{self as serenity, ActivityData, Interaction, OnlineStatus};
|
||||
use tracing::{info, warn};
|
||||
use crate::{handlers, utils::get_rustc_version, Data, Error};
|
||||
use ::serenity::all::{
|
||||
ChannelId, Colour, CreateEmbed, CreateEmbedFooter, CreateMessage, Mentionable,
|
||||
};
|
||||
use poise::serenity_prelude::{self as serenity, ActivityData, OnlineStatus};
|
||||
use tracing::{error, info};
|
||||
|
||||
// Create a span for every event
|
||||
#[tracing::instrument(skip(ctx, event, framework, data))]
|
||||
#[tracing::instrument(skip(ctx, event, _framework, data))]
|
||||
pub async fn event_handler(
|
||||
ctx: &serenity::Context,
|
||||
event: &serenity::FullEvent,
|
||||
framework: poise::FrameworkContext<'_, Data, Error>,
|
||||
_framework: poise::FrameworkContext<'_, Data, Error>,
|
||||
data: &Data,
|
||||
) -> Result<(), Error> {
|
||||
match event {
|
||||
@ -17,6 +20,82 @@ pub async fn event_handler(
|
||||
Some(ActivityData::watching("sillycord")),
|
||||
OnlineStatus::Online,
|
||||
);
|
||||
|
||||
let embed = CreateEmbed::default()
|
||||
.title("Bot Ready!")
|
||||
.description(format!(
|
||||
"Bot is ready! Running on rust version {}, sillycord-bot version {}",
|
||||
get_rustc_version().await,
|
||||
env!("CARGO_PKG_VERSION")
|
||||
))
|
||||
.footer(CreateEmbedFooter::new(
|
||||
"Bot created for sillycord. Made with ❤️ by sticks and others",
|
||||
))
|
||||
.color(Colour::DARK_GREEN);
|
||||
|
||||
let msg = CreateMessage::default().embed(embed);
|
||||
let channel_id = ChannelId::new(data.config.channels.logs_public);
|
||||
|
||||
// We have to use http here to be future safe and not block the event loop
|
||||
ctx.http.send_message(channel_id, vec![], &msg).await?;
|
||||
}
|
||||
|
||||
serenity::FullEvent::GuildMemberAddition { new_member, .. } => {
|
||||
// Is the new user in the main guild?
|
||||
if new_member.guild_id != data.config.main_guild_id {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Handling new user {}", new_member.user.tag());
|
||||
|
||||
ctx.http
|
||||
.send_message(
|
||||
ChannelId::new(data.config.channels.logs_mod),
|
||||
vec![],
|
||||
&CreateMessage::default().content(format!(
|
||||
"<:join:1310407968503894158> New user joined {} - created at: <t:{}:f>",
|
||||
new_member.mention(),
|
||||
new_member.user.created_at().timestamp_millis() / 1000
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let handler_result =
|
||||
handlers::join::join_handler(ctx.clone(), data, new_member.clone()).await;
|
||||
|
||||
if let Err(e) = handler_result {
|
||||
error!(
|
||||
"Error invoking join handler for new user {}: {:?}",
|
||||
new_member.user.tag(),
|
||||
e
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"Join handler completed successfully for new user {}",
|
||||
new_member.user.tag()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
serenity::FullEvent::GuildMemberRemoval { user, .. } => {
|
||||
// We can safely ignore the user's guild_id here, as they are leaving the guild -
|
||||
// because the bot is only in one guild, we know the user is leaving the main guild
|
||||
info!("Handling user leave {}", user.tag());
|
||||
|
||||
data.database_controller
|
||||
.delete_user_by_discord_id(user.id.into())
|
||||
.await?;
|
||||
|
||||
ctx.http
|
||||
.send_message(
|
||||
ChannelId::new(data.config.channels.logs_mod),
|
||||
vec![],
|
||||
&CreateMessage::default().content(format!(
|
||||
"<:leave:1310407968503894158> User left {}",
|
||||
user.mention()
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
serenity::FullEvent::ShardsReady { total_shards, .. } => {
|
||||
|
102
src/handlers/db.rs
Normal file
102
src/handlers/db.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use crate::structs::user::User;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
pub struct DatabaseController {
|
||||
db: MySqlPool,
|
||||
}
|
||||
|
||||
impl DatabaseController {
|
||||
pub fn new(db: MySqlPool) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn get_user_by_discord_id(
|
||||
&self,
|
||||
discord_id: u64,
|
||||
) -> Result<Option<User>, sqlx::Error> {
|
||||
let user = sqlx::query!("SELECT * FROM users WHERE discord_id = ?", discord_id)
|
||||
.fetch_optional(&self.db)
|
||||
.await?;
|
||||
|
||||
match user {
|
||||
Some(user) => Ok(Some(User {
|
||||
id: user.id,
|
||||
discord_id: user
|
||||
.discord_id
|
||||
.parse::<u64>()
|
||||
.map_err(|_| sqlx::Error::Decode("Failed to parse discord_id".into()))?,
|
||||
actions_allowed: user.actions_allowed == Some(1),
|
||||
about: user.about,
|
||||
pronouns: user.pronouns,
|
||||
})),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user_by_id(&self, id: u64) -> Result<Option<User>, sqlx::Error> {
|
||||
let user = sqlx::query!("SELECT * FROM users WHERE id = ?", id)
|
||||
.fetch_optional(&self.db)
|
||||
.await?;
|
||||
|
||||
match user {
|
||||
Some(user) => Ok(Some(User {
|
||||
id: user.id,
|
||||
discord_id: user
|
||||
.discord_id
|
||||
.parse::<u64>()
|
||||
.map_err(|_| sqlx::Error::Decode("Failed to parse discord_id".into()))?,
|
||||
actions_allowed: user.actions_allowed == Some(1),
|
||||
about: user.about,
|
||||
pronouns: user.pronouns,
|
||||
})),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_user(&self, discord_id: u64) -> Result<User, sqlx::Error> {
|
||||
let user = sqlx::query!(
|
||||
"INSERT INTO users (discord_id) VALUES (?)",
|
||||
discord_id.to_string()
|
||||
)
|
||||
.execute(&self.db)
|
||||
.await?;
|
||||
|
||||
Ok(User {
|
||||
id: user.last_insert_id(),
|
||||
discord_id,
|
||||
actions_allowed: true,
|
||||
about: None,
|
||||
pronouns: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_user(&self, user: User) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
"UPDATE users SET actions_allowed = ?, about = ?, pronouns = ? WHERE id = ?",
|
||||
user.actions_allowed as i8,
|
||||
user.about,
|
||||
user.pronouns,
|
||||
user.id
|
||||
)
|
||||
.execute(&self.db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_user(&self, user: User) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM users WHERE id = ?", user.id)
|
||||
.execute(&self.db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_user_by_discord_id(&self, discord_id: u64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM users WHERE discord_id = ?", discord_id)
|
||||
.execute(&self.db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
73
src/handlers/join.rs
Normal file
73
src/handlers/join.rs
Normal file
@ -0,0 +1,73 @@
|
||||
use serenity::all::{
|
||||
ChannelId, Colour, Context, CreateAllowedMentions, CreateEmbed, CreateEmbedFooter,
|
||||
CreateMessage, Member, Mentionable,
|
||||
};
|
||||
use std::error::Error;
|
||||
use tracing::{error, warn};
|
||||
|
||||
#[tracing::instrument(skip(ctx, new_member, data))]
|
||||
pub async fn join_handler(
|
||||
ctx: Context,
|
||||
data: &crate::Data,
|
||||
new_member: Member,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let welcome_msg = CreateEmbed::default()
|
||||
.title("Welcome to sillycord!")
|
||||
.description(format!(
|
||||
"Welcome! To keep sillycord a safe and fun place, we require all newly invited members to be vouched by a current member. Please wait for a member of the community to vouch for you. If you do not receive a vouch within 24 hours, you will be removed from the server. If you have any questions, feel free to ask a moderator or admin.",
|
||||
))
|
||||
.color(Colour::PURPLE)
|
||||
.footer(CreateEmbedFooter::new(
|
||||
"I am a bot, and this action was performed automatically. If you have any questions or concerns, please contact a moderator or admin.",
|
||||
));
|
||||
|
||||
// Try to send the welcome message to the new member
|
||||
let dm_result = new_member
|
||||
.user
|
||||
.direct_message(&ctx, CreateMessage::default().embed(welcome_msg.clone()))
|
||||
.await;
|
||||
|
||||
if let Err(why) = dm_result {
|
||||
warn!(
|
||||
"Failed to send welcome message to {}: {:?} - defaulting to public channel",
|
||||
new_member.user.tag(),
|
||||
why
|
||||
);
|
||||
|
||||
let new_welcome = welcome_msg.clone().footer(CreateEmbedFooter::new(
|
||||
"We were unable to send you a direct message. We've posted the welcome message here instead - please make sure to read it!",
|
||||
));
|
||||
|
||||
let send_result = ctx
|
||||
.http
|
||||
.send_message(
|
||||
ChannelId::new(data.config.channels.welcome),
|
||||
vec![],
|
||||
&CreateMessage::default()
|
||||
.embed(new_welcome)
|
||||
.content(format!(
|
||||
"{} Please make sure to read the welcome message below!",
|
||||
new_member.mention()
|
||||
))
|
||||
.allowed_mentions(CreateAllowedMentions::new().users(vec![new_member.user.id])),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(why) = send_result {
|
||||
error!(
|
||||
"Failed to send welcome message to {} in public channel: {:?}",
|
||||
new_member.user.tag(),
|
||||
why
|
||||
);
|
||||
|
||||
error!(
|
||||
"All options to send welcome message to {} failed - aborting",
|
||||
new_member.user.tag()
|
||||
);
|
||||
|
||||
return Err(why.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
pub mod join;
|
||||
pub mod db;
|
92
src/main.rs
92
src/main.rs
@ -1,10 +1,19 @@
|
||||
mod commands;
|
||||
mod events;
|
||||
mod handlers;
|
||||
mod structs;
|
||||
mod utils;
|
||||
|
||||
use events::event_handler;
|
||||
use serenity::all::{ClientBuilder, GatewayIntents};
|
||||
use handlers::db::DatabaseController;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::{
|
||||
all::{ClientBuilder, GatewayIntents},
|
||||
futures::lock::Mutex,
|
||||
};
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
use sqlx::MySqlPool;
|
||||
use structs::vouch::Vouch;
|
||||
use tracing::{event, info, info_span, Level};
|
||||
|
||||
fn check_required_env_vars() {
|
||||
@ -68,11 +77,37 @@ async fn init_sqlx() -> MySqlPool {
|
||||
}
|
||||
|
||||
struct Data {
|
||||
sqlx_pool: MySqlPool,
|
||||
database_controller: DatabaseController,
|
||||
uptime: std::time::Instant,
|
||||
config: Config,
|
||||
vouch_store: Mutex<Vec<Vouch>>,
|
||||
} // User data, which is stored and accessible in all command invocations
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
pub type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
|
||||
type Context<'a> = poise::Context<'a, Data, Error>;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct Config {
|
||||
main_guild_id: u64,
|
||||
channels: Channels,
|
||||
roles: Roles,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct Channels {
|
||||
welcome: u64,
|
||||
main: u64,
|
||||
logs_public: u64,
|
||||
logs_mod: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct Roles {
|
||||
admin: u64,
|
||||
mod_role: u64,
|
||||
silly_role: u64,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let start_time = std::time::Instant::now();
|
||||
@ -88,15 +123,57 @@ async fn main() {
|
||||
info!("checking required environment variables");
|
||||
check_required_env_vars();
|
||||
|
||||
info!("loading config");
|
||||
// Do we have a config.toml file? If we do, load it
|
||||
// If we don't, create it with the default values, then exit the program and tell the user to fill it out
|
||||
let config: Config = match std::fs::read_to_string("config.toml") {
|
||||
Ok(config) => toml::from_str(&config).expect("failed to parse config.toml"),
|
||||
Err(_) => {
|
||||
let default_config = Config {
|
||||
main_guild_id: 0,
|
||||
channels: Channels {
|
||||
welcome: 0,
|
||||
main: 0,
|
||||
logs_public: 0,
|
||||
logs_mod: 0,
|
||||
},
|
||||
roles: Roles {
|
||||
admin: 0,
|
||||
mod_role: 0,
|
||||
silly_role: 0,
|
||||
},
|
||||
};
|
||||
let default_config_toml = toml::to_string_pretty(&default_config).unwrap();
|
||||
std::fs::write("config.toml", default_config_toml)
|
||||
.expect("failed to write config.toml");
|
||||
event!(Level::WARN, "config.toml not found, created a default one");
|
||||
event!(
|
||||
Level::ERROR,
|
||||
"please fill out config.toml and restart the bot"
|
||||
);
|
||||
|
||||
panic!("config.toml not found, created a default one, please fill it out and restart the bot");
|
||||
}
|
||||
};
|
||||
|
||||
info!("initializing SQLx");
|
||||
let pool = init_sqlx().await;
|
||||
|
||||
info!("initializing bot");
|
||||
let intents = GatewayIntents::non_privileged();
|
||||
let intents = GatewayIntents::privileged();
|
||||
|
||||
let framework = poise::Framework::builder()
|
||||
.options(poise::FrameworkOptions::<Data, Error> {
|
||||
commands: vec![commands::ping::ping()],
|
||||
commands: vec![
|
||||
commands::ping::ping(),
|
||||
commands::vouch::vouch(),
|
||||
commands::profile::profiles(),
|
||||
commands::dog::dog(),
|
||||
commands::cta::cta(),
|
||||
commands::action::use_action_hug(),
|
||||
commands::action::use_action_kiss(),
|
||||
commands::action::use_action_pat(),
|
||||
],
|
||||
event_handler: |ctx, event, framework, data| {
|
||||
Box::pin(event_handler(ctx, event, framework, data))
|
||||
},
|
||||
@ -107,7 +184,10 @@ async fn main() {
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
Ok(Data {
|
||||
// Initialize user data here
|
||||
sqlx_pool: pool,
|
||||
database_controller: DatabaseController::new(pool.clone()),
|
||||
uptime: std::time::Instant::now(),
|
||||
config,
|
||||
vouch_store: Mutex::new(Vec::new()),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
2
src/structs/mod.rs
Normal file
2
src/structs/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod vouch;
|
||||
pub mod user;
|
7
src/structs/user.rs
Normal file
7
src/structs/user.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub struct User {
|
||||
pub id: u64,
|
||||
pub discord_id: u64,
|
||||
pub actions_allowed: bool,
|
||||
pub about: Option<String>,
|
||||
pub pronouns: Option<String>,
|
||||
}
|
27
src/structs/vouch.rs
Normal file
27
src/structs/vouch.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono_tz::US::Central;
|
||||
use serenity::all::User;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Vouch {
|
||||
pub user: User,
|
||||
pub vouched_by: User,
|
||||
pub vouch_time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Vouch {
|
||||
pub fn new(user: User, vouched_by: User) -> Self {
|
||||
Self {
|
||||
user,
|
||||
vouched_by,
|
||||
vouch_time: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_vouch_time(&self) -> String {
|
||||
self.vouch_time
|
||||
.with_timezone(&Central)
|
||||
.format("%Y-%m-%d %H:%M:%S")
|
||||
.to_string()
|
||||
}
|
||||
}
|
30
src/utils.rs
Normal file
30
src/utils.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use std::process::Command;
|
||||
|
||||
pub async fn get_rustc_version() -> String {
|
||||
let rustc_version = Command::new("rustc")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.expect("failed to get rustc version");
|
||||
|
||||
String::from_utf8(rustc_version.stdout).expect("failed to convert rustc version to string")
|
||||
}
|
||||
|
||||
pub async fn get_random_action_image(action: String) -> Result<String, reqwest::Error> {
|
||||
let action = action.to_lowercase();
|
||||
let action = action.replace(" ", "_");
|
||||
|
||||
let url = format!("https://api.otakugifs.xyz/gif?reaction={}", action);
|
||||
let response = reqwest::get(&url)
|
||||
.await
|
||||
.expect("failed to get random action image");
|
||||
|
||||
let response_json: serde_json::Value =
|
||||
serde_json::from_str(&response.text().await.expect("failed to get response text"))
|
||||
.expect("failed to parse response json");
|
||||
|
||||
let image_url = response_json["url"]
|
||||
.as_str()
|
||||
.expect("failed to get image url from response json");
|
||||
|
||||
Ok(image_url.to_string())
|
||||
}
|
Reference in New Issue
Block a user