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,
|
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
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 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")]
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user