Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/bot/command_registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use crate::commands;
use serenity::all::CreateCommand;

/// Get all slash commands and context menu commands to register
pub fn get_all_commands() -> Vec<CreateCommand> {
vec![
// Slash commands
commands::allow::register(),
commands::bg::register(),
commands::convert::register(),
commands::get_nightscout_url::register(),
commands::graph::register(),
commands::help::register(),
commands::info::register(),
commands::set_nightscout_url::register(),
commands::set_threshold::register(),
commands::set_token::register(),
commands::set_visibility::register(),
commands::setup::register(),
commands::stickers::register(),
commands::token::register(),
// Context menu commands
commands::add_sticker::register(),
commands::analyze_units::register(),
]
}
45 changes: 45 additions & 0 deletions src/bot/component_router.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use crate::bot::Handler;
use crate::commands;
use anyhow::Result;
use serenity::all::{ComponentInteraction, Context};

/// Route component interactions (button clicks) to their handlers
pub async fn route_component_interaction(
handler: &Handler,
context: &Context,
component: &ComponentInteraction,
) -> Result<()> {
let custom_id = component.data.custom_id.as_str();

match custom_id {
// Setup buttons
"setup_private" | "setup_public" => {
commands::setup::handle_button(handler, context, component).await
}

// Help pagination buttons
id if id.starts_with("help_page_") => {
commands::help::handle_button(handler, context, component).await
}

// Add sticker buttons
id if id.starts_with("add_sticker_") => {
commands::add_sticker::handle_button(handler, context, component).await
}

// Stickers management buttons
id if id.starts_with("remove_sticker_")
|| id == "clear_all_stickers"
|| id.starts_with("clear_category_stickers_")
|| id.starts_with("stickers_page_") =>
{
commands::stickers::handle_button(handler, context, component).await
}

// Unknown component interaction - ignore silently
_ => {
tracing::debug!("Unhandled component interaction: {}", custom_id);
Ok(())
}
}
}
98 changes: 98 additions & 0 deletions src/bot/event_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use crate::bot::{
Handler, command_registry, component_router, helpers::command_handler, version_checker,
};
use crate::commands;
use serenity::all::{
Command, CreateInteractionResponse, CreateInteractionResponseMessage, Interaction, Ready,
};
use serenity::prelude::*;

#[serenity::async_trait]
impl EventHandler for Handler {
async fn interaction_create(&self, context: Context, interaction: Interaction) {
let result = match interaction {
Interaction::Command(ref command) => {
// Determine if it's a context menu or slash command
let command_result =
if command.data.kind == serenity::model::application::CommandType::Message {
command_handler::handle_context_command(self, &context, command).await
} else {
command_handler::handle_slash_command(self, &context, command).await
};

// Check for version updates after successful command execution
if command_result.is_ok()
&& let Ok(exists) = self.database.user_exists(command.user.id.get()).await
&& exists
{
let _ =
version_checker::check_and_notify_version_update(self, &context, command)
.await;
}

command_result
}

Interaction::Component(ref component) => {
component_router::route_component_interaction(self, &context, component).await
}

_ => Ok(()),
};

// Handle errors
if let Err(e) = result {
let error_msg = format!("There was an error processing your interaction: {}", e);
eprintln!("ERROR: {}", error_msg);

match &interaction {
Interaction::Command(command) => {
if let Err(send_err) = commands::error::run(
&context,
command,
"An unexpected error occurred. Please try again later.",
)
.await
{
eprintln!("Failed to send error response to user: {}", send_err);
}
}
Interaction::Component(component) => {
let error_response = CreateInteractionResponseMessage::new()
.content("[ERROR] An unexpected error occurred. Please try again later.")
.ephemeral(true);

if let Err(send_err) = component
.create_response(
&context.http,
CreateInteractionResponse::Message(error_response),
)
.await
{
eprintln!(
"Failed to send component error response to user: {}",
send_err
);
}
}
_ => {
eprintln!("Unhandled interaction type in error handler");
}
}
}
}

async fn ready(&self, context: Context, ready: Ready) {
tracing::info!("[BOT] {} is ready and connected!", ready.user.name);

let commands_vec = command_registry::get_all_commands();
let command_count = commands_vec.len();

let _commands = Command::set_global_commands(&context, commands_vec).await;

tracing::info!(
"[CMD] Successfully registered {} global commands",
command_count
);
}
}
28 changes: 28 additions & 0 deletions src/bot/handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use ab_glyph::FontArc;
use anyhow::anyhow;

use crate::utils::database::Database;
use crate::utils::nightscout::Nightscout;

#[allow(dead_code)]
pub struct Handler {
pub nightscout_client: Nightscout,
pub database: Database,
pub font: FontArc,
}

impl Handler {
pub async fn new() -> Self {
let font_bytes = std::fs::read("assets/fonts/GeistMono-Regular.ttf")
.map_err(|e| anyhow!("Failed to read font: {}", e))
.unwrap();

Handler {
nightscout_client: Nightscout::new(),
database: Database::new().await.unwrap(),
font: FontArc::try_from_vec(font_bytes)
.map_err(|_| anyhow!("Failed to parse font"))
.unwrap(),
}
}
}
83 changes: 83 additions & 0 deletions src/bot/helpers/command_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use crate::bot::Handler;
use crate::commands;
use anyhow::Result;
use serenity::all::{CommandInteraction, Context};

/// List of commands that don't require user setup
const UNRESTRICTED_COMMANDS: &[&str] = &["setup", "convert", "help"];

/// Route a slash command to its handler
pub async fn handle_slash_command(
handler: &Handler,
context: &Context,
command: &CommandInteraction,
) -> Result<()> {
// Check if user exists for restricted commands
let user_exists = handler.database.user_exists(command.user.id.get()).await?;

if !user_exists && !UNRESTRICTED_COMMANDS.contains(&command.data.name.as_str()) {
return commands::error::run(
context,
command,
"You need to register your Nightscout URL first. Use `/setup` to get started.",
)
.await;
}

// Route to appropriate command handler
match command.data.name.as_str() {
"allow" => commands::allow::run(handler, context, command).await,
"bg" => commands::bg::run(handler, context, command).await,
"convert" => commands::convert::run(handler, context, command).await,
"get-nightscout-url" => commands::get_nightscout_url::run(handler, context, command).await,
"graph" => commands::graph::run(handler, context, command).await,
"help" => commands::help::run(handler, context, command).await,
"info" => commands::info::run(handler, context, command).await,
"set-nightscout-url" => commands::set_nightscout_url::run(handler, context, command).await,
"set-threshold" => commands::set_threshold::run(handler, context, command).await,
"set-token" => commands::set_token::run(handler, context, command).await,
"set-visibility" => commands::set_visibility::run(handler, context, command).await,
"setup" => commands::setup::run(handler, context, command).await,
"stickers" => commands::stickers::run(handler, context, command).await,
"token" => commands::token::run(handler, context, command).await,
unknown_command => {
eprintln!("Unknown slash command received: '{}'", unknown_command);
commands::error::run(
context,
command,
&format!(
"Unknown command: `{}`. Use `/help` to see all available commands.",
unknown_command
),
)
.await
}
}
}

/// Route a context menu command to its handler
pub async fn handle_context_command(
handler: &Handler,
context: &Context,
command: &CommandInteraction,
) -> Result<()> {
match command.data.name.as_str() {
"Add Sticker" => commands::add_sticker::run(handler, context, command).await,
"Analyze Units" => commands::analyze_units::run(handler, context, command).await,
unknown_context_command => {
eprintln!(
"Unknown context menu command received: '{}'",
unknown_context_command
);
commands::error::run(
context,
command,
&format!(
"Unknown context menu command: `{}`",
unknown_context_command
),
)
.await
}
}
}
Loading