Create client to establish connection with discord and add basic framework with sample command
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Logging (env_logger) -----------------------------------------------------------------------------------
|
# Logging (env_logger) -----------------------------------------------------------------------------------
|
||||||
|
|
||||||
RUST_LOG="rotom_discord_bot=debug"
|
RUST_LOG="cipher_discord_bot=debug"
|
||||||
|
|
||||||
# Discord ------------------------------------------------------------------------------------------------
|
# Discord ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
1213
Cargo.lock
generated
1213
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
"rotom_core",
|
"cipher_core",
|
||||||
"rotom_database", "rotom_discord_bot",
|
"cipher_database",
|
||||||
|
"cipher_discord_bot",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rotom_core"
|
name = "cipher_core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use user_repository::UserRepository;
|
use user_repository::UserRepository;
|
||||||
|
|
||||||
pub mod user_repository;
|
pub mod user_repository;
|
||||||
@@ -22,3 +24,12 @@ where
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RepositoryError<E>(pub E);
|
pub struct RepositoryError<E>(pub E);
|
||||||
|
|
||||||
|
impl<E> Display for RepositoryError<E>
|
||||||
|
where
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rotom_database"
|
name = "cipher_database"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ async-trait = "0.1.85"
|
|||||||
diesel = { version = "2.2.6", default-features = false }
|
diesel = { version = "2.2.6", default-features = false }
|
||||||
diesel-async = { version = "0.5.2", features = ["bb8"] }
|
diesel-async = { version = "0.5.2", features = ["bb8"] }
|
||||||
diesel_migrations = "2.2.0"
|
diesel_migrations = "2.2.0"
|
||||||
rotom_core = { path = "../rotom_core" }
|
cipher_core = { path = "../cipher_core" }
|
||||||
thiserror = "2.0.11"
|
thiserror = "2.0.11"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
use diesel_async::pooled_connection::bb8::Pool;
|
use diesel_async::pooled_connection::bb8::Pool;
|
||||||
use diesel_async::pooled_connection::bb8::PooledConnection;
|
use diesel_async::pooled_connection::bb8::PooledConnection;
|
||||||
use diesel_async::AsyncMysqlConnection;
|
use diesel_async::AsyncMysqlConnection;
|
||||||
use rotom_core::repository::Repository;
|
use cipher_core::repository::Repository;
|
||||||
use rotom_core::repository::RepositoryError;
|
use cipher_core::repository::RepositoryError;
|
||||||
use rotom_core::repository::RepositoryProvider;
|
use cipher_core::repository::RepositoryProvider;
|
||||||
|
|
||||||
use crate::BackendError;
|
use crate::BackendError;
|
||||||
|
|
||||||
@@ -2,10 +2,10 @@ use diesel::prelude::*;
|
|||||||
use diesel_async::scoped_futures::ScopedFutureExt;
|
use diesel_async::scoped_futures::ScopedFutureExt;
|
||||||
use diesel_async::AsyncConnection;
|
use diesel_async::AsyncConnection;
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use rotom_core::repository::user_repository::NewUser;
|
use cipher_core::repository::user_repository::NewUser;
|
||||||
use rotom_core::repository::user_repository::User;
|
use cipher_core::repository::user_repository::User;
|
||||||
use rotom_core::repository::user_repository::UserRepository;
|
use cipher_core::repository::user_repository::UserRepository;
|
||||||
use rotom_core::repository::RepositoryError;
|
use cipher_core::repository::RepositoryError;
|
||||||
|
|
||||||
use crate::mysql::schema::users;
|
use crate::mysql::schema::users;
|
||||||
use crate::BackendError;
|
use crate::BackendError;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
use diesel_async::pooled_connection::bb8::Pool;
|
use diesel_async::pooled_connection::bb8::Pool;
|
||||||
use diesel_async::pooled_connection::bb8::PooledConnection;
|
use diesel_async::pooled_connection::bb8::PooledConnection;
|
||||||
use diesel_async::AsyncPgConnection;
|
use diesel_async::AsyncPgConnection;
|
||||||
use rotom_core::repository::Repository;
|
use cipher_core::repository::Repository;
|
||||||
use rotom_core::repository::RepositoryError;
|
use cipher_core::repository::RepositoryError;
|
||||||
use rotom_core::repository::RepositoryProvider;
|
use cipher_core::repository::RepositoryProvider;
|
||||||
|
|
||||||
use crate::BackendError;
|
use crate::BackendError;
|
||||||
|
|
||||||
@@ -2,10 +2,10 @@ use diesel::prelude::*;
|
|||||||
use diesel_async::scoped_futures::ScopedFutureExt;
|
use diesel_async::scoped_futures::ScopedFutureExt;
|
||||||
use diesel_async::AsyncConnection;
|
use diesel_async::AsyncConnection;
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use rotom_core::repository::user_repository::NewUser;
|
use cipher_core::repository::user_repository::NewUser;
|
||||||
use rotom_core::repository::user_repository::User;
|
use cipher_core::repository::user_repository::User;
|
||||||
use rotom_core::repository::user_repository::UserRepository;
|
use cipher_core::repository::user_repository::UserRepository;
|
||||||
use rotom_core::repository::RepositoryError;
|
use cipher_core::repository::RepositoryError;
|
||||||
|
|
||||||
use crate::postgres::schema::users;
|
use crate::postgres::schema::users;
|
||||||
use crate::BackendError;
|
use crate::BackendError;
|
||||||
@@ -2,9 +2,9 @@ use diesel::SqliteConnection;
|
|||||||
use diesel_async::pooled_connection::bb8::Pool;
|
use diesel_async::pooled_connection::bb8::Pool;
|
||||||
use diesel_async::pooled_connection::bb8::PooledConnection;
|
use diesel_async::pooled_connection::bb8::PooledConnection;
|
||||||
use diesel_async::sync_connection_wrapper::SyncConnectionWrapper;
|
use diesel_async::sync_connection_wrapper::SyncConnectionWrapper;
|
||||||
use rotom_core::repository::Repository;
|
use cipher_core::repository::Repository;
|
||||||
use rotom_core::repository::RepositoryError;
|
use cipher_core::repository::RepositoryError;
|
||||||
use rotom_core::repository::RepositoryProvider;
|
use cipher_core::repository::RepositoryProvider;
|
||||||
|
|
||||||
use crate::BackendError;
|
use crate::BackendError;
|
||||||
|
|
||||||
@@ -2,10 +2,10 @@ use diesel::prelude::*;
|
|||||||
use diesel_async::scoped_futures::ScopedFutureExt;
|
use diesel_async::scoped_futures::ScopedFutureExt;
|
||||||
use diesel_async::AsyncConnection;
|
use diesel_async::AsyncConnection;
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use rotom_core::repository::user_repository::NewUser;
|
use cipher_core::repository::user_repository::NewUser;
|
||||||
use rotom_core::repository::user_repository::User;
|
use cipher_core::repository::user_repository::User;
|
||||||
use rotom_core::repository::user_repository::UserRepository;
|
use cipher_core::repository::user_repository::UserRepository;
|
||||||
use rotom_core::repository::RepositoryError;
|
use cipher_core::repository::RepositoryError;
|
||||||
|
|
||||||
use crate::sqlite::schema::users;
|
use crate::sqlite::schema::users;
|
||||||
use crate::BackendError;
|
use crate::BackendError;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rotom_discord_bot"
|
name = "cipher_discord_bot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -7,15 +7,18 @@ edition = "2021"
|
|||||||
clap = { version = "4.5.27", features = ["derive", "env"] }
|
clap = { version = "4.5.27", features = ["derive", "env"] }
|
||||||
dotenvy = { version = "0.15.7", features = ["clap"] }
|
dotenvy = { version = "0.15.7", features = ["clap"] }
|
||||||
env_logger = "0.11.6"
|
env_logger = "0.11.6"
|
||||||
|
humantime = "2.1.0"
|
||||||
log = "0.4.25"
|
log = "0.4.25"
|
||||||
rotom_core = { path = "../rotom_core" }
|
poise = "0.6.1"
|
||||||
rotom_database = { path = "../rotom_database", default-features = false }
|
cipher_core = { path = "../cipher_core" }
|
||||||
|
cipher_database = { path = "../cipher_database", default-features = false }
|
||||||
secrecy = "0.10.3"
|
secrecy = "0.10.3"
|
||||||
|
serenity = "0.12.4"
|
||||||
thiserror = "2.0.11"
|
thiserror = "2.0.11"
|
||||||
tokio = { version = "1.43.0", features = ["full"] }
|
tokio = { version = "1.43.0", features = ["full"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["mysql", "postgres", "sqlite"]
|
default = ["mysql", "postgres", "sqlite"]
|
||||||
mysql = ["rotom_database/mysql"]
|
mysql = ["cipher_database/mysql"]
|
||||||
postgres = ["rotom_database/postgres"]
|
postgres = ["cipher_database/postgres"]
|
||||||
sqlite = ["rotom_database/sqlite"]
|
sqlite = ["cipher_database/sqlite"]
|
||||||
30
cipher_discord_bot/src/app/event_handler.rs
Normal file
30
cipher_discord_bot/src/app/event_handler.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use cipher_core::repository::RepositoryProvider;
|
||||||
|
use serenity::all::FullEvent;
|
||||||
|
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
|
use super::AppData;
|
||||||
|
use super::AppError;
|
||||||
|
|
||||||
|
pub async fn event_handler<R: RepositoryProvider>(
|
||||||
|
serenity_ctx: &serenity::client::Context,
|
||||||
|
event: &FullEvent,
|
||||||
|
framework_ctx: poise::FrameworkContext<'_, AppData<R>, AppError<R::BackendError>>,
|
||||||
|
_data: &AppData<R>,
|
||||||
|
) -> Result<(), AppError<R::BackendError>> {
|
||||||
|
match event {
|
||||||
|
FullEvent::Ready { data_about_bot } => {
|
||||||
|
log::info!(
|
||||||
|
"Connected as {} in {} guild(s).",
|
||||||
|
data_about_bot.user.name,
|
||||||
|
data_about_bot.guilds.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
FullEvent::CacheReady { guilds } => {
|
||||||
|
utils::register_in_guilds(serenity_ctx, &framework_ctx.options.commands, guilds).await;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
41
cipher_discord_bot/src/app/framework.rs
Normal file
41
cipher_discord_bot/src/app/framework.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use poise::Framework;
|
||||||
|
use poise::FrameworkOptions;
|
||||||
|
use cipher_core::repository::RepositoryProvider;
|
||||||
|
|
||||||
|
use crate::commands;
|
||||||
|
|
||||||
|
use super::event_handler;
|
||||||
|
use super::on_error;
|
||||||
|
use super::AppData;
|
||||||
|
use super::AppError;
|
||||||
|
|
||||||
|
pub fn framework<R>(repository_provider: R) -> Framework<AppData<R>, AppError<R::BackendError>>
|
||||||
|
where
|
||||||
|
R: RepositoryProvider + Send + Sync + 'static,
|
||||||
|
R::BackendError: Send + Sync,
|
||||||
|
for<'a> R::Repository<'a>: Send + Sync,
|
||||||
|
{
|
||||||
|
let commands = commands::commands();
|
||||||
|
|
||||||
|
let app_data = AppData {
|
||||||
|
repository_provider,
|
||||||
|
};
|
||||||
|
|
||||||
|
let options = FrameworkOptions::<AppData<R>, AppError<R::BackendError>> {
|
||||||
|
commands,
|
||||||
|
on_error: |framework_error| {
|
||||||
|
Box::pin(async move { on_error::on_error(framework_error).await })
|
||||||
|
},
|
||||||
|
event_handler: |serenity_ctx, event, framework_ctx, data| {
|
||||||
|
Box::pin(async move {
|
||||||
|
event_handler::event_handler(serenity_ctx, event, framework_ctx, data).await
|
||||||
|
})
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
Framework::builder()
|
||||||
|
.options(options)
|
||||||
|
.setup(|_ctx, _ready, _framework| Box::pin(async move { Ok(app_data) }))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
63
cipher_discord_bot/src/app/mod.rs
Normal file
63
cipher_discord_bot/src/app/mod.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
use cipher_core::repository::RepositoryError;
|
||||||
|
use cipher_core::repository::RepositoryProvider;
|
||||||
|
use secrecy::ExposeSecret;
|
||||||
|
use serenity::all::GatewayIntents;
|
||||||
|
use serenity::Client;
|
||||||
|
|
||||||
|
use crate::cli::DiscordCredentials;
|
||||||
|
|
||||||
|
mod event_handler;
|
||||||
|
mod framework;
|
||||||
|
mod on_error;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AppStartError {
|
||||||
|
#[error(transparent)]
|
||||||
|
SerenityError(#[from] serenity::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppData<R> {
|
||||||
|
repository_provider: R,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AppError<E> {
|
||||||
|
#[error(transparent)]
|
||||||
|
SerenityError(#[from] serenity::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
RepositoryError(#[from] RepositoryError<E>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type AppContext<'a, R, E> = poise::ApplicationContext<'a, AppData<R>, AppError<E>>;
|
||||||
|
pub type AppCommand<R, E> = poise::Command<AppData<R>, AppError<E>>;
|
||||||
|
|
||||||
|
pub async fn start<R>(credentials: DiscordCredentials, repository_provider: R) -> Result<(), AppStartError>
|
||||||
|
where
|
||||||
|
R: RepositoryProvider + Send + Sync + 'static,
|
||||||
|
R::BackendError: Send + Sync,
|
||||||
|
for<'a> R::Repository<'a>: Send + Sync,
|
||||||
|
{
|
||||||
|
let mut client = Client::builder(credentials.bot_token.expose_secret(), GatewayIntents::all())
|
||||||
|
.framework(framework::framework(repository_provider))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let shard_manager = client.shard_manager.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = tokio::signal::ctrl_c().await {
|
||||||
|
log::error!("Failed to register ctrl+c handler: {}", err);
|
||||||
|
}
|
||||||
|
log::info!("Stopping.");
|
||||||
|
shard_manager.shutdown_all().await;
|
||||||
|
});
|
||||||
|
|
||||||
|
client.start().await.map_err(AppStartError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> AppData<R>
|
||||||
|
where
|
||||||
|
R: RepositoryProvider,
|
||||||
|
{
|
||||||
|
pub async fn repository(&self) -> Result<R::Repository<'_>, RepositoryError<R::BackendError>> {
|
||||||
|
self.repository_provider.get().await
|
||||||
|
}
|
||||||
|
}
|
||||||
298
cipher_discord_bot/src/app/on_error.rs
Normal file
298
cipher_discord_bot/src/app/on_error.rs
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
use cipher_core::repository::RepositoryError;
|
||||||
|
use cipher_core::repository::RepositoryProvider;
|
||||||
|
use poise::CreateReply;
|
||||||
|
use poise::FrameworkError;
|
||||||
|
use serenity::all::Color;
|
||||||
|
use serenity::all::CreateEmbed;
|
||||||
|
use serenity::all::Permissions;
|
||||||
|
|
||||||
|
use super::AppData;
|
||||||
|
use super::AppError;
|
||||||
|
|
||||||
|
struct ErrorMessage {
|
||||||
|
embed: Option<ErrorEmbed>,
|
||||||
|
log: Option<ErrorLog>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ErrorEmbed {
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ErrorLog {
|
||||||
|
message: String,
|
||||||
|
log_level: log::Level,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorMessage {
|
||||||
|
fn new<T, D, M>(title: T, description: D, message: M, log_level: log::Level) -> Self
|
||||||
|
where
|
||||||
|
T: ToString,
|
||||||
|
D: ToString,
|
||||||
|
M: ToString,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
embed: Some(ErrorEmbed { title: title.to_string(), description: description.to_string() }),
|
||||||
|
log: Some(ErrorLog { message: message.to_string(), log_level }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub async fn on_error<R>(framework_error: FrameworkError<'_, AppData<R>, AppError<R::BackendError>>)
|
||||||
|
where
|
||||||
|
R: RepositoryProvider,
|
||||||
|
{
|
||||||
|
let ctx = framework_error.ctx();
|
||||||
|
|
||||||
|
let error_data = ErrorMessage::from(framework_error);
|
||||||
|
|
||||||
|
match error_data.log {
|
||||||
|
Some(ErrorLog { message, log_level: log::Level::Trace }) => log::trace!("{}", message),
|
||||||
|
Some(ErrorLog { message, log_level: log::Level::Debug }) => log::debug!("{}", message),
|
||||||
|
Some(ErrorLog { message, log_level: log::Level::Info }) => log::info!("{}", message),
|
||||||
|
Some(ErrorLog { message, log_level: log::Level::Warn }) => log::warn!("{}", message),
|
||||||
|
Some(ErrorLog { message, log_level: log::Level::Error }) => log::error!("{}", message),
|
||||||
|
None => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((ctx, ErrorEmbed { title, description })) = ctx.zip(error_data.embed) {
|
||||||
|
let embed = CreateEmbed::new()
|
||||||
|
.title(title)
|
||||||
|
.description(description)
|
||||||
|
.color(Color::RED);
|
||||||
|
|
||||||
|
let reply = CreateReply::default()
|
||||||
|
.embed(embed)
|
||||||
|
.ephemeral(true);
|
||||||
|
|
||||||
|
ctx.send(reply).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, R> From<FrameworkError<'a, AppData<R>, AppError<R::BackendError>>> for ErrorMessage
|
||||||
|
where
|
||||||
|
R: RepositoryProvider,
|
||||||
|
{
|
||||||
|
fn from(value: FrameworkError<'a, AppData<R>, AppError<R::BackendError>>) -> ErrorMessage {
|
||||||
|
use FrameworkError as F;
|
||||||
|
|
||||||
|
fn format_permissions(permissions: Permissions) -> String {
|
||||||
|
permissions
|
||||||
|
.iter_names()
|
||||||
|
.map(|(name, _)| format!("`{}`", name))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
match value {
|
||||||
|
F::Setup { error, framework, data_about_bot, ctx, .. } => error.into(),
|
||||||
|
F::EventHandler { error, ctx, event, framework, .. } => error.into(),
|
||||||
|
F::Command { error, ctx, .. } => error.into(),
|
||||||
|
F::SubcommandRequired { ctx } => ErrorMessage::new(
|
||||||
|
"Expected Subcommand",
|
||||||
|
format!("Expected subcommand for `/{}`. Please contact a bot administrator to review the logs for further details.", ctx.command().qualified_name),
|
||||||
|
format!("expected subcommand for `/{}`. this error has likely occurred due to the application commands not being synced with discord.", ctx.command().qualified_name),
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
F::CommandPanic { ctx, payload, .. } => ErrorMessage::new(
|
||||||
|
"A Panic Has Occurred",
|
||||||
|
"A panic has occurred during command execution. Please contact a bot administrator to review the logs for further details.",
|
||||||
|
format!("panic in command `{}`: {}", ctx.command().qualified_name, payload.unwrap_or_else(|| "Unknown panic".to_string())),
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
F::ArgumentParse { error, input, ctx, .. } => ErrorMessage::new(
|
||||||
|
"Argument Parse Error",
|
||||||
|
"Failed to parse argument in command. Please contact a bot administrator to review the logs for further details.",
|
||||||
|
format!("failed to parse argument in command `{}` on input {:?}", ctx.command().qualified_name, input),
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
F::CommandStructureMismatch { description, ctx, .. } => ErrorMessage::new(
|
||||||
|
"Command Structure Mismatch",
|
||||||
|
"Unexpected application command structure. Please contact a bot administrator to review the logs for further details.",
|
||||||
|
format!("unexpected application command structure in command `{}`: {}", ctx.command.qualified_name, description),
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
F::CooldownHit { remaining_cooldown, ctx, .. } => ErrorMessage::new(
|
||||||
|
"Cooldown Hit",
|
||||||
|
format!("You can't use that command right now. Try again in {}.", humantime::format_duration(remaining_cooldown)),
|
||||||
|
format!("cooldown hit in command `{}` ({:?} remaining)", ctx.command().qualified_name, remaining_cooldown),
|
||||||
|
log::Level::Info,
|
||||||
|
),
|
||||||
|
F::MissingBotPermissions { missing_permissions, ctx, .. } => ErrorMessage::new(
|
||||||
|
"Insufficient Bot Permissions",
|
||||||
|
format!("The bot is missing the following permissions: {}.", format_permissions(missing_permissions)),
|
||||||
|
format!("bot is missing permissions ({}) to execute command `{}`", missing_permissions, ctx.command().qualified_name),
|
||||||
|
log::Level::Info,
|
||||||
|
),
|
||||||
|
F::MissingUserPermissions { missing_permissions, ctx, .. } => ErrorMessage::new(
|
||||||
|
"Insufficient User Permissions",
|
||||||
|
missing_permissions
|
||||||
|
.map(|p| format!("You are missing the following permissions: {}.", format_permissions(p)))
|
||||||
|
.unwrap_or_else(|| "Failed to get user permissions.".to_string()),
|
||||||
|
format!("user is or may be missing permissions ({:?}) to execute command `{}`", missing_permissions, ctx.command().qualified_name),
|
||||||
|
log::Level::Info,
|
||||||
|
),
|
||||||
|
F::NotAnOwner { ctx, .. } => ErrorMessage::new(
|
||||||
|
"Owner Only Command",
|
||||||
|
format!("`/{}` can only be used by bot owners.", ctx.command().qualified_name),
|
||||||
|
format!("owner-only command `{}` cannot be run by non-owners", ctx.command().qualified_name),
|
||||||
|
log::Level::Info,
|
||||||
|
),
|
||||||
|
F::GuildOnly { ctx, .. } => ErrorMessage::new(
|
||||||
|
"Guild Only Command",
|
||||||
|
format!("`/{}` can only be used in a server.", ctx.command().qualified_name),
|
||||||
|
format!("guild-only command `{}` cannot be run in DMs", ctx.command().qualified_name),
|
||||||
|
log::Level::Info,
|
||||||
|
),
|
||||||
|
F::DmOnly { ctx, .. } => ErrorMessage::new(
|
||||||
|
"Direct Message Only Command",
|
||||||
|
format!("`/{}` can only be used in direct messages.", ctx.command().qualified_name),
|
||||||
|
format!("DM-only command `{}` cannot be run in DMs", ctx.command().qualified_name),
|
||||||
|
log::Level::Info,
|
||||||
|
),
|
||||||
|
F::NsfwOnly { ctx, .. } => ErrorMessage::new(
|
||||||
|
"NSFW Only Command",
|
||||||
|
format!("`/{}` can only be used in channels marked as NSFW.", ctx.command().qualified_name),
|
||||||
|
format!("nsfw-only command `{}` cannot be run in non-nsfw channels", ctx.command().qualified_name),
|
||||||
|
log::Level::Info,
|
||||||
|
),
|
||||||
|
F::CommandCheckFailed { error, ctx, .. } => error.map(Into::into).unwrap_or_else(|| ErrorMessage::new(
|
||||||
|
"Command Check Failed",
|
||||||
|
"A pre-command check failed without a reason. Please contact a bot administrator to review the logs for further details.",
|
||||||
|
format!("pre-command check for command `{}` either denied access or errored without a reason", ctx.command().qualified_name),
|
||||||
|
log::Level::Warn,
|
||||||
|
)),
|
||||||
|
F::DynamicPrefix { error, ctx, msg, .. } => ErrorMessage::new(
|
||||||
|
"Dynamic Prefix Error",
|
||||||
|
format!("Dynamic prefix callback error on message {:?}", msg.content),
|
||||||
|
format!("dynamic prefix callback error on message {:?}", msg.content),
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
F::UnknownCommand { ctx, msg, prefix, msg_content, framework, invocation_data, trigger, .. } => ErrorMessage::new(
|
||||||
|
"Unknown Command",
|
||||||
|
format!("Unknown command `{}`", msg_content),
|
||||||
|
format!("unknown command `{}`", msg_content),
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
F::UnknownInteraction { ctx, framework, interaction, .. } => ErrorMessage::new(
|
||||||
|
"Unknown Interaction",
|
||||||
|
format!("Unknown interaction `{}`", interaction.data.name),
|
||||||
|
format!("unknown interaction `{}`", interaction.data.name),
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
unknown_error => ErrorMessage::new(
|
||||||
|
"Unexpected Error",
|
||||||
|
"An unexpected error has occurred. Please contact a bot administrator to review the logs for further details.",
|
||||||
|
format!("unknown error: {}", unknown_error),
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> From<AppError<E>> for ErrorMessage
|
||||||
|
where
|
||||||
|
E: std::error::Error,
|
||||||
|
{
|
||||||
|
fn from(value: AppError<E>) -> ErrorMessage {
|
||||||
|
use AppError as A;
|
||||||
|
use serenity::Error as S;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
match value {
|
||||||
|
A::SerenityError(S::Decode(msg, value)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
msg,
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(S::Format(error)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
error,
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(S::Io(error)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
error,
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(S::Json(error)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
error,
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(S::Model(error)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
error,
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(S::ExceededLimit(_, _)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
"input exceeded a limit",
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(S::NotInRange(_, _, _, _)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
"input is not in the specified range",
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(S::Other(msg)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
msg,
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(S::Url(msg)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
msg,
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(S::Client(error)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
error,
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(S::Gateway(error)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
error,
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(S::Http(http_error)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
http_error,
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(S::Tungstenite(error)) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
error,
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
A::SerenityError(unknown_error) => ErrorMessage::new(
|
||||||
|
"Internal Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
format!("unknown error: {}", unknown_error),
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
|
||||||
|
A::RepositoryError(RepositoryError(error)) => ErrorMessage::new(
|
||||||
|
"Repository Backend Error",
|
||||||
|
"Please contact a bot administrator to review the logs for further details.",
|
||||||
|
format!("repository backend error: {}", error),
|
||||||
|
log::Level::Error,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ pub fn parse() -> Result<Cli, CliError> {
|
|||||||
/// because `Dotenv` disables them and `Cli` requires them to be enabled.
|
/// because `Dotenv` disables them and `Cli` requires them to be enabled.
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
name = "rotom",
|
name = "cipher",
|
||||||
about,
|
about,
|
||||||
version,
|
version,
|
||||||
long_about = None,
|
long_about = None,
|
||||||
@@ -97,9 +97,9 @@ pub enum DatabaseDialect {
|
|||||||
Sqlite,
|
Sqlite,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DatabaseDialect> for rotom_database::DatabaseDialect {
|
impl From<DatabaseDialect> for cipher_database::DatabaseDialect {
|
||||||
fn from(value: DatabaseDialect) -> Self {
|
fn from(value: DatabaseDialect) -> Self {
|
||||||
use rotom_database::DatabaseDialect as Dialect;
|
use cipher_database::DatabaseDialect as Dialect;
|
||||||
match value {
|
match value {
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
DatabaseDialect::Mysql => Dialect::Mysql,
|
DatabaseDialect::Mysql => Dialect::Mysql,
|
||||||
@@ -19,9 +19,9 @@ pub struct Start {
|
|||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum StartError {
|
pub enum StartError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
RepositoryBackendError(#[from] rotom_database::BackendError),
|
RepositoryBackendError(#[from] cipher_database::BackendError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
AppError(#[from] crate::app::AppError),
|
AppError(#[from] crate::app::AppStartError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Start {
|
impl Start {
|
||||||
@@ -34,24 +34,24 @@ impl Start {
|
|||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
crate::cli::DatabaseDialect::Mysql => {
|
crate::cli::DatabaseDialect::Mysql => {
|
||||||
log::info!("Running any pending database migrations.");
|
log::info!("Running any pending database migrations.");
|
||||||
rotom_database::mysql::run_pending_migrations(database_url)?;
|
cipher_database::mysql::run_pending_migrations(database_url)?;
|
||||||
let repository_provider = rotom_database::mysql::repository_provider(database_url).await?;
|
let repository_provider = cipher_database::mysql::repository_provider(database_url).await?;
|
||||||
log::info!("Starting discord application.");
|
log::info!("Starting discord application.");
|
||||||
crate::app::start(self.discord, repository_provider).await?;
|
crate::app::start(self.discord, repository_provider).await?;
|
||||||
},
|
},
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
crate::cli::DatabaseDialect::Postgres => {
|
crate::cli::DatabaseDialect::Postgres => {
|
||||||
log::info!("Running any pending database migrations.");
|
log::info!("Running any pending database migrations.");
|
||||||
rotom_database::postgres::run_pending_migrations(database_url)?;
|
cipher_database::postgres::run_pending_migrations(database_url)?;
|
||||||
let repository_provider = rotom_database::postgres::repository_provider(database_url).await?;
|
let repository_provider = cipher_database::postgres::repository_provider(database_url).await?;
|
||||||
log::info!("Starting discord application.");
|
log::info!("Starting discord application.");
|
||||||
crate::app::start(self.discord, repository_provider).await?;
|
crate::app::start(self.discord, repository_provider).await?;
|
||||||
},
|
},
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
crate::cli::DatabaseDialect::Sqlite => {
|
crate::cli::DatabaseDialect::Sqlite => {
|
||||||
log::info!("Running any pending database migrations.");
|
log::info!("Running any pending database migrations.");
|
||||||
rotom_database::sqlite::run_pending_migrations(database_url)?;
|
cipher_database::sqlite::run_pending_migrations(database_url)?;
|
||||||
let repository_provider = rotom_database::sqlite::repository_provider(database_url).await?;
|
let repository_provider = cipher_database::sqlite::repository_provider(database_url).await?;
|
||||||
log::info!("Starting discord application.");
|
log::info!("Starting discord application.");
|
||||||
crate::app::start(self.discord, repository_provider).await?;
|
crate::app::start(self.discord, repository_provider).await?;
|
||||||
},
|
},
|
||||||
14
cipher_discord_bot/src/commands/mod.rs
Normal file
14
cipher_discord_bot/src/commands/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use cipher_core::repository::RepositoryProvider;
|
||||||
|
|
||||||
|
use crate::app::AppCommand;
|
||||||
|
|
||||||
|
mod ping;
|
||||||
|
|
||||||
|
pub fn commands<R>() -> Vec<AppCommand<R, R::BackendError>>
|
||||||
|
where
|
||||||
|
R: RepositoryProvider + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
vec![
|
||||||
|
ping::ping(),
|
||||||
|
]
|
||||||
|
}
|
||||||
22
cipher_discord_bot/src/commands/ping.rs
Normal file
22
cipher_discord_bot/src/commands/ping.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use poise::CreateReply;
|
||||||
|
use cipher_core::repository::RepositoryProvider;
|
||||||
|
use serenity::all::CreateEmbed;
|
||||||
|
|
||||||
|
use crate::app::AppContext;
|
||||||
|
use crate::app::AppError;
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
|
/// Is the bot alive or dead? :thinking:
|
||||||
|
#[poise::command(slash_command)]
|
||||||
|
pub async fn ping<R: RepositoryProvider + Send + Sync>(ctx: AppContext<'_, R, R::BackendError>) -> Result<(), AppError<R::BackendError>> {
|
||||||
|
let embed = CreateEmbed::new()
|
||||||
|
.title("Pong :ping_pong:")
|
||||||
|
.color(utils::bot_color(&ctx).await);
|
||||||
|
|
||||||
|
let reply = CreateReply::default()
|
||||||
|
.embed(embed);
|
||||||
|
|
||||||
|
ctx.send(reply).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
mod commands;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
enum MainError {
|
enum MainError {
|
||||||
35
cipher_discord_bot/src/utils.rs
Normal file
35
cipher_discord_bot/src/utils.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use cipher_core::repository::RepositoryProvider;
|
||||||
|
use serenity::all::{Color, GuildId};
|
||||||
|
|
||||||
|
use crate::app::{AppCommand, AppContext};
|
||||||
|
|
||||||
|
pub async fn register_in_guilds<R>(
|
||||||
|
serenity_ctx: &serenity::client::Context,
|
||||||
|
commands: &[AppCommand<R, R::BackendError>],
|
||||||
|
guilds: &[GuildId],
|
||||||
|
)
|
||||||
|
where
|
||||||
|
R: RepositoryProvider,
|
||||||
|
{
|
||||||
|
for guild in guilds {
|
||||||
|
let result = poise::builtins::register_in_guild(serenity_ctx, commands, *guild).await;
|
||||||
|
match (guild.name(serenity_ctx), result) {
|
||||||
|
(None, Err(err)) => log::warn!("Failed to register command in guild with id {}: {}", guild.get().to_string(), err),
|
||||||
|
(None, Ok(())) => log::info!("Successfully registered commands in guild with id {}", guild.get().to_string()),
|
||||||
|
(Some(guild_name), Err(err)) => log::warn!("Failed to register command in guild with id {} ({}): {}", guild.get().to_string(), guild_name, err),
|
||||||
|
(Some(guild_name), Ok(())) => log::info!("Successfully registered commands in guild with id {} ({})", guild.get().to_string(), guild_name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bot_color<R>(ctx: &AppContext<'_, R, R::BackendError>) -> Color
|
||||||
|
where
|
||||||
|
R: RepositoryProvider + Send + Sync,
|
||||||
|
{
|
||||||
|
let member = match ctx.guild_id() {
|
||||||
|
Some(guild) => guild.member(ctx, ctx.framework.bot_id).await.ok(),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
member.and_then(|m| m.colour(ctx)).unwrap_or(Color::BLURPLE)
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
use rotom_core::repository::RepositoryProvider;
|
|
||||||
|
|
||||||
use crate::cli::DiscordCredentials;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum AppError {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start<R>(_credentials: DiscordCredentials, _repository_provider: R) -> Result<(), AppError>
|
|
||||||
where
|
|
||||||
R: RepositoryProvider + Send + Sync + 'static,
|
|
||||||
R::BackendError: Send + Sync,
|
|
||||||
for<'a> R::Repository<'a>: Send + Sync,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user