feat: add /set_welcome command
This commit is contained in:
parent
239928a22a
commit
59bf42998b
7 changed files with 286 additions and 51 deletions
|
@ -1,17 +1,8 @@
|
||||||
mod help;
|
pub mod help;
|
||||||
mod joke;
|
pub mod joke;
|
||||||
mod members;
|
pub mod members;
|
||||||
mod ping;
|
pub mod ping;
|
||||||
mod rory;
|
pub mod rory;
|
||||||
mod say;
|
pub mod say;
|
||||||
mod stars;
|
pub mod stars;
|
||||||
mod tag;
|
pub 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;
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::Context;
|
use crate::{utils, Context};
|
||||||
|
|
||||||
use eyre::{OptionExt, Result};
|
use eyre::{OptionExt, Result};
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use poise::serenity_prelude::{CreateEmbed, CreateEmbedAuthor, CreateMessage};
|
use poise::serenity_prelude::{CreateEmbed, CreateMessage};
|
||||||
|
|
||||||
/// Say something through the bot
|
/// Say something through the bot
|
||||||
#[poise::command(
|
#[poise::command(
|
||||||
|
@ -17,7 +17,6 @@ pub async fn say(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "the message content"] content: String,
|
#[description = "the message content"] content: String,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let guild = ctx.guild().ok_or_eyre("Couldn't get guild!")?.to_owned();
|
|
||||||
let channel = ctx
|
let channel = ctx
|
||||||
.guild_channel()
|
.guild_channel()
|
||||||
.await
|
.await
|
||||||
|
@ -41,19 +40,9 @@ pub async fn say(
|
||||||
.clone()
|
.clone()
|
||||||
.discord_config()
|
.discord_config()
|
||||||
.channels()
|
.channels()
|
||||||
.say_log_channel_id()
|
.log_channel_id()
|
||||||
{
|
{
|
||||||
let log_channel = guild
|
let author = utils::embed_author_from_user(ctx.author());
|
||||||
.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 embed = CreateEmbed::default()
|
let embed = CreateEmbed::default()
|
||||||
.title("Say command used!")
|
.title("Say command used!")
|
||||||
|
@ -61,7 +50,7 @@ pub async fn say(
|
||||||
.author(author);
|
.author(author);
|
||||||
|
|
||||||
let message = CreateMessage::new().embed(embed);
|
let message = CreateMessage::new().embed(embed);
|
||||||
log_channel.1.send_message(ctx, message).await?;
|
channel_id.send_message(ctx, message).await?;
|
||||||
} else {
|
} else {
|
||||||
trace!("Not sending /say log as no channel is set");
|
trace!("Not sending /say log as no channel is set");
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,47 @@ use crate::Data;
|
||||||
use eyre::Report;
|
use eyre::Report;
|
||||||
|
|
||||||
mod general;
|
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 type Command = poise::Command<Data, Report>;
|
||||||
|
|
||||||
pub fn get() -> Vec<Command> {
|
pub fn get() -> Vec<Command> {
|
||||||
vec![
|
vec![
|
||||||
general::joke(),
|
general!(help),
|
||||||
general::members(),
|
general!(joke),
|
||||||
general::ping(),
|
general!(members),
|
||||||
general::rory(),
|
general!(ping),
|
||||||
general::say(),
|
general!(rory),
|
||||||
general::stars(),
|
general!(say),
|
||||||
general::tag(),
|
general!(stars),
|
||||||
general::help(),
|
general!(tag),
|
||||||
|
moderation!(set_welcome),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
1
src/commands/moderation/mod.rs
Normal file
1
src/commands/moderation/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod set_welcome;
|
203
src/commands/moderation/set_welcome.rs
Normal file
203
src/commands/moderation/set_welcome.rs
Normal 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(())
|
||||||
|
}
|
|
@ -5,7 +5,8 @@ use poise::serenity_prelude::ChannelId;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
pub struct RefractionChannels {
|
pub struct RefractionChannels {
|
||||||
say_log_channel_id: Option<ChannelId>,
|
log_channel_id: Option<ChannelId>,
|
||||||
|
welcome_channel_id: Option<ChannelId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
@ -14,20 +15,29 @@ pub struct Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RefractionChannels {
|
impl RefractionChannels {
|
||||||
pub fn new(say_log_channel_id: Option<ChannelId>) -> Self {
|
pub fn new(log_channel_id: Option<ChannelId>, welcome_channel_id: Option<ChannelId>) -> Self {
|
||||||
Self { say_log_channel_id }
|
Self {
|
||||||
|
log_channel_id,
|
||||||
|
welcome_channel_id,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_from_env() -> Self {
|
pub fn new_from_env() -> Self {
|
||||||
let say_log_channel_id = Self::get_channel_from_env("DISCORD_SAY_LOG_CHANNELID");
|
let log_channel_id = Self::get_channel_from_env("DISCORD_LOG_CHANNEL_ID");
|
||||||
|
if let Some(channel_id) = log_channel_id {
|
||||||
if let Some(channel_id) = say_log_channel_id {
|
|
||||||
info!("Log channel is {channel_id}");
|
info!("Log channel is {channel_id}");
|
||||||
} else {
|
} 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> {
|
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())
|
.and_then(|env_var| ChannelId::from_str(&env_var).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn say_log_channel_id(self) -> Option<ChannelId> {
|
pub fn log_channel_id(self) -> Option<ChannelId> {
|
||||||
self.say_log_channel_id
|
self.log_channel_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn welcome_channel_id(self) -> Option<ChannelId> {
|
||||||
|
self.welcome_channel_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ use crate::api::REQWEST_CLIENT;
|
||||||
|
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
use poise::serenity_prelude::{CreateEmbedAuthor, User};
|
||||||
use reqwest::Response;
|
use reqwest::Response;
|
||||||
|
|
||||||
pub mod resolve_message;
|
pub mod resolve_message;
|
||||||
|
@ -27,3 +28,10 @@ pub async fn bytes_from_url(url: &str) -> Result<Vec<u8>> {
|
||||||
let bytes = resp.bytes().await?;
|
let bytes = resp.bytes().await?;
|
||||||
Ok(bytes.to_vec())
|
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()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue