use std::fmt;
pub type Result<T> = std::result::Result<T, BotError>;
#[derive(Debug, thiserror::Error)]
pub enum BotError {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("WebSocket error: {0}")]
WebSocket(Box<tokio_tungstenite::tungstenite::Error>),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("URL error: {0}")]
Url(#[from] url::ParseError),
#[error("API error: {code} - {message}")]
Api { code: u32, message: String },
#[error("Authentication failed: {0}")]
AuthenticationFailed(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Method not allowed: {0}")]
MethodNotAllowed(String),
#[error("Forbidden: {0}")]
Forbidden(String),
#[error("Sequence number error: {0}")]
SequenceNumber(String),
#[error("Server error: {0}")]
Server(String),
#[error("Authentication error: {0}")]
Auth(String),
#[error("Connection error: {0}")]
Connection(String),
#[error("Rate limited: retry after {retry_after} seconds")]
RateLimit { retry_after: u64 },
#[error("Invalid configuration: {0}")]
Config(String),
#[error("Invalid data: {0}")]
InvalidData(String),
#[error("Network timeout")]
Timeout,
#[error("Gateway error: {0}")]
Gateway(String),
#[error("Session error: {0}")]
Session(String),
#[error("Internal error: {0}")]
Internal(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Not implemented: {0}")]
NotImplemented(String),
}
impl BotError {
pub fn api(code: u32, message: impl Into<String>) -> Self {
Self::Api {
code,
message: message.into(),
}
}
pub fn auth(message: impl Into<String>) -> Self {
Self::Auth(message.into())
}
pub fn connection(message: impl Into<String>) -> Self {
Self::Connection(message.into())
}
pub fn config(message: impl Into<String>) -> Self {
Self::Config(message.into())
}
pub fn invalid_data(message: impl Into<String>) -> Self {
Self::InvalidData(message.into())
}
pub fn gateway(message: impl Into<String>) -> Self {
Self::Gateway(message.into())
}
pub fn session(message: impl Into<String>) -> Self {
Self::Session(message.into())
}
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal(message.into())
}
pub fn rate_limit(retry_after: u64) -> Self {
Self::RateLimit { retry_after }
}
pub fn not_implemented(message: impl Into<String>) -> Self {
Self::NotImplemented(message.into())
}
pub fn is_retryable(&self) -> bool {
match self {
BotError::Http(e) => e.is_timeout() || e.is_connect(),
BotError::WebSocket(_) => true,
BotError::Connection(_) => true,
BotError::Timeout => true,
BotError::Gateway(_) => true,
BotError::RateLimit { .. } => true,
_ => false,
}
}
pub fn retry_after(&self) -> Option<u64> {
match self {
BotError::RateLimit { retry_after } => Some(*retry_after),
BotError::Connection(_) => Some(5),
BotError::Gateway(_) => Some(1),
BotError::Timeout => Some(3),
_ if self.is_retryable() => Some(1),
_ => None,
}
}
}
pub trait IntoBotError<T> {
fn with_context(self, context: &str) -> Result<T>;
}
impl<T, E> IntoBotError<T> for std::result::Result<T, E>
where
E: fmt::Display,
{
fn with_context(self, context: &str) -> Result<T> {
self.map_err(|e| BotError::internal(format!("{context}: {e}")))
}
}
impl From<tokio_tungstenite::tungstenite::Error> for BotError {
fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
BotError::WebSocket(Box::new(err))
}
}
pub fn http_error_from_status(status: u16, message: String) -> BotError {
match status {
401 => BotError::AuthenticationFailed(message),
403 => BotError::Forbidden(message),
404 => BotError::NotFound(message),
405 => BotError::MethodNotAllowed(message),
429 => BotError::SequenceNumber(message),
500 | 504 => BotError::Server(message),
_ => BotError::api(status as u32, message),
}
}