Finish refactoring
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
framework::commands::{
|
||||
Command, CommandError,
|
||||
arguments::{Argument, NeedyString, ParseArguments},
|
||||
context::CommandContext,
|
||||
},
|
||||
groups::about::{
|
||||
components::{
|
||||
HelpDonateLinkButton, HelpGoBackButton, HelpSupportLinkButton, HelpWebsiteLinkButton,
|
||||
},
|
||||
embeds::{get_all_command_help_embed, get_command_help_embed, get_command_not_found_embed},
|
||||
},
|
||||
state::State,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HelpCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl Command<State> for HelpCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"help"
|
||||
}
|
||||
|
||||
fn arguments(&self) -> &'static [Argument] {
|
||||
&[Argument {
|
||||
name: "command",
|
||||
is_optional: true,
|
||||
is_greedy: false,
|
||||
}]
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Lists all commands and shows information about them individually."
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: CommandContext<'_, State>,
|
||||
args: &str,
|
||||
) -> Result<(), CommandError> {
|
||||
let command_name: Option<NeedyString> = ParseArguments::parse(args).unwrap();
|
||||
|
||||
if let Some(command_name) = command_name {
|
||||
if let Some(command) = ctx.rt.get_command(&command_name) {
|
||||
ctx.send()
|
||||
.embed(get_command_help_embed(&command, &ctx))
|
||||
.component(HelpGoBackButton, 1)
|
||||
.component(HelpSupportLinkButton, 1)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to send message! {e}"))
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
} else {
|
||||
ctx.send()
|
||||
.embed(get_command_not_found_embed(ctx.rt, command_name.0))
|
||||
.component(HelpGoBackButton, 1)
|
||||
.component(HelpSupportLinkButton, 1)
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
}
|
||||
} else {
|
||||
ctx.send()
|
||||
.embed(get_all_command_help_embed(ctx.prefix, ctx.rt))
|
||||
.component(HelpWebsiteLinkButton, 1)
|
||||
.component(HelpSupportLinkButton, 1)
|
||||
.component(HelpDonateLinkButton, 1)
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
framework::interactions::{InteractionError, components::Button, context::InteractionContext},
|
||||
groups::about::embeds::get_all_command_help_embed,
|
||||
state::State,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HelpGoBackButton;
|
||||
|
||||
#[async_trait]
|
||||
impl Button<State> for HelpGoBackButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("← Go back to commands list")
|
||||
}
|
||||
|
||||
async fn on_press(&self, ctx: InteractionContext<State>) -> Result<(), InteractionError> {
|
||||
ctx
|
||||
.original_message()
|
||||
.expect(
|
||||
"Buttons always have the original message in the interaction context, this one didn't (got None)"
|
||||
)
|
||||
.edit()
|
||||
.embed(get_all_command_help_embed(&ctx.prefix, ctx.rt))
|
||||
.component(HelpWebsiteLinkButton, 1)
|
||||
.component(HelpSupportLinkButton, 1)
|
||||
.component(HelpDonateLinkButton, 1)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("An error occurred while attempting to edit the original message of an interaction. {e:?}"))
|
||||
.map_err(
|
||||
|_| InteractionError::OriginalMessageEditingFailed
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HelpSupportLinkButton;
|
||||
|
||||
#[async_trait]
|
||||
impl Button<State> for HelpSupportLinkButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("Get help")
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<&'static str> {
|
||||
Some("https://discord.gg/9daSsrfTmE")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HelpWebsiteLinkButton;
|
||||
|
||||
#[async_trait]
|
||||
impl Button<State> for HelpWebsiteLinkButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("Website")
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<&'static str> {
|
||||
Some("https://payphone.lat/")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HelpDonateLinkButton;
|
||||
|
||||
#[async_trait]
|
||||
impl Button<State> for HelpDonateLinkButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("Donate")
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<&'static str> {
|
||||
Some("https://ko-fi.com/payphonedev")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use twilight_model::channel::message::Embed;
|
||||
use twilight_util::builder::embed::{
|
||||
EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder, EmbedFooterBuilder, ImageSource,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
constants::{COLOR_RED, COLOR_WHITE},
|
||||
discord::{images, sanitizing::SanitizeDiscordMarkdown},
|
||||
framework::{
|
||||
commands::{Command, context::CommandContext},
|
||||
runtime::Runtime,
|
||||
},
|
||||
state::State,
|
||||
utils::ToTitleCase,
|
||||
};
|
||||
|
||||
pub(super) fn get_command_help_embed(
|
||||
command: &Arc<dyn Command<State>>,
|
||||
ctx: &CommandContext<'_, State>,
|
||||
) -> Embed {
|
||||
let name = format!("{}{}", ctx.prefix, command.name());
|
||||
let arguments = command
|
||||
.arguments()
|
||||
.iter()
|
||||
.map(|arg| arg.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
let usage = if !arguments.is_empty() {
|
||||
format!("{name} {arguments}")
|
||||
} else {
|
||||
name
|
||||
};
|
||||
|
||||
EmbedBuilder::new()
|
||||
.author(
|
||||
EmbedAuthorBuilder::new(ctx.rt.app.name.clone())
|
||||
.icon_url(
|
||||
ImageSource::url(images::get_avatar_url(
|
||||
ctx.rt.app.bot.as_ref().unwrap().avatar,
|
||||
ctx.rt.app.bot.as_ref().unwrap().id.get(),
|
||||
ctx.rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.thumbnail(
|
||||
ImageSource::url(images::get_avatar_url(
|
||||
ctx.rt.app.bot.as_ref().unwrap().avatar,
|
||||
ctx.rt.app.bot.as_ref().unwrap().id.get(),
|
||||
ctx.rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.color(COLOR_WHITE)
|
||||
.title(format!("{} Command", command.name().to_title_case()))
|
||||
.description(command.description())
|
||||
.field(EmbedFieldBuilder::new("Usage", format!("`{usage}`")).build())
|
||||
.field(EmbedFieldBuilder::new(
|
||||
"Aliases",
|
||||
if !command.aliases().is_empty() {
|
||||
command
|
||||
.aliases()
|
||||
.iter()
|
||||
.map(|alias| format!("`{}{alias}`", ctx.prefix))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
} else {
|
||||
String::from("*This command doesn't have aliases.*")
|
||||
},
|
||||
))
|
||||
.footer(EmbedFooterBuilder::new(format!(
|
||||
"Use {}help <command> to get help about a specific command.",
|
||||
ctx.prefix
|
||||
)))
|
||||
.build()
|
||||
}
|
||||
|
||||
pub(super) fn get_all_command_help_embed<T>(prefix: &str, rt: Arc<Runtime<T>>) -> Embed {
|
||||
EmbedBuilder::new()
|
||||
.author(EmbedAuthorBuilder::new(rt.app.name.clone()).icon_url(ImageSource::url(images::get_avatar_url(
|
||||
rt.app.bot.as_ref().unwrap().avatar,
|
||||
rt.app.bot.as_ref().unwrap().id.get(),
|
||||
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||
))
|
||||
.unwrap()))
|
||||
.title("Pick up the phone and start making friends!")
|
||||
.description(format!("Here is the list of all available commands. You can see more details about a specific command with `{prefix}help <command>`."))
|
||||
.thumbnail(ImageSource::url(images::get_avatar_url(
|
||||
rt.app.bot.as_ref().unwrap().avatar,
|
||||
rt.app.bot.as_ref().unwrap().id.get(),
|
||||
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||
))
|
||||
.unwrap())
|
||||
.color(COLOR_WHITE)
|
||||
.field(EmbedFieldBuilder::new("Commands", get_commands_table(rt)).build())
|
||||
.footer(EmbedFooterBuilder::new(format!("v{} - Use {}help <command> to get more information about a specific command.", env!("CARGO_PKG_VERSION"), prefix)))
|
||||
.build()
|
||||
}
|
||||
|
||||
pub(super) fn get_commands_table<T>(rt: Arc<Runtime<T>>) -> String {
|
||||
let mut table = String::new();
|
||||
|
||||
for command in rt.commands.iter() {
|
||||
table.push_str(&format!("{:<12}", command.name()));
|
||||
}
|
||||
|
||||
format!("```\n{table}\n```")
|
||||
}
|
||||
|
||||
pub(super) fn get_command_not_found_embed<T>(rt: Arc<Runtime<T>>, name: String) -> Embed {
|
||||
EmbedBuilder::new()
|
||||
.color(COLOR_RED)
|
||||
.author(EmbedAuthorBuilder::new(rt.app.name.clone()).icon_url(ImageSource::url(images::get_avatar_url(
|
||||
rt.app.bot.as_ref().unwrap().avatar,
|
||||
rt.app.bot.as_ref().unwrap().id.get(),
|
||||
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||
))
|
||||
.unwrap()))
|
||||
.title("That's not a command")
|
||||
.description(format!("`{}` is not a valid command name (nor alias). If you put it with the prefix on, take the prefix off before trying again. For example, if you did `p.help p.call`, do `p.help call` instead.\n\nIf you still can't figure it out, press the button below to go back to the full list. Everything's there!", name.sanitize(true)))
|
||||
.thumbnail(ImageSource::url(images::get_avatar_url(
|
||||
rt.app.bot.as_ref().unwrap().avatar,
|
||||
rt.app.bot.as_ref().unwrap().id.get(),
|
||||
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||
))
|
||||
.unwrap())
|
||||
.build()
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod commands;
|
||||
pub mod components;
|
||||
pub mod embeds;
|
||||
@@ -0,0 +1,204 @@
|
||||
use async_trait::async_trait;
|
||||
use scc::HashSet;
|
||||
use twilight_model::id::Id;
|
||||
|
||||
use crate::{
|
||||
database::models::{Call, Channel},
|
||||
framework::commands::{Command, CommandError, context::CommandContext},
|
||||
state::State,
|
||||
utils::Bye,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct CallCommand {
|
||||
/// A set of channel IDs that are currently searching for a call. This works as a lock for two
|
||||
/// users not to use p.call at the same time in the same channel, as it could create a race
|
||||
/// condition. IDs are removed from this set once the DB is properly updated with the channel's
|
||||
/// state, meaning that not all searching channels will be in this set at all times.
|
||||
channel_lock: HashSet<u64>,
|
||||
}
|
||||
|
||||
impl CallCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
channel_lock: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Command<State> for CallCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"call"
|
||||
}
|
||||
|
||||
fn aliases(&self) -> &'static [&'static str] {
|
||||
&["c"]
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: CommandContext<'_, State>,
|
||||
_args: &str,
|
||||
) -> Result<(), CommandError> {
|
||||
let channel = Channel::get_or_create_by_id(
|
||||
ctx.event.channel_id.get(),
|
||||
&ctx.rt.state.db,
|
||||
&ctx.rt.http,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to get or create channel: {e:?}");
|
||||
})
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
|
||||
if channel.is_searching || self.channel_lock.contains(&ctx.event.channel_id.get()) {
|
||||
ctx.send()
|
||||
.content(format!(
|
||||
"☎️ You are already searching for a call! Use `{}hangup` to hang up the phone.",
|
||||
ctx.prefix
|
||||
))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if channel.call_id.is_some() {
|
||||
ctx.send()
|
||||
.content(format!("☎️ You are already on a call! Use `{}hangup` to hang up the phone, or `{}skip` to skip to the next call.", ctx.prefix, ctx.prefix))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.channel_lock
|
||||
.insert_async(ctx.event.channel_id.get())
|
||||
.await
|
||||
.bye();
|
||||
|
||||
let call = Call::start(&channel, &ctx.rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to start call: {e:?}");
|
||||
self.channel_lock.remove(&ctx.event.channel_id.get());
|
||||
})
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
|
||||
self.channel_lock
|
||||
.remove_async(&ctx.event.channel_id.get())
|
||||
.await;
|
||||
|
||||
if let Some(call) = call {
|
||||
ctx.send().content("☎️ Call connected. Say hi!").await.bye();
|
||||
ctx.rt
|
||||
.http
|
||||
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
||||
.content("☎️ Call connected. Say hi!")
|
||||
.await
|
||||
.bye();
|
||||
} else {
|
||||
ctx.send()
|
||||
.content("☎️ Searching for a call...")
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HangupCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl Command<State> for HangupCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"hangup"
|
||||
}
|
||||
|
||||
fn aliases(&self) -> &'static [&'static str] {
|
||||
&["h", "hang"]
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: CommandContext<'_, State>,
|
||||
_args: &str,
|
||||
) -> Result<(), CommandError> {
|
||||
let Some(channel) = Channel::get_by_id(ctx.event.channel_id.get(), &ctx.rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to get channel: {e:?}"))
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?
|
||||
else {
|
||||
ctx.send()
|
||||
.content(format!(
|
||||
"❌ You're not on a call! Use `{}call` to start one.",
|
||||
ctx.prefix
|
||||
))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if channel.is_searching {
|
||||
channel
|
||||
.cancel_search(&ctx.rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to cancel search: {e:?}");
|
||||
})
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
|
||||
ctx.send()
|
||||
.content("☎️ The phone was hung up.")
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(call_id) = channel.call_id {
|
||||
let Some(call) = Call::get_by_id(call_id, &ctx.rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to get call: {e:?}"))
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?
|
||||
else {
|
||||
ctx.send()
|
||||
.content("❌ Call not found.")
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
return Err(CommandError::DatabaseInconsistentState);
|
||||
};
|
||||
|
||||
call.hangup(&ctx.rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
|
||||
ctx.send().content("☎️ You hung up the phone.").await.bye();
|
||||
|
||||
ctx.rt
|
||||
.http
|
||||
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
||||
.content("☎️ The other party hung up the phone.")
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
} else {
|
||||
ctx.send()
|
||||
.content(format!(
|
||||
"❌ You're not on a call! Use `{}call` to start one.",
|
||||
ctx.prefix
|
||||
))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use linkify::LinkFinder;
|
||||
use twilight_model::{gateway::payload::incoming::MessageCreate, id::Id};
|
||||
use twilight_validate::request::webhook_username;
|
||||
|
||||
use crate::{
|
||||
database::models::{Call, Channel},
|
||||
discord::{images, sanitizing::SanitizeDiscordMarkdown},
|
||||
framework::{
|
||||
events::traits::{EventResult, OnMessageHandler},
|
||||
runtime::Runtime,
|
||||
},
|
||||
state::State,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MessageRedirector;
|
||||
|
||||
#[async_trait]
|
||||
impl OnMessageHandler<State> for MessageRedirector {
|
||||
async fn on_message(&self, event: &MessageCreate, rt: Arc<Runtime<State>>) -> EventResult {
|
||||
if event.author.bot || event.author.system.unwrap_or(false) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let prefixes = rt
|
||||
.get_prefixes(event.guild_id.map(|i| i.get()), Some(event.author.id.get()))
|
||||
.await;
|
||||
|
||||
if prefixes.iter().any(|prefix| {
|
||||
event
|
||||
.content
|
||||
.to_ascii_lowercase()
|
||||
.starts_with(&prefix.to_ascii_lowercase())
|
||||
}) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(channel) = Channel::get_by_id(event.channel_id.get(), &rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to get channel from DB when processing message create event! {e:?}"
|
||||
);
|
||||
})?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let call = if let Some(call_id) = channel.call_id {
|
||||
if let Some(call) = Call::get_by_id(call_id, &rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to get call from DB when processing message create event! {e:?}"
|
||||
);
|
||||
})?
|
||||
{
|
||||
if !call.is_active() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
call
|
||||
} else {
|
||||
log::error!(
|
||||
"Call ID {} in channel {} does not correspond to an active call.",
|
||||
call_id,
|
||||
channel.id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(opposite_channel) = Channel::get_by_id(
|
||||
call.other_party_channel_id(event.channel_id.get()),
|
||||
&rt.state.db,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to get opposite channel from DB when processing message create event! {e:?}"
|
||||
);
|
||||
})?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some((webhook_id, webhook_token)) = opposite_channel
|
||||
.webhook_id
|
||||
.zip(opposite_channel.webhook_token)
|
||||
else {
|
||||
// Handle DM calls in the future.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let profile = get_display_profile(event);
|
||||
|
||||
rt.http
|
||||
.execute_webhook(Id::new(webhook_id), &webhook_token)
|
||||
.allowed_mentions(None)
|
||||
.username(&profile.name)
|
||||
.avatar_url(&profile.avatar_url)
|
||||
.content(&escape_message(event))
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to send message to webhook for call {} in channel {}: {e:?}",
|
||||
call.id,
|
||||
opposite_channel.id
|
||||
);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayProfile {
|
||||
pub name: String,
|
||||
pub avatar_url: String,
|
||||
}
|
||||
|
||||
fn get_display_profile(event: &MessageCreate) -> DisplayProfile {
|
||||
let member = event.member.clone();
|
||||
|
||||
let name = if let Some(nick) = &member.clone().and_then(|member| member.nick) {
|
||||
if webhook_username(nick).is_err() {
|
||||
"Invalid Nickname".to_string()
|
||||
} else {
|
||||
nick.clone()
|
||||
}
|
||||
} else if let Some(name) = &event.author.global_name {
|
||||
if webhook_username(name).is_err() {
|
||||
"Invalid Global Name".to_string()
|
||||
} else {
|
||||
name.clone()
|
||||
}
|
||||
} else if webhook_username(&event.author.name).is_err() {
|
||||
"Invalid Username".to_string()
|
||||
} else {
|
||||
event.author.name.clone()
|
||||
};
|
||||
|
||||
let avatar_hash = member
|
||||
.and_then(|inner| inner.avatar)
|
||||
.or(event.author.avatar);
|
||||
let avatar_url = images::get_avatar_url(
|
||||
avatar_hash,
|
||||
event.author.id.get(),
|
||||
event.author.discriminator as u64,
|
||||
);
|
||||
|
||||
DisplayProfile { name, avatar_url }
|
||||
}
|
||||
|
||||
fn escape_message(event: &MessageCreate) -> String {
|
||||
let mut message = event.content.to_string();
|
||||
|
||||
for mention in &event.mentions {
|
||||
message = message.replace(
|
||||
&format!("<@{}>", mention.id),
|
||||
&format!("**@{}**", mention.name.sanitize(true)),
|
||||
);
|
||||
}
|
||||
|
||||
for mention in &event.mention_roles {
|
||||
message = message.replace(&format!("<@&{mention}>"), "**@Role**");
|
||||
}
|
||||
|
||||
for mention in &event.mention_channels {
|
||||
message = message.replace(
|
||||
&format!("<#{}>", mention.id),
|
||||
&format!("**#{}**", mention.name.sanitize(true)),
|
||||
);
|
||||
}
|
||||
|
||||
let mut finder = LinkFinder::new();
|
||||
finder.url_must_have_scheme(false);
|
||||
|
||||
for link in finder.links(&message.clone()) {
|
||||
let url = link.as_str();
|
||||
|
||||
message = message.replace(url, &"\\#".repeat(url.len()));
|
||||
}
|
||||
|
||||
let replacements: HashMap<char, Option<char>> = [
|
||||
('@', Some('a')),
|
||||
('0', Some('o')),
|
||||
('1', Some('i')),
|
||||
('2', Some('z')),
|
||||
('3', Some('e')),
|
||||
('4', Some('a')),
|
||||
('5', Some('s')),
|
||||
('6', Some('b')),
|
||||
('7', Some('t')),
|
||||
('8', Some('g')),
|
||||
('9', Some('q')),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let normalized_unidecoded = unidecode::unidecode(&message)
|
||||
.to_lowercase()
|
||||
.split_whitespace()
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ");
|
||||
let mut normalized = String::with_capacity(normalized_unidecoded.len());
|
||||
|
||||
for ch in normalized_unidecoded.chars() {
|
||||
if let Some(&replacement) = replacements.get(&ch) {
|
||||
if let Some(replacement) = replacement {
|
||||
normalized.push(replacement);
|
||||
}
|
||||
} else if ch.is_alphanumeric() || ch.is_whitespace() {
|
||||
normalized.push(ch);
|
||||
}
|
||||
|
||||
// skip everything else (punctuation not in map, zero-width, etc.)
|
||||
}
|
||||
|
||||
log::debug!("Escaped message: {normalized}");
|
||||
|
||||
message.sanitize(false)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod commands;
|
||||
pub mod events;
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use twilight_model::gateway::payload::incoming::Ready;
|
||||
|
||||
use crate::{
|
||||
framework::{
|
||||
events::traits::{EventResult, OnReadyHandler, OnReconnectHandler},
|
||||
runtime::Runtime,
|
||||
},
|
||||
state::State,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ReadyHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl OnReadyHandler<State> for ReadyHandler {
|
||||
async fn on_ready(&self, event: &Ready, _: Arc<Runtime<State>>) -> EventResult {
|
||||
log::info!(
|
||||
"Ready! Logged in as {}#{:>04}",
|
||||
event.user.name,
|
||||
event.user.discriminator
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ReconnectHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl OnReconnectHandler<State> for ReconnectHandler {
|
||||
async fn on_reconnect(&self, _: Arc<Runtime<State>>) -> EventResult {
|
||||
log::info!("The gateway asked for a reconnection");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod events;
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod about;
|
||||
pub mod calls;
|
||||
pub mod gateway;
|
||||
pub mod servers;
|
||||
@@ -0,0 +1,95 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use twilight_model::gateway::payload::incoming::{GuildCreate, GuildDelete, GuildUpdate};
|
||||
|
||||
use crate::{
|
||||
database::models::Server,
|
||||
framework::{
|
||||
events::traits::{
|
||||
EventResult, OnServerCreateHandler, OnServerDeleteHandler, OnServerUpdateHandler,
|
||||
},
|
||||
runtime::Runtime,
|
||||
},
|
||||
state::State,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServerCreateHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl OnServerCreateHandler<State> for ServerCreateHandler {
|
||||
async fn on_server_create(&self, event: &GuildCreate, rt: Arc<Runtime<State>>) -> EventResult {
|
||||
let server = match event {
|
||||
GuildCreate::Available(guild) => Server::from(guild.clone()),
|
||||
GuildCreate::Unavailable(_) => {
|
||||
// If the guild is unavailable, Discord will later send this event again when the guild
|
||||
// becomes available.
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
log::debug!("The server \"{}\" was created", server.name);
|
||||
|
||||
server.insert(&rt.state.db).await.inspect_err(|e| {
|
||||
log::error!(
|
||||
"Could not insert the recently joined server into the DB!\n\n{server:?}\n\n{e:?}\n"
|
||||
)
|
||||
})?;
|
||||
|
||||
// TODO: Send hello message to the server's default channel on server join.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServerUpdateHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl OnServerUpdateHandler<State> for ServerUpdateHandler {
|
||||
async fn on_server_update(&self, event: &GuildUpdate, rt: Arc<Runtime<State>>) -> EventResult {
|
||||
let mut server = match Server::get_by_id(event.0.id.get(), &rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!("Could not get server from DB when it was updated! {e:?}")
|
||||
})? {
|
||||
Some(server) => server,
|
||||
None => {
|
||||
log::debug!("Server was updated but it was not in DB");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if server.name != event.name {
|
||||
server.name = event.name.clone();
|
||||
|
||||
server
|
||||
.upsert(&rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Could not update server's name. {e:?}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServerDeleteHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl OnServerDeleteHandler<State> for ServerDeleteHandler {
|
||||
async fn on_server_delete(&self, event: &GuildDelete, rt: Arc<Runtime<State>>) -> EventResult {
|
||||
Server::delete_by_id(event.id.get(), &rt.state.db)
|
||||
.await
|
||||
.inspect(|_| log::debug!("Deleted server with ID {} because Payphone left", event.id))
|
||||
.inspect_err(|e| {
|
||||
log::error!(
|
||||
"Could not delete server with ID {} from DB when Payphone left! {e:?}",
|
||||
event.id
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod events;
|
||||
Reference in New Issue
Block a user