feat: add /set_welcome command

This commit is contained in:
seth 2024-03-29 17:54:53 -04:00
parent 239928a22a
commit 59bf42998b
No known key found for this signature in database
GPG key ID: D31BD0D494BBEE86
7 changed files with 286 additions and 51 deletions

View file

@ -1,17 +1,8 @@
mod help;
mod joke;
mod members;
mod ping;
mod rory;
mod say;
mod stars;
mod tag;
pub use help::help;
pub use joke::joke;
pub use members::members;
pub use ping::ping;
pub use rory::rory;
pub use say::say;
pub use stars::stars;
pub use tag::tag;
pub mod help;
pub mod joke;
pub mod members;
pub mod ping;
pub mod rory;
pub mod say;
pub mod stars;
pub mod tag;

View file

@ -1,8 +1,8 @@
use crate::Context;
use crate::{utils, Context};
use eyre::{OptionExt, Result};
use log::trace;
use poise::serenity_prelude::{CreateEmbed, CreateEmbedAuthor, CreateMessage};
use poise::serenity_prelude::{CreateEmbed, CreateMessage};
/// Say something through the bot
#[poise::command(
@ -17,7 +17,6 @@ pub async fn say(
ctx: Context<'_>,
#[description = "the message content"] content: String,
) -> Result<()> {
let guild = ctx.guild().ok_or_eyre("Couldn't get guild!")?.to_owned();
let channel = ctx
.guild_channel()
.await
@ -41,19 +40,9 @@ pub async fn say(
.clone()
.discord_config()
.channels()
.say_log_channel_id()
.log_channel_id()
{
let log_channel = guild
.channels
.iter()
.find(|c| c.0 == &channel_id)
.ok_or_eyre("Couldn't get log channel from guild!")?;
let author = CreateEmbedAuthor::new(ctx.author().tag()).icon_url(
ctx.author()
.avatar_url()
.unwrap_or_else(|| ctx.author().default_avatar_url()),
);
let author = utils::embed_author_from_user(ctx.author());
let embed = CreateEmbed::default()
.title("Say command used!")
@ -61,7 +50,7 @@ pub async fn say(
.author(author);
let message = CreateMessage::new().embed(embed);
log_channel.1.send_message(ctx, message).await?;
channel_id.send_message(ctx, message).await?;
} else {
trace!("Not sending /say log as no channel is set");
}

View file

@ -3,18 +3,47 @@ use crate::Data;
use eyre::Report;
mod general;
mod moderation;
macro_rules! command {
($module: ident, $name: ident) => {
$module::$name::$name()
};
($module: ident, $name: ident, $func: ident) => {
$module::$name::$func()
};
}
macro_rules! module_macro {
($module: ident) => {
macro_rules! $module {
($name: ident) => {
command!($module, $name)
};
($name: ident, $func: ident) => {
command!($module, $name, $func)
};
}
};
}
module_macro!(general);
module_macro!(moderation);
pub type Command = poise::Command<Data, Report>;
pub fn get() -> Vec<Command> {
vec![
general::joke(),
general::members(),
general::ping(),
general::rory(),
general::say(),
general::stars(),
general::tag(),
general::help(),
general!(help),
general!(joke),
general!(members),
general!(ping),
general!(rory),
general!(say),
general!(stars),
general!(tag),
moderation!(set_welcome),
]
}

View file

@ -0,0 +1 @@
pub mod set_welcome;

View file

@ -0,0 +1,203 @@
use std::{fmt::Write, str::FromStr};
use crate::{utils, Context};
use eyre::{bail, Result};
use log::trace;
use poise::serenity_prelude::{
futures::StreamExt, Attachment, CreateActionRow, CreateButton, CreateEmbed, CreateMessage,
Mentionable, ReactionType,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct WelcomeEmbed {
title: String,
description: Option<String>,
url: Option<Url>,
hex_color: Option<String>,
image: Option<Url>,
}
impl From<WelcomeEmbed> for CreateMessage {
fn from(val: WelcomeEmbed) -> Self {
let mut embed = CreateEmbed::new();
embed = embed.title(val.title);
if let Some(description) = val.description {
embed = embed.description(description);
}
if let Some(url) = val.url {
embed = embed.url(url);
}
if let Some(color) = val.hex_color {
let hex = i32::from_str_radix(&color, 16).unwrap();
embed = embed.color(hex);
}
if let Some(image) = val.image {
embed = embed.image(image);
}
Self::new().embed(embed)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct WelcomeRole {
title: String,
id: u64,
emoji: Option<String>,
}
impl From<WelcomeRole> for CreateButton {
fn from(value: WelcomeRole) -> Self {
let mut button = Self::new(value.id.to_string()).label(value.title);
if let Some(emoji) = value.emoji {
button = button.emoji(ReactionType::from_str(&emoji).unwrap());
}
button
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct WelcomeRoleCategory {
title: String,
description: Option<String>,
roles: Vec<WelcomeRole>,
}
impl From<WelcomeRoleCategory> for CreateMessage {
fn from(value: WelcomeRoleCategory) -> Self {
let mut content = format!("**{}**", value.title);
if let Some(description) = value.description {
write!(content, "\n{description}").ok();
}
let buttons: Vec<CreateButton> = value
.roles
.iter()
.map(|role| CreateButton::from(role.clone()))
.collect();
let components = vec![CreateActionRow::Buttons(buttons)];
Self::new().content(content).components(components)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct WelcomeLayout {
embeds: Vec<WelcomeEmbed>,
messages: Vec<String>,
roles: Vec<WelcomeRoleCategory>,
}
/// Sets your welcome channel info
#[poise::command(
slash_command,
guild_only,
ephemeral,
default_member_permissions = "MANAGE_GUILD",
required_permissions = "MANAGE_GUILD"
)]
pub async fn set_welcome(
ctx: Context<'_>,
#[description = "A file to use"] file: Option<Attachment>,
#[description = "A URL for a file to use"] url: Option<String>,
) -> Result<()> {
trace!("Running set_welcome command!");
let configured_channels = ctx.data().config.clone().discord_config().channels();
let Some(channel_id) = configured_channels.welcome_channel_id() else {
ctx.say("You don't have a welcome channel ID set, so I can't do anything :(")
.await?;
return Ok(());
};
ctx.defer_ephemeral().await?;
// download attachment from discord or URL
let file = if let Some(attachment) = file {
let Some(content_type) = &attachment.content_type else {
bail!("Welcome channel attachment was sent without a content type!");
};
if !content_type.starts_with("application/json;") {
trace!("Not attempting to read non-json content type {content_type}");
ctx.say("Invalid file! Please only send json").await?;
return Ok(());
}
let downloaded = attachment.download().await?;
String::from_utf8(downloaded)?
} else if let Some(url) = url {
if Url::parse(&url).is_err() {
ctx.say("Invalid url!").await?;
return Ok(());
}
utils::text_from_url(&url).await?
} else {
ctx.say("A text file or URL must be provided!").await?;
return Ok(());
};
// parse and create messages from file
let welcome_layout: WelcomeLayout = serde_json::from_str(&file)?;
let embed_messages: Vec<CreateMessage> = welcome_layout
.embeds
.iter()
.map(|e| CreateMessage::from(e.clone()))
.collect();
let roles_messages: Vec<CreateMessage> = welcome_layout
.roles
.iter()
.map(|c| CreateMessage::from(c.clone()))
.collect();
// clear previous messages
let mut prev_messages = channel_id.messages_iter(ctx).boxed();
while let Some(prev_message) = prev_messages.next().await {
if let Ok(message) = prev_message {
message.delete(ctx).await?;
}
}
// send our new ones
for embed in embed_messages {
channel_id.send_message(ctx, embed).await?;
}
for message in roles_messages {
channel_id.send_message(ctx, message).await?;
}
for message in welcome_layout.messages {
channel_id.say(ctx, message).await?;
}
if let Some(log_channel) = configured_channels.log_channel_id() {
let author = utils::embed_author_from_user(ctx.author());
let embed = CreateEmbed::new()
.title("set_welcome command used!")
.author(author);
let message = CreateMessage::new().embed(embed);
log_channel.send_message(ctx, message).await?;
} else {
trace!("Not sending /set_welcome log as no channel is set");
}
ctx.reply(format!("Updated {}!", channel_id.mention()))
.await?;
Ok(())
}

View file

@ -5,7 +5,8 @@ use poise::serenity_prelude::ChannelId;
#[derive(Clone, Copy, Debug, Default)]
pub struct RefractionChannels {
say_log_channel_id: Option<ChannelId>,
log_channel_id: Option<ChannelId>,
welcome_channel_id: Option<ChannelId>,
}
#[derive(Clone, Debug, Default)]
@ -14,20 +15,29 @@ pub struct Config {
}
impl RefractionChannels {
pub fn new(say_log_channel_id: Option<ChannelId>) -> Self {
Self { say_log_channel_id }
pub fn new(log_channel_id: Option<ChannelId>, welcome_channel_id: Option<ChannelId>) -> Self {
Self {
log_channel_id,
welcome_channel_id,
}
}
pub fn new_from_env() -> Self {
let say_log_channel_id = Self::get_channel_from_env("DISCORD_SAY_LOG_CHANNELID");
if let Some(channel_id) = say_log_channel_id {
let log_channel_id = Self::get_channel_from_env("DISCORD_LOG_CHANNEL_ID");
if let Some(channel_id) = log_channel_id {
info!("Log channel is {channel_id}");
} else {
warn!("DISCORD_SAY_LOG_CHANNELID is empty; this will disable logging in your server.");
warn!("DISCORD_LOG_CHANNEL_ID is empty; this will disable logging in your server.");
}
Self::new(say_log_channel_id)
let welcome_channel_id = Self::get_channel_from_env("DISCORD_WELCOME_CHANNEL_ID");
if let Some(channel_id) = welcome_channel_id {
info!("Welcome channel is {channel_id}");
} else {
warn!("DISCORD_WELCOME_CHANNEL_ID is empty; this will disable welcome channel features in your server");
}
Self::new(log_channel_id, welcome_channel_id)
}
fn get_channel_from_env(var: &str) -> Option<ChannelId> {
@ -36,8 +46,12 @@ impl RefractionChannels {
.and_then(|env_var| ChannelId::from_str(&env_var).ok())
}
pub fn say_log_channel_id(self) -> Option<ChannelId> {
self.say_log_channel_id
pub fn log_channel_id(self) -> Option<ChannelId> {
self.log_channel_id
}
pub fn welcome_channel_id(self) -> Option<ChannelId> {
self.welcome_channel_id
}
}

View file

@ -2,6 +2,7 @@ use crate::api::REQWEST_CLIENT;
use eyre::Result;
use log::debug;
use poise::serenity_prelude::{CreateEmbedAuthor, User};
use reqwest::Response;
pub mod resolve_message;
@ -27,3 +28,10 @@ pub async fn bytes_from_url(url: &str) -> Result<Vec<u8>> {
let bytes = resp.bytes().await?;
Ok(bytes.to_vec())
}
pub fn embed_author_from_user(user: &User) -> CreateEmbedAuthor {
CreateEmbedAuthor::new(user.tag()).icon_url(
user.avatar_url()
.unwrap_or_else(|| user.default_avatar_url()),
)
}