Compare commits
10 Commits
3a7d0a6905
...
1d4d5c05ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d4d5c05ed | ||
|
|
869526005f | ||
|
|
79ed3a7e4c | ||
|
|
0eef18ea06 | ||
|
|
764c17af05 | ||
|
|
71fe1e60c0 | ||
|
|
4e5283b530 | ||
|
|
8745583990 | ||
|
|
af1f3271a3 | ||
|
|
5212175df5 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/target
|
||||
!.env.example
|
||||
.env
|
||||
*.db
|
||||
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -1548,12 +1548,16 @@ dependencies = [
|
||||
"chrono",
|
||||
"dotenvy",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"rstest",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"teloxide",
|
||||
"teloxide-core",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -1985,21 +1989,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rstest"
|
||||
version = "0.21.0"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afd55a67069d6e434a95161415f5beeada95a01c7b815508a82dcb0e1593682"
|
||||
checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"futures-timer",
|
||||
"futures-util",
|
||||
"rstest_macros",
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest_macros"
|
||||
version = "0.21.0"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4165dfae59a39dd41d8dec720d3cbfbc71f69744efb480a3920f5d4e0cc6798d"
|
||||
checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"glob",
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -4,21 +4,26 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
teloxide = { version = "0.17.0", features = ["macros"] }
|
||||
teloxide = { version = "0.17.0", features = ["macros", "ctrlc_handler"] }
|
||||
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"chrono",
|
||||
"macros",
|
||||
"rust_decimal",
|
||||
] }
|
||||
rust_decimal = { version = "1.33", features = ["serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
rust_decimal = { version = "1.33" }
|
||||
chrono = { version = "0.4" }
|
||||
log = "0.4"
|
||||
env_logger = "0.11.8"
|
||||
anyhow = "1.0"
|
||||
dotenvy = "0.15"
|
||||
lazy_static = "1.4"
|
||||
serde = "1.0.219"
|
||||
futures = "0.3.31"
|
||||
thiserror = "2.0.16"
|
||||
teloxide-core = "0.13.0"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.21"
|
||||
rstest = "0.26.1"
|
||||
|
||||
BIN
pawctioneer_bot_dev.db
Normal file
BIN
pawctioneer_bot_dev.db
Normal file
Binary file not shown.
@@ -1,8 +1,8 @@
|
||||
use teloxide::{prelude::*, types::Message, utils::command::BotCommands, Bot};
|
||||
|
||||
use crate::Command;
|
||||
use crate::{Command, HandlerResult};
|
||||
|
||||
pub async fn handle_help(bot: Bot, msg: Message) -> ResponseResult<()> {
|
||||
pub async fn handle_help(bot: Bot, msg: Message) -> HandlerResult {
|
||||
let help_message = format!(
|
||||
"📋 Available Commands:\n\n{}\n\n\
|
||||
📧 Support: Contact @admin for help\n\
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
pub mod help;
|
||||
pub mod mybids;
|
||||
pub mod mylistings;
|
||||
pub mod newlisting;
|
||||
pub mod my_bids;
|
||||
pub mod my_listings;
|
||||
pub mod new_listing;
|
||||
pub mod settings;
|
||||
pub mod start;
|
||||
|
||||
// Re-export all command handlers for easy access
|
||||
pub use help::handle_help;
|
||||
pub use mybids::handle_my_bids;
|
||||
pub use mylistings::handle_my_listings;
|
||||
pub use newlisting::handle_new_listing;
|
||||
pub use my_bids::handle_my_bids;
|
||||
pub use my_listings::handle_my_listings;
|
||||
pub use settings::handle_settings;
|
||||
pub use start::handle_start;
|
||||
|
||||
// Note: Text message handling is now handled by the dialogue system
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use log::info;
|
||||
use teloxide::{prelude::*, types::Message, Bot};
|
||||
|
||||
pub async fn handle_my_bids(bot: Bot, msg: Message) -> ResponseResult<()> {
|
||||
use crate::HandlerResult;
|
||||
|
||||
pub async fn handle_my_bids(bot: Bot, msg: Message) -> HandlerResult {
|
||||
let response = "🎯 My Bids (Coming Soon)\n\n\
|
||||
Here you'll be able to view:\n\
|
||||
• Your active bids\n\
|
||||
@@ -1,7 +1,9 @@
|
||||
use log::info;
|
||||
use teloxide::{prelude::*, types::Message, Bot};
|
||||
|
||||
pub async fn handle_my_listings(bot: Bot, msg: Message) -> ResponseResult<()> {
|
||||
use crate::HandlerResult;
|
||||
|
||||
pub async fn handle_my_listings(bot: Bot, msg: Message) -> HandlerResult {
|
||||
let response = "📊 My Listings and Auctions (Coming Soon)\n\n\
|
||||
Here you'll be able to view and manage:\n\
|
||||
• Your active listings and auctions\n\
|
||||
1758
src/commands/new_listing.rs
Normal file
1758
src/commands/new_listing.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
use log::info;
|
||||
use teloxide::{prelude::*, types::Message, Bot};
|
||||
|
||||
pub async fn handle_new_listing(bot: Bot, msg: Message) -> ResponseResult<()> {
|
||||
let response = "🏗️ New Listing or Auction Creation (Coming Soon)\n\n\
|
||||
This feature will allow you to create:\n\
|
||||
• Standard time-based auctions\n\
|
||||
• Multi-slot auctions\n\
|
||||
• Fixed price sales\n\
|
||||
• Blind auctions\n\n\
|
||||
Stay tuned! 🎪";
|
||||
|
||||
info!(
|
||||
"User {} ({}) attempted to create new listing or auctionc",
|
||||
msg.chat.username().unwrap_or("unknown"),
|
||||
msg.chat.id
|
||||
);
|
||||
|
||||
bot.send_message(msg.chat.id, response).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
use log::info;
|
||||
use teloxide::{prelude::*, types::Message, Bot};
|
||||
|
||||
pub async fn handle_settings(bot: Bot, msg: Message) -> ResponseResult<()> {
|
||||
use crate::HandlerResult;
|
||||
|
||||
pub async fn handle_settings(bot: Bot, msg: Message) -> HandlerResult {
|
||||
let response = "⚙️ Settings (Coming Soon)\n\n\
|
||||
Here you'll be able to configure:\n\
|
||||
• Notification preferences\n\
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use log::info;
|
||||
use teloxide::{prelude::*, types::Message, Bot};
|
||||
|
||||
pub async fn handle_start(bot: Bot, msg: Message) -> ResponseResult<()> {
|
||||
use crate::HandlerResult;
|
||||
|
||||
pub async fn handle_start(bot: Bot, msg: Message) -> HandlerResult {
|
||||
let welcome_message = "🎯 Welcome to Pawctioneer Bot! 🎯\n\n\
|
||||
This bot helps you participate in various types of auctions:\n\
|
||||
• Standard auctions with anti-sniping protection\n\
|
||||
|
||||
@@ -26,8 +26,8 @@ impl Config {
|
||||
///
|
||||
/// The configuration is automatically validated during construction.
|
||||
pub fn from_env() -> Result<Self> {
|
||||
// Load .env file if present (fails silently if not found)
|
||||
let _ = dotenvy::dotenv();
|
||||
dotenvy::dotenv()?;
|
||||
env_logger::init();
|
||||
|
||||
let telegram_token = env::var("TELOXIDE_TOKEN")
|
||||
.context("TELOXIDE_TOKEN environment variable is required")?;
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
pub mod currency_type;
|
||||
pub mod dao;
|
||||
pub mod listing_id;
|
||||
pub mod models;
|
||||
pub mod money_amount;
|
||||
pub mod telegram_user_id;
|
||||
pub mod user_id;
|
||||
pub mod types;
|
||||
|
||||
// Re-export all models for easy access
|
||||
pub use currency_type::*;
|
||||
// Re-export all modules for easy access
|
||||
pub use dao::*;
|
||||
pub use listing_id::*;
|
||||
pub use models::*;
|
||||
pub use money_amount::*;
|
||||
pub use telegram_user_id::*;
|
||||
pub use user_id::*;
|
||||
pub use types::*;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
use crate::db::money_amount::MoneyAmount;
|
||||
|
||||
/// Actual bids placed on listings
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct Bid {
|
||||
pub id: i64,
|
||||
pub listing_id: i64,
|
||||
|
||||
@@ -13,6 +13,13 @@ use super::listing_type::ListingType;
|
||||
use crate::db::{listing_id::ListingId, money_amount::MoneyAmount, user_id::UserId};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// Main listing/auction entity
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Listing {
|
||||
pub base: ListingBase,
|
||||
pub fields: ListingFields,
|
||||
}
|
||||
|
||||
/// Common fields shared by all listing types
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ListingBase {
|
||||
@@ -26,13 +33,6 @@ pub struct ListingBase {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Main listing/auction entity enum
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Listing {
|
||||
pub base: ListingBase,
|
||||
pub fields: ListingFields,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ListingFields {
|
||||
BasicAuction {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
/// Media attachments for listings
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct ListingMedia {
|
||||
pub id: i64,
|
||||
pub listing_id: i64,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Types of listings supported by the platform
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, sqlx::Type)]
|
||||
#[sqlx(type_name = "TEXT")]
|
||||
#[sqlx(rename_all = "snake_case")]
|
||||
pub enum ListingType {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
use crate::db::money_amount::MoneyAmount;
|
||||
|
||||
/// Proxy bid strategies (automatic bidding settings)
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct ProxyBid {
|
||||
pub id: i64,
|
||||
pub listing_id: i64,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
use crate::db::{TelegramUserId, UserId};
|
||||
|
||||
/// Core user information
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct User {
|
||||
pub id: UserId,
|
||||
pub telegram_id: TelegramUserId,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
/// User preferences and settings
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct UserSettings {
|
||||
pub user_id: i64,
|
||||
pub language_code: String,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||
};
|
||||
|
||||
/// Currency types supported by the platform
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CurrencyType {
|
||||
#[serde(rename = "USD")]
|
||||
USD,
|
||||
}
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
//! This newtype prevents accidentally mixing up listing IDs with other ID types
|
||||
//! while maintaining compatibility with the database layer through SQLx traits.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
/// Type-safe wrapper for listing IDs
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ListingId(i64);
|
||||
|
||||
impl ListingId {
|
||||
12
src/db/types/mod.rs
Normal file
12
src/db/types/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
pub mod currency_type;
|
||||
pub mod listing_id;
|
||||
pub mod money_amount;
|
||||
pub mod telegram_user_id;
|
||||
pub mod user_id;
|
||||
|
||||
// Re-export all types for easy access
|
||||
pub use currency_type::*;
|
||||
pub use listing_id::*;
|
||||
pub use money_amount::*;
|
||||
pub use telegram_user_id::*;
|
||||
pub use user_id::*;
|
||||
@@ -45,6 +45,12 @@ impl MoneyAmount {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MoneyAmount {
|
||||
fn default() -> Self {
|
||||
Self::zero()
|
||||
}
|
||||
}
|
||||
|
||||
// Allow easy conversion from Decimal
|
||||
impl From<Decimal> for MoneyAmount {
|
||||
fn from(decimal: Decimal) -> Self {
|
||||
@@ -4,14 +4,13 @@
|
||||
//! This newtype prevents accidentally mixing up user IDs with other ID types
|
||||
//! while maintaining compatibility with the database layer through SQLx traits.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
/// Type-safe wrapper for user IDs
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct TelegramUserId(teloxide::types::UserId);
|
||||
|
||||
impl TelegramUserId {
|
||||
@@ -3,14 +3,13 @@
|
||||
//! This newtype prevents accidentally mixing up user IDs with other ID types
|
||||
//! while maintaining compatibility with the database layer through SQLx traits.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
/// Type-safe wrapper for user IDs
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct UserId(i64);
|
||||
|
||||
impl UserId {
|
||||
48
src/main.rs
48
src/main.rs
@@ -1,16 +1,23 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use teloxide::{prelude::*, utils::command::BotCommands};
|
||||
|
||||
mod commands;
|
||||
mod config;
|
||||
mod db;
|
||||
mod message_utils;
|
||||
mod sqlite_storage;
|
||||
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use teloxide::dispatching::dialogue::serializer::Json;
|
||||
use teloxide::{prelude::*, utils::command::BotCommands};
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
|
||||
use commands::*;
|
||||
use config::Config;
|
||||
|
||||
use crate::commands::new_listing::new_listing_handler;
|
||||
use crate::sqlite_storage::SqliteStorage;
|
||||
|
||||
pub type HandlerResult = anyhow::Result<()>;
|
||||
|
||||
#[derive(BotCommands, Clone)]
|
||||
#[command(rename_rule = "lowercase", description = "Auction Bot Commands")]
|
||||
pub enum Command {
|
||||
@@ -28,13 +35,8 @@ pub enum Command {
|
||||
Settings,
|
||||
}
|
||||
|
||||
// No longer needed - dptree will dispatch directly!
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize logging
|
||||
env_logger::init();
|
||||
|
||||
// Load and validate configuration from environment/.env file
|
||||
let config = Config::from_env()?;
|
||||
|
||||
@@ -42,24 +44,26 @@ async fn main() -> Result<()> {
|
||||
let db_pool = config.create_database_pool().await?;
|
||||
|
||||
info!("Starting Pawctioneer Bot...");
|
||||
|
||||
let bot = Bot::new(&config.telegram_token);
|
||||
|
||||
// Create dispatcher with direct command routing
|
||||
let dialog_storage = SqliteStorage::new(db_pool.clone(), Json).await?;
|
||||
|
||||
// Create dispatcher with dialogue system
|
||||
Dispatcher::builder(
|
||||
bot,
|
||||
Update::filter_message().branch(
|
||||
dptree::entry()
|
||||
.filter_command::<Command>()
|
||||
.branch(dptree::case![Command::Start].endpoint(handle_start))
|
||||
.branch(dptree::case![Command::Help].endpoint(handle_help))
|
||||
.branch(dptree::case![Command::NewListing].endpoint(handle_new_listing))
|
||||
.branch(dptree::case![Command::MyListings].endpoint(handle_my_listings))
|
||||
.branch(dptree::case![Command::MyBids].endpoint(handle_my_bids))
|
||||
.branch(dptree::case![Command::Settings].endpoint(handle_settings)),
|
||||
dptree::entry().branch(new_listing_handler()).branch(
|
||||
Update::filter_message().branch(
|
||||
dptree::entry()
|
||||
.filter_command::<Command>()
|
||||
.branch(dptree::case![Command::Start].endpoint(handle_start))
|
||||
.branch(dptree::case![Command::Help].endpoint(handle_help))
|
||||
.branch(dptree::case![Command::MyListings].endpoint(handle_my_listings))
|
||||
.branch(dptree::case![Command::MyBids].endpoint(handle_my_bids))
|
||||
.branch(dptree::case![Command::Settings].endpoint(handle_settings)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.dependencies(dptree::deps![db_pool])
|
||||
.dependencies(dptree::deps![db_pool, dialog_storage])
|
||||
.enable_ctrlc_handler()
|
||||
.build()
|
||||
.dispatch()
|
||||
|
||||
79
src/message_utils.rs
Normal file
79
src/message_utils.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use teloxide::{
|
||||
payloads::{EditMessageTextSetters as _, SendMessageSetters as _},
|
||||
prelude::Requester as _,
|
||||
types::{Chat, ChatId, InlineKeyboardMarkup, MessageId, ParseMode, User},
|
||||
Bot,
|
||||
};
|
||||
|
||||
use crate::HandlerResult;
|
||||
|
||||
pub struct UserHandleAndId<'s> {
|
||||
pub handle: Option<&'s str>,
|
||||
pub id: Option<i64>,
|
||||
}
|
||||
impl<'s> Display for UserHandleAndId<'s> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} ({})",
|
||||
self.handle.unwrap_or("unknown"),
|
||||
self.id.unwrap_or(-1)
|
||||
)
|
||||
}
|
||||
}
|
||||
impl<'s> UserHandleAndId<'s> {
|
||||
pub fn from_chat(chat: &'s Chat) -> Self {
|
||||
Self {
|
||||
handle: chat.username(),
|
||||
id: Some(chat.id.0),
|
||||
}
|
||||
}
|
||||
pub fn from_user(user: &'s User) -> Self {
|
||||
Self {
|
||||
handle: user.username.as_deref(),
|
||||
id: Some(user.id.0 as i64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_cancel_or_no(text: &str) -> bool {
|
||||
is_cancel(text) || text.eq_ignore_ascii_case("no")
|
||||
}
|
||||
|
||||
pub fn is_cancel(text: &str) -> bool {
|
||||
text.eq_ignore_ascii_case("/cancel")
|
||||
}
|
||||
|
||||
// Unified HTML message sending utility
|
||||
pub async fn send_html_message(
|
||||
bot: &Bot,
|
||||
chat_id: ChatId,
|
||||
text: &str,
|
||||
keyboard: Option<InlineKeyboardMarkup>,
|
||||
) -> HandlerResult {
|
||||
let mut message = bot.send_message(chat_id, text).parse_mode(ParseMode::Html);
|
||||
if let Some(kb) = keyboard {
|
||||
message = message.reply_markup(kb);
|
||||
}
|
||||
message.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn edit_html_message(
|
||||
bot: &Bot,
|
||||
chat_id: ChatId,
|
||||
message_id: MessageId,
|
||||
text: &str,
|
||||
keyboard: Option<InlineKeyboardMarkup>,
|
||||
) -> HandlerResult {
|
||||
let mut edit_request = bot
|
||||
.edit_message_text(chat_id, message_id, text)
|
||||
.parse_mode(ParseMode::Html);
|
||||
if let Some(kb) = keyboard {
|
||||
edit_request = edit_request.reply_markup(kb);
|
||||
}
|
||||
edit_request.await?;
|
||||
Ok(())
|
||||
}
|
||||
150
src/sqlite_storage.rs
Normal file
150
src/sqlite_storage.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use futures::future::BoxFuture;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use sqlx::{sqlite::SqlitePool, Executor};
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
fmt::{Debug, Display},
|
||||
str,
|
||||
sync::Arc,
|
||||
};
|
||||
use teloxide::dispatching::dialogue::{serializer::Serializer, Storage};
|
||||
use teloxide_core::types::ChatId;
|
||||
use thiserror::Error;
|
||||
|
||||
/// A persistent dialogue storage based on [SQLite](https://www.sqlite.org/).
|
||||
pub struct SqliteStorage<S> {
|
||||
pool: SqlitePool,
|
||||
serializer: S,
|
||||
}
|
||||
|
||||
/// An error returned from [`SqliteStorage`].
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SqliteStorageError<SE>
|
||||
where
|
||||
SE: Debug + Display,
|
||||
{
|
||||
#[error("dialogue serialization error: {0}")]
|
||||
SerdeError(SE),
|
||||
|
||||
#[error("sqlite error: {0}")]
|
||||
SqliteError(#[from] sqlx::Error),
|
||||
|
||||
/// Returned from [`SqliteStorage::remove_dialogue`].
|
||||
#[error("row not found")]
|
||||
DialogueNotFound,
|
||||
}
|
||||
|
||||
impl<S> SqliteStorage<S> {
|
||||
pub async fn new(
|
||||
pool: SqlitePool,
|
||||
serializer: S,
|
||||
) -> Result<Arc<Self>, SqliteStorageError<Infallible>> {
|
||||
sqlx::query(
|
||||
"
|
||||
CREATE TABLE IF NOT EXISTS teloxide_dialogues (
|
||||
chat_id BIGINT PRIMARY KEY,
|
||||
dialogue BLOB NOT NULL
|
||||
);
|
||||
",
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
Ok(Arc::new(Self { pool, serializer }))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, D> Storage<D> for SqliteStorage<S>
|
||||
where
|
||||
S: Send + Sync + Serializer<D> + 'static,
|
||||
D: Send + Serialize + DeserializeOwned + 'static,
|
||||
<S as Serializer<D>>::Error: Debug + Display,
|
||||
{
|
||||
type Error = SqliteStorageError<<S as Serializer<D>>::Error>;
|
||||
|
||||
/// Returns [`sqlx::Error::RowNotFound`] if a dialogue does not exist.
|
||||
fn remove_dialogue(
|
||||
self: Arc<Self>,
|
||||
ChatId(chat_id): ChatId,
|
||||
) -> BoxFuture<'static, Result<(), Self::Error>> {
|
||||
Box::pin(async move {
|
||||
let deleted_rows_count =
|
||||
sqlx::query("DELETE FROM teloxide_dialogues WHERE chat_id = ?")
|
||||
.bind(chat_id)
|
||||
.execute(&self.pool)
|
||||
.await?
|
||||
.rows_affected();
|
||||
|
||||
if deleted_rows_count == 0 {
|
||||
return Err(SqliteStorageError::DialogueNotFound);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn update_dialogue(
|
||||
self: Arc<Self>,
|
||||
ChatId(chat_id): ChatId,
|
||||
dialogue: D,
|
||||
) -> BoxFuture<'static, Result<(), Self::Error>> {
|
||||
Box::pin(async move {
|
||||
let d = self
|
||||
.serializer
|
||||
.serialize(&dialogue)
|
||||
.map_err(SqliteStorageError::SerdeError)?;
|
||||
|
||||
self.pool
|
||||
.acquire()
|
||||
.await?
|
||||
.execute(
|
||||
sqlx::query(
|
||||
"
|
||||
INSERT INTO teloxide_dialogues VALUES (?, ?)
|
||||
ON CONFLICT(chat_id) DO UPDATE SET dialogue=excluded.dialogue
|
||||
",
|
||||
)
|
||||
.bind(chat_id)
|
||||
.bind(d),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn get_dialogue(
|
||||
self: Arc<Self>,
|
||||
chat_id: ChatId,
|
||||
) -> BoxFuture<'static, Result<Option<D>, Self::Error>> {
|
||||
Box::pin(async move {
|
||||
get_dialogue(&self.pool, chat_id)
|
||||
.await?
|
||||
.map(|d| {
|
||||
self.serializer
|
||||
.deserialize(&d)
|
||||
.map_err(SqliteStorageError::SerdeError)
|
||||
})
|
||||
.transpose()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_dialogue(
|
||||
pool: &SqlitePool,
|
||||
ChatId(chat_id): ChatId,
|
||||
) -> Result<Option<Vec<u8>>, sqlx::Error> {
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct DialogueDbRow {
|
||||
dialogue: Vec<u8>,
|
||||
}
|
||||
|
||||
let bytes = sqlx::query_as::<_, DialogueDbRow>(
|
||||
"SELECT dialogue FROM teloxide_dialogues WHERE chat_id = ?",
|
||||
)
|
||||
.bind(chat_id)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
.map(|r| r.dialogue);
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
Reference in New Issue
Block a user