Create client to establish connection with discord and add basic framework with sample command

This commit is contained in:
2025-01-29 22:22:08 +00:00
parent fc07051558
commit a719b143de
50 changed files with 1712 additions and 128 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
resolver = "2" resolver = "2"
members = [ members = [
"rotom_core", "cipher_core",
"rotom_database", "rotom_discord_bot", "cipher_database",
"cipher_discord_bot",
] ]

View File

@@ -1 +1 @@
# rotom # cipher

View File

@@ -1,5 +1,5 @@
[package] [package]
name = "rotom_core" name = "cipher_core"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

View File

@@ -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)
}
}

View File

@@ -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]

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"]

View 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(())
}

View 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()
}

View 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
}
}

View 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,
),
}
}
}

View File

@@ -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,

View File

@@ -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?;
}, },

View 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(),
]
}

View 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(())
}

View File

@@ -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 {

View 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)
}

View File

@@ -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!()
}