use std::{
error::Error,
fmt::{Debug, Display, Formatter, Write as _},
fs::File,
io::Write,
};
use twilight_model::{
guild::Permissions,
http::attachment::Attachment,
id::{marker::ChannelMarker, Id},
};
#[cfg(doc)]
use crate::interaction::InteractionHandle;
use crate::{error::extract::HttpErrorExt, Bot};
pub mod conversion;
pub mod extract;
impl Bot {
pub async fn set_logging_channel(
&mut self,
channel_id: Id<ChannelMarker>,
) -> Result<(), anyhow::Error> {
let webhook = if let Some(webhook) = self
.http
.channel_webhooks(channel_id)
.await?
.models()
.await?
.into_iter()
.find(|webhook| webhook.token.is_some())
{
webhook
} else {
self.http
.create_webhook(channel_id, "Bot Error Logger")?
.await?
.model()
.await?
};
self.logging_webhook = Some((webhook.id, webhook.token.unwrap()));
Ok(())
}
#[allow(clippy::missing_const_for_fn)]
pub fn set_logging_file(&mut self, logging_file_path: String) {
self.logging_file_path = Some(logging_file_path);
}
pub async fn log(&self, mut message: String) {
if let Err(e) = self.log_webhook(message.clone()).await {
let _ = writeln!(message, "Failed to log the message in the channel: {e}");
}
if let Some(path) = &self.logging_file_path {
if let Err(e) = File::options()
.create(true)
.append(true)
.open(path)
.and_then(|mut file| writeln!(file, "{message}"))
{
let _ = writeln!(message, "Failed to log the message to file: {e}");
}
}
println!("\n{message}");
}
async fn log_webhook(&self, message: String) -> Result<(), anyhow::Error> {
if let Some((webhook_id, webhook_token)) = &self.logging_webhook {
self.http
.execute_webhook(*webhook_id, webhook_token)
.username(&self.user.name)?
.attachments(&[Attachment::from_bytes(
"error_message.txt".to_string(),
message.into_bytes(),
0,
)])?
.await?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(clippy::module_name_repetitions)]
pub enum UserError {
MissingPermissions(Option<Permissions>),
Ignore,
}
impl Display for UserError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str("a user error has been handled like an internal error")
}
}
impl Error for UserError {}
#[allow(clippy::module_name_repetitions)]
pub trait ErrorExt: Sized {
fn user(&self) -> Option<UserError>;
fn with_permissions(&mut self, required_permissions: Permissions);
fn internal<Custom: Display + Debug + Send + Sync + 'static>(self) -> Option<Self>;
fn ignore(&self) -> bool;
}
impl ErrorExt for anyhow::Error {
fn user(&self) -> Option<UserError> {
if let Some(user_err) = self.downcast_ref().copied() {
return Some(user_err);
}
if let Some(http_err) = self.downcast_ref::<twilight_http::Error>() {
if http_err.unknown_message() || http_err.failed_dm() || http_err.reaction_blocked() {
return Some(UserError::Ignore);
}
if http_err.missing_permissions() || http_err.missing_access() {
return Some(UserError::MissingPermissions(None));
}
}
None
}
fn with_permissions(&mut self, required_permissions: Permissions) {
if let Some(UserError::MissingPermissions(_)) = self.user() {
*self = UserError::MissingPermissions(Some(required_permissions)).into();
}
}
fn internal<Custom: Display + Debug + Send + Sync + 'static>(self) -> Option<Self> {
if self.user().is_none() && self.downcast_ref::<Custom>().is_none() {
Some(self)
} else {
None
}
}
fn ignore(&self) -> bool {
matches!(self.user(), Some(UserError::Ignore))
}
}
#[cfg(test)]
mod tests {
use std::{
env::VarError,
error::Error,
fmt::{Display, Formatter},
};
use twilight_model::guild::Permissions;
use crate::error::{ErrorExt, UserError};
#[derive(Debug)]
enum CustomError {
TooSlay,
}
impl Display for CustomError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str("You slayed too hard")
}
}
impl Error for CustomError {}
#[test]
fn user_err_downcast() {
let ignore_err = UserError::Ignore;
assert_eq!(Some(ignore_err), anyhow::Error::from(ignore_err).user());
let permissions_err = UserError::MissingPermissions(Some(Permissions::CREATE_INVITE));
assert_eq!(
Some(permissions_err),
anyhow::Error::from(permissions_err).user()
);
}
#[test]
fn err_with_permissions() {
let permissions = Permissions::CREATE_INVITE;
let mut err = anyhow::Error::from(UserError::MissingPermissions(None));
err.with_permissions(permissions);
assert_eq!(
Some(UserError::MissingPermissions(Some(permissions))),
err.user()
);
}
#[test]
fn internal_err_downcast() {
let user_err = anyhow::Error::from(UserError::Ignore);
assert!(user_err.internal::<CustomError>().is_none());
let custom_err = anyhow::Error::from(CustomError::TooSlay);
assert!(custom_err.internal::<CustomError>().is_none());
assert_eq!(
Some(&VarError::NotPresent),
anyhow::Error::from(VarError::NotPresent)
.internal::<CustomError>()
.as_ref()
.and_then(anyhow::Error::downcast_ref)
);
}
}