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:
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ use sqlx::{
|
||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||
};
|
||||
use std::fmt;
|
||||
use teloxide::types::ChatId;
|
||||
|
||||
/// Type-safe wrapper for user IDs
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
|
||||
83
src/keyboard_utils.rs
Normal file
83
src/keyboard_utils.rs
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod commands;
|
||||
mod config;
|
||||
mod db;
|
||||
mod keyboard_utils;
|
||||
mod message_utils;
|
||||
mod sqlite_storage;
|
||||
|
||||
@@ -16,7 +17,7 @@ use config::Config;
|
||||
use crate::commands::new_listing::new_listing_handler;
|
||||
use crate::sqlite_storage::SqliteStorage;
|
||||
|
||||
pub type HandlerResult = anyhow::Result<()>;
|
||||
pub type HandlerResult<T = ()> = anyhow::Result<T>;
|
||||
|
||||
#[derive(BotCommands, Clone)]
|
||||
#[command(rename_rule = "lowercase", description = "Auction Bot Commands")]
|
||||
|
||||
@@ -1,43 +1,57 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use anyhow::bail;
|
||||
use teloxide::{
|
||||
dispatching::dialogue::GetChatId,
|
||||
payloads::{EditMessageTextSetters as _, SendMessageSetters as _},
|
||||
prelude::Requester as _,
|
||||
types::{Chat, ChatId, InlineKeyboardMarkup, MessageId, ParseMode, User},
|
||||
types::{
|
||||
CallbackQuery, Chat, ChatId, InlineKeyboardButton, InlineKeyboardMarkup, MessageId,
|
||||
ParseMode, User,
|
||||
},
|
||||
Bot,
|
||||
};
|
||||
|
||||
use crate::HandlerResult;
|
||||
|
||||
pub struct UserHandleAndId<'s> {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct HandleAndId<'s> {
|
||||
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 {
|
||||
write!(
|
||||
f,
|
||||
"{} ({})",
|
||||
self.handle.unwrap_or("unknown"),
|
||||
self.id.unwrap_or(-1)
|
||||
)
|
||||
write!(f, "{}", self.handle.unwrap_or("unknown"))?;
|
||||
write!(f, " ({})", self.id.0)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl<'s> UserHandleAndId<'s> {
|
||||
impl<'s> HandleAndId<'s> {
|
||||
pub fn from_chat(chat: &'s Chat) -> Self {
|
||||
Self {
|
||||
handle: chat.username(),
|
||||
id: Some(chat.id.0),
|
||||
id: chat.id,
|
||||
}
|
||||
}
|
||||
pub fn from_user(user: &'s User) -> Self {
|
||||
Self {
|
||||
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 {
|
||||
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
|
||||
pub async fn send_html_message(
|
||||
bot: &Bot,
|
||||
chat_id: ChatId,
|
||||
chat: impl Into<HandleAndId<'_>>,
|
||||
text: &str,
|
||||
keyboard: Option<InlineKeyboardMarkup>,
|
||||
) -> 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 {
|
||||
message = message.reply_markup(kb);
|
||||
}
|
||||
@@ -63,13 +78,14 @@ pub async fn send_html_message(
|
||||
|
||||
pub async fn edit_html_message(
|
||||
bot: &Bot,
|
||||
chat_id: ChatId,
|
||||
chat: impl Into<HandleAndId<'_>>,
|
||||
message_id: MessageId,
|
||||
text: &str,
|
||||
keyboard: Option<InlineKeyboardMarkup>,
|
||||
) -> HandlerResult {
|
||||
let chat = chat.into();
|
||||
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);
|
||||
if let Some(kb) = keyboard {
|
||||
edit_request = edit_request.reply_markup(kb);
|
||||
@@ -77,3 +93,68 @@ pub async fn edit_html_message(
|
||||
edit_request.await?;
|
||||
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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user