feat: Don't delete triggering message when replying to message

This commit is contained in:
oSumAtrIX 2024-06-04 03:07:13 +02:00
parent 6bd4890cad
commit b87c601e53
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
12 changed files with 890 additions and 707 deletions

992
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,12 +5,11 @@ homepage = "https://revanced.app"
license = "GPL-3.0"
name = "revanced-discord-bot"
repository = "https://github.com/revanced/revanced-discord-bot"
version = "2.7.0"
version = "2.8.0"
edition = "2021"
[profile.release]
strip = true
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
@ -20,14 +19,14 @@ bson = "2.4"
serde_with_macros = "3.4"
mongodb = "2.4"
poise = "0.6"
decancer = "1.5"
decancer = "3.2"
tokio = { version = "1.26", features = ["rt-multi-thread"] }
dotenv = "0.15"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
regex = "1.7"
serde_regex = "1.1"
reqwest = { version = "0.11", features = [
reqwest = { version = "0.12", features = [
"rustls-tls",
"json",
], default-features = false }
@ -39,5 +38,5 @@ tracing = { version = "0.1", features = [
] }
tracing-subscriber = "0.3"
hmac-sha256 = "1.1"
base64 = "0.21"
base64 = "0.22"
parse_duration = "2.1"

View File

@ -1,48 +1,28 @@
{
"general": {
"embed_color": 0,
"mute": {
"role": 0,
"take": [
0
]
},
"logging_channel": 0
"default_embed": {
"color": 0
},
"mute": {
"role": 0,
"take": [0]
},
"log_channel": 0,
"administrators": {
"roles": [
0
],
"users": [
0
]
"roles": [0],
"users": [0]
},
"message_responses": [
"responses": [
{
"includes": {
"channels": [
0
],
"match": []
"whitelist": {
"channels": [0],
"regex": [""]
},
"excludes": {
"roles": [
0
],
"match": []
"blacklist": {
"roles": [0],
"regex": [""]
},
"condition": {
"user": {
"server_age": 2
}
},
"response": {
"message": ""
},
"thread_options": {
"lock_on_response": false,
"close_on_response": false,
"only_on_first_message": false
"message": {
"content": ""
},
"respond_to_reference": false
}

View File

@ -1,33 +1,25 @@
use poise::serenity_prelude::CreateEmbed;
use poise::CreateReply;
use tracing::debug;
use crate::utils::bot::load_configuration;
use crate::utils::create_default_embed;
use crate::{Context, Error};
/// Reload the Discord bot.
#[poise::command(slash_command)]
pub async fn reload(ctx: Context<'_>) -> Result<(), Error> {
// Update the configuration
// Update the configuration.
let configuration = load_configuration();
// Use the embed color from the updated configuration
let embed_color = configuration.general.embed_color;
// Also save the new configuration to the user data
let embed = create_default_embed(&configuration);
ctx.data().write().await.configuration = configuration;
debug!("{} reloaded the configuration.", ctx.author().name);
ctx.send(
CreateReply {
embeds: vec![
CreateEmbed::new()
.description("Reloading configuration...")
.color(embed_color),
],
ephemeral: Some(true),
..Default::default()
}
)
ctx.send(CreateReply {
embeds: vec![embed.description("Reloading configuration...")],
ephemeral: Some(true),
..Default::default()
})
.await?;
Ok(())
@ -38,19 +30,13 @@ pub async fn reload(ctx: Context<'_>) -> Result<(), Error> {
pub async fn stop(ctx: Context<'_>) -> Result<(), Error> {
debug!("{} stopped the bot.", ctx.author().name);
let color = ctx.data().read().await.configuration.general.embed_color;
let configuration = &ctx.data().read().await.configuration;
ctx.send(
CreateReply {
ephemeral: Some(true),
embeds: vec![
CreateEmbed::new()
.description("Stopping the bot...")
.color(color),
],
..Default::default()
}
)
ctx.send(CreateReply {
ephemeral: Some(true),
embeds: vec![create_default_embed(configuration).description("Stopping the bot...")],
..Default::default()
})
.await?;
ctx.framework().shard_manager().shutdown_all().await;

View File

@ -2,7 +2,7 @@ use bson::{doc, Document};
use chrono::Utc;
use mongodb::options::{UpdateModifications, UpdateOptions};
use poise::serenity_prelude::{
self as serenity, CreateEmbed, CreateEmbedFooter, EditMessage, GetMessages, Mentionable, UserId,
CreateEmbedFooter, EditMessage, GetMessages, Mentionable, UserId,
};
use poise::CreateReply;
use tracing::{debug, trace};
@ -13,7 +13,7 @@ use crate::utils::macros::to_user;
use crate::utils::moderation::{
ban_moderation, queue_unmute_member, respond_moderation, BanKind, ModerationKind,
};
use crate::utils::parse_duration;
use crate::utils::{create_default_embed, parse_duration};
use crate::{Context, Error};
/// Unmute a member.
@ -41,7 +41,7 @@ pub async fn unmute(
data.database.clone(),
ctx.guild_id().unwrap(),
id,
configuration.general.mute.role,
configuration.mute.role,
0,
)
.await
@ -73,7 +73,7 @@ pub async fn mute(
let configuration = &data.configuration;
let author = ctx.author();
let mute = &configuration.general.mute;
let mute = &configuration.mute;
let guild_id = ctx.guild_id().unwrap();
let discord = ctx.serenity_context();
@ -185,7 +185,6 @@ pub async fn purge(
let data = ctx.data().read().await;
let configuration = &data.configuration;
let embed_color = configuration.general.embed_color;
let channel = ctx.channel_id();
let too_old_timestamp = Utc::now().timestamp() - MAX_BULK_DELETE_AGO_SECS;
@ -196,10 +195,9 @@ pub async fn purge(
let handle = ctx
.send(CreateReply {
embeds: vec![CreateEmbed::new()
embeds: vec![create_default_embed(configuration)
.title("Purging messages")
.description("Accumulating...")
.color(embed_color)
.thumbnail(&image)],
..Default::default()
})
@ -271,14 +269,12 @@ pub async fn purge(
.edit(
&ctx.serenity_context(),
EditMessage::new().embed(
serenity::CreateEmbed::default()
create_default_embed(configuration)
.title("Purge successful")
.field("Deleted messages", deleted_amount.to_string(), false)
.field("Action by", author.mention().to_string(), false)
.color(embed_color)
.thumbnail(&image)
.footer(CreateEmbedFooter::new("ReVanced").icon_url(image))
.clone(),
.footer(CreateEmbedFooter::new("ReVanced").icon_url(image)),
),
)
.await?;

View File

@ -6,7 +6,7 @@ use crate::utils::moderation::queue_unmute_member;
pub async fn load_muted_members(ctx: &serenity::Context, data: &Arc<RwLock<Data>>) {
let data = &mut *data.write().await;
let mute_role_id = data.configuration.general.mute.role;
let mute_role_id = data.configuration.mute.role;
let mut cursor = data
.database

View File

@ -6,11 +6,12 @@ use commands::{configuration, misc, moderation};
use db::database::Database;
use events::event_handler;
use poise::serenity_prelude::prelude::{RwLock, TypeMapKey};
use poise::serenity_prelude::{CreateEmbed, UserId};
use poise::serenity_prelude::{UserId};
use poise::CreateReply;
use tokio::task::JoinHandle;
use tracing::{error, trace};
use utils::bot::load_configuration;
use utils::create_default_embed;
use crate::model::application::Configuration;
@ -125,12 +126,11 @@ async fn main() {
if let Err(e) = ctx
.send(CreateReply {
ephemeral: Some(true),
embeds: vec![CreateEmbed::new()
embeds: vec![create_default_embed(configuration)
.title("Permission error")
.description(
"You do not have permission to use this command.",
)
.color(configuration.general.embed_color)
.thumbnail(member.user.avatar_url().unwrap_or_else(
|| member.user.default_avatar_url(),
))],

View File

@ -9,9 +9,11 @@ use serde::{Deserialize, Serialize};
#[derive(Default, Serialize, Deserialize)]
pub struct Configuration {
pub general: General,
pub administrators: Administrators,
pub message_responses: Vec<MessageResponse>,
pub default_embed: Embed,
pub mute: Mute,
pub log_channel: u64,
pub responses: Vec<Response>,
}
const CONFIG_PATH: &str = "configuration.json";
@ -39,15 +41,15 @@ impl Configuration {
sys_config_dir.to_string_lossy()
);
// config file in current dir
// Config file in the current directory.
let mut file = if Path::new(CONFIG_PATH).exists() {
File::open(CONFIG_PATH)?
}
// config file in system dir (on *nix: `~/.config/revanced-discord-bot/`)
// Config file in the system directory.
else if Path::new(&sys_config).exists() {
File::open(sys_config)?
}
// create defalt config
// Create a default config file.
else {
let default_config = Configuration::default();
default_config.save()?;
@ -62,13 +64,6 @@ impl Configuration {
}
}
#[derive(Default, Serialize, Deserialize)]
pub struct General {
pub embed_color: i32,
pub mute: Mute,
pub logging_channel: u64,
}
#[derive(Default, Serialize, Deserialize)]
pub struct Mute {
pub role: u64,
@ -81,89 +76,57 @@ pub struct Administrators {
}
#[derive(Serialize, Deserialize)]
pub struct MessageResponse {
pub includes: Option<Includes>,
pub excludes: Option<Excludes>,
pub condition: Option<Condition>,
pub response: Response,
pub thread_options: Option<ThreadOptions>,
pub struct Response {
pub whitelist: Option<Trigger>,
pub blacklist: Option<Trigger>,
pub message: Message,
pub respond_to_reference: Option<bool>,
}
#[derive(Serialize, Deserialize)]
pub struct ThreadOptions {
pub close_on_response: bool,
pub lock_on_response: bool,
pub only_on_first_message: bool,
}
#[derive(Serialize, Deserialize)]
pub struct Response {
pub message: Option<String>,
pub struct Message {
pub content: Option<String>,
pub embed: Option<Embed>,
}
#[derive(Serialize, Deserialize)]
#[derive(Default)]
pub struct Embed {
pub title: String,
pub description: String,
pub color: i32,
pub fields: Vec<Field>,
pub footer: Footer,
pub image: Image,
pub thumbnail: Thumbnail,
pub author: Author,
pub author: Option<Author>,
pub color: Option<i32>,
pub title: Option<String>,
pub description: Option<String>,
pub fields: Option<Vec<Field>>,
pub footer: Option<Footer>,
pub image_url: Option<String>,
pub thumbnail_url: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct Field {
pub name: String,
pub value: String,
pub inline: bool,
pub inline: Option<bool>,
}
#[derive(Serialize, Deserialize)]
pub struct Footer {
pub text: String,
pub icon_url: String,
}
#[derive(Serialize, Deserialize)]
pub struct Image {
pub url: String,
}
#[derive(Serialize, Deserialize)]
pub struct Thumbnail {
pub url: String,
pub icon_url: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct Author {
pub name: String,
pub icon_url: String,
pub url: String,
pub icon_url: Option<String>,
pub url: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct Includes {
pub struct Trigger {
pub channels: Option<HashSet<u64>>,
pub roles: Option<HashSet<u64>>,
#[serde(rename = "match", with = "serde_regex")]
pub match_field: Vec<Regex>,
}
#[derive(Serialize, Deserialize)]
pub struct Excludes {
pub roles: Option<HashSet<u64>>,
}
#[derive(Serialize, Deserialize)]
pub struct Condition {
pub user: User,
}
#[derive(Serialize, Deserialize)]
pub struct User {
pub server_age: i64,
#[serde(with = "serde_regex")]
pub regex: Vec<Regex>,
}

View File

@ -27,7 +27,7 @@ pub async fn cure(
}
}
let mut cured_name = decancer::cure(&name).replace(
let mut cured_name = decancer::cure!(&name).unwrap().as_str().replace(
|c: char| !(c == ' ' || c == '-' || c == '_' || c.is_ascii_alphanumeric()),
"",
);

View File

@ -1,18 +1,37 @@
use poise::serenity_prelude::CreateMessage;
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
use poise::serenity_prelude::{
CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateMessage, EditThread, GetMessages,
};
use regex::Regex;
use serenity::Message;
use tracing::log::error;
use super::*;
use crate::{BotData};
use crate::{
model::application::Trigger,
BotData,
};
pub fn contains_match(regex: &[Regex], text: &str) -> bool {
regex.iter().any(|r| r.is_match(text))
impl Trigger {
fn matches(&self, new_message: &Message, member_roles: &[RoleId]) -> bool {
if let Some(channels) = &self.channels {
if !channels.contains(&new_message.channel_id.get()) {
return true;
}
}
if let Some(roles) = &self.roles {
if !member_roles
.iter()
.any(|&member_role| roles.contains(&member_role.get()))
{
return true;
}
}
if !self.regex.iter().any(|r| r.is_match(&new_message.content)) {
return true;
}
false
}
}
pub async fn handle_message_response(
@ -24,191 +43,53 @@ pub async fn handle_message_response(
return;
}
let responses = &data.read().await.configuration.message_responses;
let message = &new_message.content;
let configuration = &data.read().await.configuration;
let mut guild_message = None;
let member_roles = &new_message.member.as_ref().unwrap().roles;
let member = &new_message.member.as_ref().unwrap();
let member_roles = &member.roles;
let joined_at = member.joined_at.unwrap().unix_timestamp();
let must_joined_at = DateTime::<Utc>::from_naive_utc_and_offset(
NaiveDateTime::from_timestamp_opt(joined_at, 0).unwrap(),
Utc,
);
for response in responses {
if let Some(includes) = &response.includes {
if let Some(roles) = &includes.roles {
// check if the role is whitelisted
if !roles.iter().any(|&role_id| {
member_roles
.iter()
.any(|&member_role| role_id == member_role.get())
}) {
continue;
}
}
if let Some(channels) = &includes.channels {
// check if the channel is whitelisted, if not, check if the channel is a thread, if it is check if the parent id is whitelisted
if !channels.contains(&new_message.channel_id.get()) {
if response.thread_options.is_some() {
if guild_message.is_none() {
guild_message = Some(
new_message
.channel(&ctx.http)
.await
.unwrap()
.guild()
.unwrap(),
);
};
let Some(parent_id) = guild_message.as_ref().unwrap().parent_id else {
continue;
};
if !channels.contains(&parent_id.get()) {
continue;
}
} else {
continue;
}
}
}
// check if message matches regex
if !contains_match(&includes.match_field, message) {
tracing::log::trace!("Message does not match regex");
for response in &configuration.responses {
if let Some(whitelist) = &response.whitelist {
if whitelist.matches(new_message, member_roles) {
continue;
}
}
if let Some(excludes) = &response.excludes {
// check if the role is blacklisted
if let Some(roles) = &excludes.roles {
if roles.iter().any(|&role_id| {
member_roles
.iter()
.any(|&member_role| role_id == member_role.get())
}) {
continue;
}
if let Some(blacklist) = &response.blacklist {
if blacklist.matches(new_message, member_roles) {
continue;
}
}
if let Some(condition) = &response.condition {
let min_age = condition.user.server_age;
if min_age != 0 {
let but_joined_at = Utc::now() - Duration::days(min_age);
if must_joined_at <= but_joined_at {
continue;
}
}
}
let channel_id = new_message.channel_id;
let mut message_reference: Option<&serenity::Message> = None;
// If the message has a reference and the response is set to respond to references, respond to the reference
if let Some(respond_to_reference) = response.respond_to_reference {
if respond_to_reference {
if let Some(reference) = &new_message.referenced_message {
message_reference = Some(reference.as_ref());
if let Err(err) = new_message.delete(&ctx.http).await {
error!(
"Failed to delete the message from {}. Error: {:?}",
new_message.author.tag(),
err
);
}
}
}
}
if let Err(err) = channel_id
if let Err(err) = new_message
.channel_id
.send_message(&ctx.http, {
let mut message = CreateMessage::default();
message = if let Some(reference) = message_reference {
message.reference_message(reference)
} else {
message.reference_message(new_message)
};
match &response.response.embed {
Some(embed) => message.embed(
CreateEmbed::new()
.title(&embed.title)
.description(&embed.description)
.color(embed.color)
.fields(embed.fields.iter().map(|field| {
(field.name.clone(), field.value.clone(), field.inline)
}))
.footer(
CreateEmbedFooter::new(&embed.footer.text)
.icon_url(&embed.footer.icon_url),
)
.thumbnail(&embed.thumbnail.url)
.image(&embed.image.url)
.author(
CreateEmbedAuthor::new(&embed.author.name)
.icon_url(&embed.author.icon_url),
),
),
None => message.content(response.response.message.as_ref().unwrap()),
message = message.reference_message(
if let Some(reference) = &new_message.referenced_message {
reference.as_ref()
} else {
new_message
},
);
if let Some(embed_configuration) = &response.message.embed {
message = message.embed(create_embed(configuration, embed_configuration))
}
if let Some(content) = &response.message.content {
message = message.content(content)
}
message
})
.await
{
error!(
"Failed to reply to the message from {}. Error: {:?}",
"Failed to reply to {}. Error: {:?}",
new_message.author.tag(),
err
);
} else if let Some(thread_options) = &response.thread_options {
let mut channel = channel_id
.to_channel(&ctx.http)
.await
.unwrap()
.guild()
.unwrap();
// only apply this thread if the channel is a thread
if channel.thread_metadata.is_none() {
return;
}
// only edit this thread if the message is the first one
if thread_options.only_on_first_message
&& !channel_id
.messages(&ctx.http, GetMessages::new().limit(1).before(new_message))
.await
.unwrap()
.is_empty()
{
return;
}
if let Err(err) = channel
.edit_thread(
&ctx.http,
EditThread::new()
.locked(thread_options.lock_on_response)
.archived(thread_options.close_on_response),
)
.await
{
error!(
"Failed to edit the thread from {}. Error: {:?}",
new_message.author.tag(),
err
);
}
}
}
}

View File

@ -1,5 +1,9 @@
use chrono::Duration;
use poise::serenity_prelude::{self as serenity, Member, RoleId};
use poise::serenity_prelude::{
self as serenity, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Member, RoleId,
};
use crate::model::application::{Configuration, Embed};
pub mod bot;
pub mod code_embed;
@ -12,3 +16,94 @@ pub fn parse_duration(duration: String) -> Result<Duration, parse_duration::pars
let d = parse_duration::parse(&duration)?;
Ok(Duration::nanoseconds(d.as_nanos() as i64))
}
pub fn create_embed(configuration: &Configuration, embed_configuration: &Embed) -> CreateEmbed {
let mut create_embed = CreateEmbed::new();
if let Some(title) = embed_configuration
.title
.as_ref()
.or(configuration.default_embed.title.as_ref())
{
create_embed = create_embed.title(title);
}
if let Some(description) = embed_configuration
.description
.as_ref()
.or(configuration.default_embed.description.as_ref())
{
create_embed = create_embed.description(description);
}
if let Some(color) = embed_configuration
.color
.or(configuration.default_embed.color)
{
create_embed = create_embed.color(color);
}
if let Some(fields) = embed_configuration
.fields
.as_ref()
.or(configuration.default_embed.fields.as_ref())
{
for field in fields {
create_embed =
create_embed.field(&field.name, &field.value, field.inline.unwrap_or(false));
}
}
if let Some(footer) = embed_configuration
.footer
.as_ref()
.or(configuration.default_embed.footer.as_ref())
{
let mut create_footer = CreateEmbedFooter::new(&footer.text);
if let Some(icon_url) = &footer.icon_url {
create_footer = create_footer.icon_url(icon_url);
}
create_embed = create_embed.footer(create_footer);
}
if let Some(image_url) = embed_configuration
.image_url
.as_ref()
.or(configuration.default_embed.image_url.as_ref())
{
create_embed = create_embed.image(image_url);
}
if let Some(thumbnail_url) = embed_configuration
.thumbnail_url
.as_ref()
.or(configuration.default_embed.thumbnail_url.as_ref())
{
create_embed = create_embed.thumbnail(thumbnail_url);
}
if let Some(author) = embed_configuration
.author
.as_ref()
.or(configuration.default_embed.author.as_ref())
{
let mut create_author = CreateEmbedAuthor::new(&author.name);
if let Some(icon_url) = &author.icon_url {
create_author = create_author.icon_url(icon_url);
}
if let Some(url) = &author.url {
create_author = create_author.url(url);
}
create_embed = create_embed.author(create_author);
}
create_embed
}
pub fn create_default_embed(configuration: &Configuration) -> CreateEmbed {
create_embed(configuration, &configuration.default_embed)
}

View File

@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex};
use mongodb::options::FindOptions;
use poise::serenity_prelude::{
ChannelId, CreateEmbed, CreateEmbedFooter, CreateMessage, GuildId, Mentionable, User, UserId,
ChannelId, CreateEmbedFooter, CreateMessage, GuildId, Mentionable, User, UserId,
};
use poise::CreateReply;
use serenity::prelude::SerenityError;
@ -48,7 +48,7 @@ pub async fn mute_on_join(ctx: &serenity::Context, new_member: &serenity::Member
if found {
debug!("Muted member {} rejoined the server", new_member.user.tag());
if new_member
.add_role(&ctx.http, data.configuration.general.mute.role)
.add_role(&ctx.http, data.configuration.mute.role)
.await
.is_ok()
{
@ -126,15 +126,16 @@ pub async fn respond_moderation<'a>(
let send_ephemeral = Arc::new(Mutex::new(false));
let create_embed = || {
let f = CreateEmbed::new();
let mut create_embed = create_default_embed(configuration);
let result = match moderation {
create_embed = match moderation {
ModerationKind::Mute(user, author, reason, expires, error) => {
let embed = match error {
Some(err) => {
*send_ephemeral.lock().unwrap() = true;
f.title(format!("Failed to mute {}", user.tag()))
create_embed
.title(format!("Failed to mute {}", user.tag()))
.field("Exception", err.to_string(), false)
.field(
"Action",
@ -146,7 +147,7 @@ pub async fn respond_moderation<'a>(
false,
)
},
None => f.title(format!("Muted {}", user.tag())).field(
None => create_embed.title(format!("Muted {}", user.tag())).field(
"Action",
format!("{} was muted by {}", user.mention(), author.mention()),
false,
@ -165,7 +166,8 @@ pub async fn respond_moderation<'a>(
Some(err) => {
*send_ephemeral.lock().unwrap() = true;
f.title(format!("Failed to unmute {}", user.tag()))
create_embed
.title(format!("Failed to unmute {}", user.tag()))
.field("Exception", err.to_string(), false)
.field(
"Action",
@ -177,7 +179,7 @@ pub async fn respond_moderation<'a>(
false,
)
},
None => f.title(format!("Unmuted {}", user.tag())).field(
None => create_embed.title(format!("Unmuted {}", user.tag())).field(
"Action",
format!("{} was unmuted by {}", user.mention(), author.mention()),
false,
@ -188,7 +190,8 @@ pub async fn respond_moderation<'a>(
Some(err) => {
*send_ephemeral.lock().unwrap() = true;
f.title(format!("Failed to ban {}", user.tag()))
create_embed
.title(format!("Failed to ban {}", user.tag()))
.field("Exception", err.to_string(), false)
.field(
"Action",
@ -200,7 +203,7 @@ pub async fn respond_moderation<'a>(
false,
)
},
None => f.title(format!("Banned {}", user.tag())).field(
None => create_embed.title(format!("Banned {}", user.tag())).field(
"Action",
format!("{} was banned by {}", user.mention(), author.mention()),
false,
@ -216,7 +219,8 @@ pub async fn respond_moderation<'a>(
Some(err) => {
*send_ephemeral.lock().unwrap() = true;
f.title(format!("Failed to unban {}", user.tag()))
create_embed
.title(format!("Failed to unban {}", user.tag()))
.field("Exception", err.to_string(), false)
.field(
"Action",
@ -228,18 +232,19 @@ pub async fn respond_moderation<'a>(
false,
)
},
None => f.title(format!("Unbanned {}", user.tag())).field(
"Action",
format!("{} was unbanned by {}", user.mention(), author.mention()),
false,
),
None => create_embed
.title(format!("Unbanned {}", user.tag()))
.field(
"Action",
format!("{} was unbanned by {}", user.mention(), author.mention()),
false,
),
},
}
.color(configuration.general.embed_color);
};
let user = current_user.face();
result
create_embed
.thumbnail(&user)
.footer(CreateEmbedFooter::new("ReVanced").icon_url(&user))
};
@ -256,7 +261,7 @@ pub async fn respond_moderation<'a>(
let response = reply.message().await?;
ChannelId::from(configuration.general.logging_channel)
ChannelId::from(configuration.log_channel)
.send_message(
&ctx.serenity_context().http,
CreateMessage::new().embed(create_embed().field(