Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7926e07
fix erronous 'add-sticker' command in text string
RansomTime Oct 4, 2025
0a7b12f
Merge pull request #12 from RansomTime/string_fix_new_sticker
ItsLimeNade Oct 4, 2025
f8d644e
feat(graph): doubled graph size
ItsLimeNade Oct 6, 2025
c95d7bd
Merge pull request #22 from ItsLimeNade/v0.2.0-pre-graph_res_bump
ItsLimeNade Oct 6, 2025
a8dc720
feat(bg): add old data warning
ItsLimeNade Oct 10, 2025
45c7b41
chore(fmt)
ItsLimeNade Oct 10, 2025
85a2a65
Merge pull request #23 from ItsLimeNade/v0.2.0-pre-old_data_warn
ItsLimeNade Oct 10, 2025
c8a6574
feat(graph): fixed graph gap collapsing
ItsLimeNade Oct 10, 2025
3f804ce
Contextual stickers
ItsLimeNade Oct 11, 2025
0806571
Merge pull request #24 from ItsLimeNade/v0.2.0-pre_graph-enhancements
ItsLimeNade Oct 12, 2025
a49977e
Merge pull request #25 from ItsLimeNade/v0.2.0-pre
ItsLimeNade Oct 12, 2025
d1a36f3
Added misc commands
ItsLimeNade Oct 12, 2025
cc73a92
chore(fmt): cargo fmt
ItsLimeNade Oct 12, 2025
8499db1
feat(everything): Small update.
ItsLimeNade Oct 12, 2025
b77455b
feat(readme): delete helper readme
ItsLimeNade Oct 12, 2025
7aeea79
Merge pull request #26 from ItsLimeNade/v0.2.0-pre_misc-commands
ItsLimeNade Oct 12, 2025
97f27ba
feat(mbg): fix mbg not rendering
ItsLimeNade Oct 12, 2025
8f93100
chore(fmt & clippy)
ItsLimeNade Oct 12, 2025
b8de7f4
feat(graph & bg)
ItsLimeNade Oct 12, 2025
9854195
feat(bg): finger prick
ItsLimeNade Oct 12, 2025
ab78e3f
Merge pull request #27 from ItsLimeNade/v0.2.0-pre_enhancements
ItsLimeNade Oct 12, 2025
49dc682
feat(ver): Version bump to 0.2.0
ItsLimeNade Oct 12, 2025
3c81aab
Merge branch 'v0.2.0-pre' of https://github.com/ItsLimeNade/Beetroot …
ItsLimeNade Oct 12, 2025
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "beetroot"
version = "0.1.1"
version = "0.2.0"
edition = "2024"

[dependencies]
Expand Down
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