Refactor keyboard utilities and improve user interface

- Extract keyboard utility functions to new keyboard_utils.rs module
- Update new listing command with improved keyboard handling
- Enhance message utilities with better user interaction
- Refactor user ID type handling
- Remove development database file
- Update main.rs with improved structure
This commit is contained in:
Dylan Knutson
2025-08-28 19:50:17 +00:00
parent 1d4d5c05ed
commit d4ccbb884c
6 changed files with 813 additions and 1057 deletions

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ use sqlx::{
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type, encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
}; };
use std::fmt; use std::fmt;
use teloxide::types::ChatId;
/// Type-safe wrapper for user IDs /// Type-safe wrapper for user IDs
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]

83
src/keyboard_utils.rs Normal file
View File

@@ -0,0 +1,83 @@
#[macro_export]
macro_rules! keyboard_buttons {
($vis:vis enum $name:ident {
$($variant:ident($text:literal, $callback_data:literal),)*
}) => {
keyboard_buttons! {
$vis enum $name {
[$($variant($text, $callback_data),)*]
}
}
};
($vis:vis enum $name:ident {
$([
$($variant:ident($text:literal, $callback_data:literal),)*
]),*
}) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
$vis enum $name {
$(
$($variant,)*
)*
}
impl $name {
#[allow(unused)]
pub fn to_keyboard() -> teloxide::types::InlineKeyboardMarkup {
let markup = teloxide::types::InlineKeyboardMarkup::default();
$(
let markup = markup.append_row([
$(
teloxide::types::InlineKeyboardButton::callback($text, $callback_data),
)*
]);
)*
markup
}
}
impl Into<teloxide::types::InlineKeyboardButton> for $name {
fn into(self) -> teloxide::types::InlineKeyboardButton {
match self {
$($(Self::$variant => teloxide::types::InlineKeyboardButton::callback($text, $callback_data)),*),*
}
}
}
impl<'a> TryFrom<&'a str> for $name {
type Error = &'a str;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
match value {
$($(
$callback_data => Ok(Self::$variant),
)*)*
_ => Err(value),
}
}
}
};
}
#[cfg(test)]
mod tests {
use teloxide::types::{InlineKeyboardButton, InlineKeyboardButtonKind};
use super::*;
keyboard_buttons! {
pub enum DurationKeyboardButtons {
OneDay("1 day", "duration_1_day"),
ThreeDays("3 days", "duration_3_days"),
SevenDays("7 days", "duration_7_days"),
FourteenDays("14 days", "duration_14_days"),
}
}
#[test]
fn test_duration_keyboard_buttons() {
let button: InlineKeyboardButton = DurationKeyboardButtons::OneDay.into();
assert_eq!(button.text, "1 day");
assert_eq!(
button.kind,
InlineKeyboardButtonKind::CallbackData("duration_1_day".to_string())
);
}
}

View File

@@ -1,6 +1,7 @@
mod commands; mod commands;
mod config; mod config;
mod db; mod db;
mod keyboard_utils;
mod message_utils; mod message_utils;
mod sqlite_storage; mod sqlite_storage;
@@ -16,7 +17,7 @@ use config::Config;
use crate::commands::new_listing::new_listing_handler; use crate::commands::new_listing::new_listing_handler;
use crate::sqlite_storage::SqliteStorage; use crate::sqlite_storage::SqliteStorage;
pub type HandlerResult = anyhow::Result<()>; pub type HandlerResult<T = ()> = anyhow::Result<T>;
#[derive(BotCommands, Clone)] #[derive(BotCommands, Clone)]
#[command(rename_rule = "lowercase", description = "Auction Bot Commands")] #[command(rename_rule = "lowercase", description = "Auction Bot Commands")]

View File

@@ -1,43 +1,57 @@
use std::fmt::Display; use std::fmt::Display;
use anyhow::bail;
use teloxide::{ use teloxide::{
dispatching::dialogue::GetChatId,
payloads::{EditMessageTextSetters as _, SendMessageSetters as _}, payloads::{EditMessageTextSetters as _, SendMessageSetters as _},
prelude::Requester as _, prelude::Requester as _,
types::{Chat, ChatId, InlineKeyboardMarkup, MessageId, ParseMode, User}, types::{
CallbackQuery, Chat, ChatId, InlineKeyboardButton, InlineKeyboardMarkup, MessageId,
ParseMode, User,
},
Bot, Bot,
}; };
use crate::HandlerResult; use crate::HandlerResult;
pub struct UserHandleAndId<'s> { #[derive(Debug, Clone, Copy)]
pub struct HandleAndId<'s> {
pub handle: Option<&'s str>, pub handle: Option<&'s str>,
pub id: Option<i64>, pub id: ChatId,
} }
impl<'s> Display for UserHandleAndId<'s> { impl<'s> Display for HandleAndId<'s> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(f, "{}", self.handle.unwrap_or("unknown"))?;
f, write!(f, " ({})", self.id.0)?;
"{} ({})", Ok(())
self.handle.unwrap_or("unknown"),
self.id.unwrap_or(-1)
)
} }
} }
impl<'s> UserHandleAndId<'s> { impl<'s> HandleAndId<'s> {
pub fn from_chat(chat: &'s Chat) -> Self { pub fn from_chat(chat: &'s Chat) -> Self {
Self { Self {
handle: chat.username(), handle: chat.username(),
id: Some(chat.id.0), id: chat.id,
} }
} }
pub fn from_user(user: &'s User) -> Self { pub fn from_user(user: &'s User) -> Self {
Self { Self {
handle: user.username.as_deref(), handle: user.username.as_deref(),
id: Some(user.id.0 as i64), id: user.id.into(),
} }
} }
} }
impl<'s> Into<HandleAndId<'s>> for &'s User {
fn into(self) -> HandleAndId<'s> {
HandleAndId::from_user(self)
}
}
impl<'s> Into<HandleAndId<'s>> for &'s Chat {
fn into(self) -> HandleAndId<'s> {
HandleAndId::from_chat(self)
}
}
pub fn is_cancel_or_no(text: &str) -> bool { pub fn is_cancel_or_no(text: &str) -> bool {
is_cancel(text) || text.eq_ignore_ascii_case("no") is_cancel(text) || text.eq_ignore_ascii_case("no")
} }
@@ -49,11 +63,12 @@ pub fn is_cancel(text: &str) -> bool {
// Unified HTML message sending utility // Unified HTML message sending utility
pub async fn send_html_message( pub async fn send_html_message(
bot: &Bot, bot: &Bot,
chat_id: ChatId, chat: impl Into<HandleAndId<'_>>,
text: &str, text: &str,
keyboard: Option<InlineKeyboardMarkup>, keyboard: Option<InlineKeyboardMarkup>,
) -> HandlerResult { ) -> HandlerResult {
let mut message = bot.send_message(chat_id, text).parse_mode(ParseMode::Html); let chat = chat.into();
let mut message = bot.send_message(chat.id, text).parse_mode(ParseMode::Html);
if let Some(kb) = keyboard { if let Some(kb) = keyboard {
message = message.reply_markup(kb); message = message.reply_markup(kb);
} }
@@ -63,13 +78,14 @@ pub async fn send_html_message(
pub async fn edit_html_message( pub async fn edit_html_message(
bot: &Bot, bot: &Bot,
chat_id: ChatId, chat: impl Into<HandleAndId<'_>>,
message_id: MessageId, message_id: MessageId,
text: &str, text: &str,
keyboard: Option<InlineKeyboardMarkup>, keyboard: Option<InlineKeyboardMarkup>,
) -> HandlerResult { ) -> HandlerResult {
let chat = chat.into();
let mut edit_request = bot let mut edit_request = bot
.edit_message_text(chat_id, message_id, text) .edit_message_text(chat.id, message_id, text)
.parse_mode(ParseMode::Html); .parse_mode(ParseMode::Html);
if let Some(kb) = keyboard { if let Some(kb) = keyboard {
edit_request = edit_request.reply_markup(kb); edit_request = edit_request.reply_markup(kb);
@@ -77,3 +93,68 @@ pub async fn edit_html_message(
edit_request.await?; edit_request.await?;
Ok(()) Ok(())
} }
// ============================================================================
// KEYBOARD CREATION UTILITIES
// ============================================================================
// Create a simple single-button keyboard
pub fn create_single_button_keyboard(text: &str, callback_data: &str) -> InlineKeyboardMarkup {
InlineKeyboardMarkup::new([[InlineKeyboardButton::callback(text, callback_data)]])
}
// Create a keyboard with multiple buttons in a single row
pub fn create_single_row_keyboard(buttons: &[(&str, &str)]) -> InlineKeyboardMarkup {
let keyboard_buttons: Vec<InlineKeyboardButton> = buttons
.iter()
.map(|(text, callback_data)| InlineKeyboardButton::callback(*text, *callback_data))
.collect();
InlineKeyboardMarkup::new([keyboard_buttons])
}
// Create a keyboard with multiple rows
pub fn create_multi_row_keyboard(rows: &[&[(&str, &str)]]) -> InlineKeyboardMarkup {
let mut keyboard = InlineKeyboardMarkup::default();
for row in rows {
let buttons: Vec<InlineKeyboardButton> = row
.iter()
.map(|(text, callback_data)| InlineKeyboardButton::callback(*text, *callback_data))
.collect();
keyboard = keyboard.append_row(buttons);
}
keyboard
}
// Create numeric option keyboard (common pattern for slots, duration, etc.)
pub fn create_numeric_options_keyboard(
options: &[(i32, &str)],
prefix: &str,
) -> InlineKeyboardMarkup {
let buttons: Vec<InlineKeyboardButton> = options
.iter()
.map(|(value, label)| {
InlineKeyboardButton::callback(*label, format!("{}_{}", prefix, value))
})
.collect();
InlineKeyboardMarkup::new([buttons])
}
// Extract callback data and answer callback query
pub async fn extract_callback_data(
bot: &Bot,
callback_query: &CallbackQuery,
) -> HandlerResult<(String, User)> {
let data = match callback_query.data.as_deref() {
Some(data) => data.to_string(),
None => bail!("Missing data in callback query"),
};
let from = callback_query.from.clone();
// Answer the callback query to remove loading state
if let Err(e) = bot.answer_callback_query(callback_query.id.clone()).await {
log::warn!("Failed to answer callback query: {}", e);
}
Ok((data, from))
}