diff --git a/src/commands/my_listings.rs b/src/commands/my_listings.rs index 558df59..7292669 100644 --- a/src/commands/my_listings.rs +++ b/src/commands/my_listings.rs @@ -1,5 +1,6 @@ use crate::{ case, + commands::new_listing::{enter_edit_listing_draft, ListingDraft}, db::{listing::PersistedListing, user::PersistedUser, ListingDAO, ListingDbId, UserDAO}, keyboard_buttons, message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget}, @@ -18,7 +19,6 @@ use teloxide::{ pub enum MyListingsState { ViewingListings, ManagingListing(ListingDbId), - EditingListing(ListingDbId), } impl From for DialogueRootState { fn from(state: MyListingsState) -> Self { @@ -122,21 +122,13 @@ async fn show_listings_for_user( )]); } - let mut response = format!( + let response = format!( "📋 My Listings\n\n\ - You have {}.\n\n", + You have {}.\n\n\ + Select a listing to view details", pluralize_with_count(listings.len(), "listing", "listings") ); - // Add each listing with its ID and title - for listing in &listings { - response.push_str(&format!( - "• ID {}: {}\n", - listing.persisted.id, listing.base.title - )); - } - - response.push_str("\nTap a listing ID below to view details:"); send_message(&bot, target, response, Some(keyboard)).await?; Ok(()) } @@ -169,15 +161,13 @@ async fn show_listing_details( let response = format!( "🔍 Viewing Listing Details\n\n\ Title: {}\n\ - Description: {}\n\ - ID: {}", + Description: {}\n", listing.base.title, listing .base .description .as_deref() .unwrap_or("No description"), - listing.persisted.id ); send_message( @@ -207,9 +197,8 @@ async fn handle_managing_listing_callback( ManageListingButtons::Edit => { let (_, listing) = get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?; - dialogue - .update(MyListingsState::EditingListing(listing.persisted.id)) - .await?; + let draft = ListingDraft::from_persisted(listing); + enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?; } ManageListingButtons::Delete => { ListingDAO::delete_listing(&db_pool, listing_id).await?; diff --git a/src/commands/new_listing/keyboard.rs b/src/commands/new_listing/keyboard.rs index cdcf556..ad61b8b 100644 --- a/src/commands/new_listing/keyboard.rs +++ b/src/commands/new_listing/keyboard.rs @@ -20,9 +20,11 @@ keyboard_buttons! { keyboard_buttons! { pub enum ConfirmationKeyboardButtons { + Save("✅ Save", "confirm_save"), Create("✅ Create", "confirm_create"), Edit("✏️ Edit", "confirm_edit"), Discard("🗑️ Discard", "confirm_discard"), + Cancel("❌ Cancel", "confirm_cancel"), } } diff --git a/src/commands/new_listing/mod.rs b/src/commands/new_listing/mod.rs index 6caba9f..70fa2ac 100644 --- a/src/commands/new_listing/mod.rs +++ b/src/commands/new_listing/mod.rs @@ -61,7 +61,7 @@ async fn handle_new_listing_command( dialogue .update(NewListingState::AwaitingDraftField { field: ListingField::Title, - draft: ListingDraft::draft_for_seller(user.persisted.id), + draft: ListingDraft::new_for_seller(user.persisted.id), }) .await?; @@ -75,7 +75,7 @@ async fn handle_new_listing_command( } async fn handle_awaiting_draft_field_input( - bot: &Bot, + bot: Bot, dialogue: RootDialogue, (field, draft): (ListingField, ListingDraft), msg: Message, @@ -90,18 +90,18 @@ async fn handle_awaiting_draft_field_input( ); if is_cancel(text) { - return cancel_wizard(bot, dialogue, chat).await; + return cancel_wizard(&bot, dialogue, chat).await; } match field { - ListingField::Title => handle_title_input(bot, chat, text, dialogue, draft).await, + ListingField::Title => handle_title_input(&bot, chat, text, dialogue, draft).await, ListingField::Description => { - handle_description_input(bot, chat, text, dialogue, draft).await + handle_description_input(&bot, chat, text, dialogue, draft).await } - ListingField::Price => handle_price_input(bot, chat, text, dialogue, draft).await, - ListingField::Slots => handle_slots_input(bot, chat, text, dialogue, draft).await, - ListingField::StartTime => handle_start_time_input(bot, chat, text, dialogue, draft).await, - ListingField::Duration => handle_duration_input(bot, chat, text, dialogue, draft).await, + ListingField::Price => handle_price_input(&bot, chat, text, dialogue, draft).await, + ListingField::Slots => handle_slots_input(&bot, chat, text, dialogue, draft).await, + ListingField::StartTime => handle_start_time_input(&bot, chat, text, dialogue, draft).await, + ListingField::Duration => handle_duration_input(&bot, chat, text, dialogue, draft).await, } } @@ -196,7 +196,7 @@ async fn handle_description_callback( } async fn handle_awaiting_draft_field_callback( - bot: &Bot, + bot: Bot, dialogue: RootDialogue, (field, draft): (ListingField, ListingDraft), callback_query: CallbackQuery, @@ -206,7 +206,7 @@ async fn handle_awaiting_draft_field_callback( let target = (from, message_id); if data == "cancel" { - return cancel_wizard(bot, dialogue, target).await; + return cancel_wizard(&bot, dialogue, target).await; } match field { @@ -216,7 +216,7 @@ async fn handle_awaiting_draft_field_callback( Ok(()) } ListingField::Description => { - handle_description_callback(bot, dialogue, draft, data.as_str(), target).await + handle_description_callback(&bot, dialogue, draft, data.as_str(), target).await } ListingField::Price => { error!("Unknown callback data: {data}"); @@ -224,13 +224,13 @@ async fn handle_awaiting_draft_field_callback( Ok(()) } ListingField::Slots => { - handle_slots_callback(bot, dialogue, draft, data.as_str(), target).await + handle_slots_callback(&bot, dialogue, draft, data.as_str(), target).await } ListingField::StartTime => { - handle_start_time_callback(bot, dialogue, draft, data.as_str(), target).await + handle_start_time_callback(&bot, dialogue, draft, data.as_str(), target).await } ListingField::Duration => { - handle_duration_callback(bot, dialogue, draft, data.as_str(), target).await + handle_duration_callback(&bot, dialogue, draft, data.as_str(), target).await } } } @@ -331,39 +331,44 @@ async fn handle_viewing_draft_callback( callback_query: CallbackQuery, ) -> HandlerResult { let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?; - let target = (from, message_id); + + // Ensure the user exists before saving the listing + UserDAO::find_or_create_by_telegram_user(&db_pool, from.clone()) + .await + .inspect_err(|e| { + error!("Error finding or creating user: {e}"); + })?; + + let target = (from.clone(), message_id); let button = ConfirmationKeyboardButtons::try_from(data.as_str()) .map_err(|_| anyhow::anyhow!("Unknown ConfirmationKeyboardButtons data: {}", data))?; match button { - ConfirmationKeyboardButtons::Create => { + ConfirmationKeyboardButtons::Create | ConfirmationKeyboardButtons::Save => { info!("User {target:?} confirmed listing creation"); + save_listing(db_pool, bot, target, draft).await?; + dialogue.exit().await?; + } + ConfirmationKeyboardButtons::Cancel => { + info!("User {target:?} cancelled listing update"); + let response = "🗑️ Changes Discarded\n\n\ + Your changes have been discarded and not saved."; + send_message(&bot, target, &response, None).await?; dialogue.exit().await?; - save_listing(db_pool, bot, dialogue, target, draft).await?; } ConfirmationKeyboardButtons::Discard => { info!("User {target:?} discarded listing creation"); - // Exit dialogue and send cancellation message - dialogue.exit().await?; - let response = "🗑️ Listing Discarded\n\n\ Your listing has been discarded and not created.\n\ You can start a new listing anytime with /newlisting."; - send_message(&bot, target, &response, None).await?; + dialogue.exit().await?; } ConfirmationKeyboardButtons::Edit => { info!("User {target:?} chose to edit listing"); - - // Delete the old message and show the edit screen - show_edit_screen(&bot, target, &draft, None).await?; - - // Go to editing state to allow user to modify specific fields - dialogue - .update(NewListingState::EditingDraft(draft)) - .await?; + enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?; } } @@ -583,7 +588,12 @@ async fn display_listing_summary( response_lines.push(flash_message.to_string()); } - response_lines.push("📋 Listing Summary".to_string()); + let unsaved_changes = if draft.has_changes { + "Unsaved changes" + } else { + "" + }; + response_lines.push(format!("📋 Listing Summary {unsaved_changes}")); response_lines.push("".to_string()); response_lines.push(format!("Title: {}", draft.base.title)); response_lines.push(format!( @@ -608,8 +618,14 @@ async fn display_listing_summary( response_lines.push(format!("Duration: {}", fields.end_delay)); } ListingDraftPersisted::Persisted(fields) => { - response_lines.push(format!("Starts on: {}", fields.start_at)); - response_lines.push(format!("Ends on: {}", fields.end_at)); + response_lines.push(format!( + "Starts on: {}", + format_datetime(fields.start_at) + )); + response_lines.push(format!( + "Ends on: {}", + format_datetime(fields.end_at) + )); } } @@ -621,20 +637,25 @@ async fn display_listing_summary( Ok(()) } -async fn show_edit_screen( +pub async fn enter_edit_listing_draft( bot: &Bot, target: impl Into, - draft: &ListingDraft, + draft: ListingDraft, + dialogue: RootDialogue, flash_message: Option<&str>, ) -> HandlerResult { display_listing_summary( bot, target, - draft, + &draft, Some(FieldSelectionKeyboardButtons::to_keyboard()), flash_message, ) .await?; + dialogue + .update(NewListingState::EditingDraft(draft)) + .await?; + Ok(()) } @@ -643,19 +664,25 @@ async fn show_confirmation_screen( target: impl Into, draft: &ListingDraft, ) -> HandlerResult { - display_listing_summary( - bot, - target, - draft, - Some(ConfirmationKeyboardButtons::to_keyboard()), - None, - ) - .await?; + let keyboard = match draft.persisted { + ListingDraftPersisted::New(_) => InlineKeyboardMarkup::default().append_row([ + ConfirmationKeyboardButtons::Create.to_button(), + ConfirmationKeyboardButtons::Edit.to_button(), + ConfirmationKeyboardButtons::Discard.to_button(), + ]), + ListingDraftPersisted::Persisted(_) => InlineKeyboardMarkup::default().append_row([ + ConfirmationKeyboardButtons::Save.to_button(), + ConfirmationKeyboardButtons::Edit.to_button(), + ConfirmationKeyboardButtons::Cancel.to_button(), + ]), + }; + + display_listing_summary(bot, target, draft, Some(keyboard), None).await?; Ok(()) } async fn handle_editing_field_input( - bot: &Bot, + bot: Bot, dialogue: RootDialogue, (field, draft): (ListingField, ListingDraft), msg: Message, @@ -667,22 +694,22 @@ async fn handle_editing_field_input( match field { ListingField::Title => { - handle_edit_title(bot, dialogue, draft, text, chat).await?; + handle_edit_title(&bot, dialogue, draft, text, chat).await?; } ListingField::Description => { - handle_edit_description(bot, dialogue, draft, text, chat).await?; + handle_edit_description(&bot, dialogue, draft, text, chat).await?; } ListingField::Price => { - handle_edit_price(bot, dialogue, draft, text, chat).await?; + handle_edit_price(&bot, dialogue, draft, text, chat).await?; } ListingField::Slots => { - handle_edit_slots(bot, dialogue, draft, text, chat).await?; + handle_edit_slots(&bot, dialogue, draft, text, chat).await?; } ListingField::StartTime => { - handle_edit_start_time(bot, dialogue, draft, text, chat).await?; + handle_edit_start_time(&bot, dialogue, draft, text, chat).await?; } ListingField::Duration => { - handle_edit_duration(bot, dialogue, draft, text, chat).await?; + handle_edit_duration(&bot, dialogue, draft, text, chat).await?; } } @@ -690,7 +717,7 @@ async fn handle_editing_field_input( } async fn handle_editing_draft_callback( - bot: &Bot, + bot: Bot, draft: ListingDraft, dialogue: RootDialogue, callback_query: CallbackQuery, @@ -759,7 +786,7 @@ async fn handle_editing_draft_callback( create_back_button_keyboard_with(DurationKeyboardButtons::to_keyboard()), ), FieldSelectionKeyboardButtons::Done => { - show_confirmation_screen(bot, target, &draft).await?; + show_confirmation_screen(&bot, target, &draft).await?; dialogue .update(DialogueRootState::NewListing( NewListingState::ViewingDraft(draft), @@ -790,13 +817,12 @@ async fn handle_editing_draft_callback( async fn save_listing( db_pool: SqlitePool, bot: Bot, - dialogue: RootDialogue, target: impl Into, draft: ListingDraft, ) -> HandlerResult { - let listing: PersistedListing = match draft.persisted { + let (listing, success_message) = match draft.persisted { ListingDraftPersisted::New(fields) => { - ListingDAO::insert_listing( + let listing = ListingDAO::insert_listing( &db_pool, NewListing { persisted: fields, @@ -804,10 +830,11 @@ async fn save_listing( fields: draft.fields, }, ) - .await? + .await?; + (listing, "Listing created!") } ListingDraftPersisted::Persisted(fields) => { - ListingDAO::update_listing( + let listing = ListingDAO::update_listing( &db_pool, PersistedListing { persisted: fields, @@ -815,21 +842,13 @@ async fn save_listing( fields: draft.fields, }, ) - .await? + .await?; + (listing, "Listing updated!") } }; - let response = format!( - "✅ Listing Created Successfully!\n\n\ - Listing ID: {}\n\ - Title: {}\n\ - Your fixed price listing is now live! 🎉", - listing.persisted.id, listing.base.title - ); - - dialogue.exit().await?; + let response = format!("✅ {}: {}", success_message, listing.base.title); send_message(&bot, target, response, None).await?; - Ok(()) } @@ -857,14 +876,8 @@ async fn handle_edit_title( return Ok(()); } }; - - // Go back to editing listing state - show_edit_screen(bot, target, &draft, Some("✅ Title updated!")).await?; - dialogue - .update(DialogueRootState::NewListing( - NewListingState::EditingDraft(draft), - )) - .await?; + draft.has_changes = true; + enter_edit_listing_draft(bot, target, draft, dialogue, Some("✅ Title updated!")).await?; Ok(()) } @@ -885,14 +898,16 @@ async fn handle_edit_description( return Ok(()); } }; + draft.has_changes = true; - // Go back to editing listing state - show_edit_screen(bot, target, &draft, Some("✅ Description updated!")).await?; - dialogue - .update(DialogueRootState::NewListing( - NewListingState::EditingDraft(draft), - )) - .await?; + enter_edit_listing_draft( + bot, + target, + draft, + dialogue, + Some("✅ Description updated!"), + ) + .await?; Ok(()) } @@ -919,15 +934,9 @@ async fn handle_edit_price( } }; - // Go back to editing listing state - show_edit_screen(bot, target, &draft, Some("✅ Price updated!")).await?; - - dialogue - .update(DialogueRootState::NewListing( - NewListingState::EditingDraft(draft), - )) - .await?; + draft.has_changes = true; + enter_edit_listing_draft(bot, target, draft, dialogue, Some("✅ Price updated!")).await?; Ok(()) } @@ -956,13 +965,9 @@ async fn handle_edit_slots( } }; - show_edit_screen(bot, target, &draft, Some("✅ Slots updated!")).await?; - dialogue - .update(DialogueRootState::NewListing( - NewListingState::EditingDraft(draft), - )) - .await?; + draft.has_changes = true; + enter_edit_listing_draft(bot, target, draft, dialogue, Some("✅ Slots updated!")).await?; Ok(()) } @@ -989,13 +994,9 @@ async fn handle_edit_start_time( } }; - // Go back to editing listing state - show_edit_screen(bot, target, &draft, Some("✅ Start time updated!")).await?; - dialogue - .update(DialogueRootState::NewListing( - NewListingState::EditingDraft(draft), - )) - .await?; + draft.has_changes = true; + + enter_edit_listing_draft(bot, target, draft, dialogue, Some("✅ Start time updated!")).await?; Ok(()) } @@ -1021,18 +1022,14 @@ async fn handle_edit_duration( return Ok(()); } }; + draft.has_changes = true; - show_edit_screen(bot, target, &draft, Some("✅ Duration updated!")).await?; - dialogue - .update(DialogueRootState::NewListing( - NewListingState::EditingDraft(draft), - )) - .await?; + enter_edit_listing_draft(bot, target, draft, dialogue, Some("✅ Duration updated!")).await?; Ok(()) } async fn handle_editing_draft_field_callback( - bot: &Bot, + bot: Bot, dialogue: RootDialogue, (field, draft): (ListingField, ListingDraft), callback_query: CallbackQuery, @@ -1041,33 +1038,28 @@ async fn handle_editing_draft_field_callback( let target = (from, message_id); info!("User {:?} editing field: {:?} -> {}", target, field, &data); if data == "edit_back" { - show_edit_screen(bot, target, &draft, None).await?; - dialogue - .update(DialogueRootState::NewListing( - NewListingState::EditingDraft(draft), - )) - .await?; + enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?; return Ok(()); } match field { ListingField::Title => { - handle_edit_title(bot, dialogue, draft, data.as_str(), target).await?; + handle_edit_title(&bot, dialogue, draft, data.as_str(), target).await?; } ListingField::Description => { - handle_edit_description(bot, dialogue, draft, data.as_str(), target).await?; + handle_edit_description(&bot, dialogue, draft, data.as_str(), target).await?; } ListingField::Price => { - handle_edit_price(bot, dialogue, draft, data.as_str(), target).await?; + handle_edit_price(&bot, dialogue, draft, data.as_str(), target).await?; } ListingField::Slots => { - handle_edit_slots(bot, dialogue, draft, data.as_str(), target).await?; + handle_edit_slots(&bot, dialogue, draft, data.as_str(), target).await?; } ListingField::StartTime => { - handle_edit_start_time(bot, dialogue, draft, data.as_str(), target).await?; + handle_edit_start_time(&bot, dialogue, draft, data.as_str(), target).await?; } ListingField::Duration => { - handle_edit_duration(bot, dialogue, draft, data.as_str(), target).await?; + handle_edit_duration(&bot, dialogue, draft, data.as_str(), target).await?; } }; diff --git a/src/commands/new_listing/types.rs b/src/commands/new_listing/types.rs index a1729e4..6efe367 100644 --- a/src/commands/new_listing/types.rs +++ b/src/commands/new_listing/types.rs @@ -1,6 +1,8 @@ use crate::{ db::{ - listing::{ListingBase, ListingFields, NewListingFields, PersistedListingFields}, + listing::{ + ListingBase, ListingFields, NewListingFields, PersistedListing, PersistedListingFields, + }, MoneyAmount, UserDbId, }, DialogueRootState, @@ -9,14 +11,16 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct ListingDraft { + pub has_changes: bool, pub persisted: ListingDraftPersisted, pub base: ListingBase, pub fields: ListingFields, } impl ListingDraft { - pub fn draft_for_seller(seller_id: UserDbId) -> Self { + pub fn new_for_seller(seller_id: UserDbId) -> Self { Self { + has_changes: false, persisted: ListingDraftPersisted::New(NewListingFields::default()), base: ListingBase { seller_id, @@ -29,6 +33,15 @@ impl ListingDraft { }, } } + + pub fn from_persisted(listing: PersistedListing) -> Self { + Self { + has_changes: false, + persisted: ListingDraftPersisted::Persisted(listing.persisted), + base: listing.base, + fields: listing.fields, + } + } } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] diff --git a/src/db/dao/user_dao.rs b/src/db/dao/user_dao.rs index 6ea7a28..6a8cfa1 100644 --- a/src/db/dao/user_dao.rs +++ b/src/db/dao/user_dao.rs @@ -89,27 +89,34 @@ impl UserDAO { pool: &SqlitePool, user: teloxide::types::User, ) -> Result { - let mut tx = pool.begin().await?; - let telegram_id = TelegramUserDbId::from(user.id); + let binds = BindFields::default() + .push("telegram_id", &TelegramUserDbId::from(user.id)) + .push("username", &user.username) + .push("first_name", &user.first_name) + .push("last_name", &user.last_name); - let user = sqlx::query_as( + let query_str = format!( r#" - INSERT INTO users (telegram_id, username, first_name, last_name) - VALUES (?, ?, ?, ?) + INSERT INTO users ({}) + VALUES ({}) ON CONFLICT (telegram_id) DO UPDATE SET username = EXCLUDED.username, first_name = EXCLUDED.first_name, last_name = EXCLUDED.last_name - RETURNING id, telegram_id, username, first_name, last_name, is_banned, created_at, updated_at + RETURNING {} "#, - ) - .bind(telegram_id) - .bind(user.username) - .bind(user.first_name) - .bind(user.last_name) - .fetch_one(&mut *tx) - .await?; + binds.bind_names().join(", "), + binds.bind_placeholders().join(", "), + USER_RETURN_FIELDS.join(", ") + ); + let row = binds + .bind_to_query(sqlx::query(&query_str)) + .fetch_one(pool) + .await?; + + let user = FromRow::from_row(&row)?; + log::info!("load user from db: {:?}", user); Ok(user) } @@ -366,4 +373,145 @@ mod tests { assert!(not_found_by_telegram.is_none()); } + + mod upsert_tests { + use super::*; + use rstest::rstest; + + #[rstest] + #[case(None, None, None)] + #[case(Some("new_user"), None, None)] + #[case(None, Some("New First"), None)] + #[case(None, None, Some("New Last"))] + #[case(Some(""), None, Some(""))] + #[tokio::test] + async fn test_upsert_updates_fields( + #[case] username: Option<&str>, + #[case] first_name: Option<&str>, + #[case] last_name: Option<&str>, + ) { + let pool = create_test_pool().await; + let user_id = UserId(12345); + + let initial = teloxide::types::User { + id: user_id, + is_bot: false, + first_name: "First".into(), + last_name: Some("Last".into()), + username: Some("user".into()), + language_code: None, + is_premium: false, + added_to_attachment_menu: false, + }; + + let created = UserDAO::find_or_create_by_telegram_user(&pool, initial) + .await + .unwrap(); + + let mut updated = teloxide::types::User { + id: user_id, + is_bot: false, + first_name: "First".into(), + last_name: Some("Last".into()), + username: Some("user".into()), + language_code: None, + is_premium: false, + added_to_attachment_menu: false, + }; + if let Some(u) = username { + updated.username = if u.is_empty() { None } else { Some(u.into()) }; + } + if let Some(f) = first_name { + updated.first_name = f.into(); + } + if let Some(l) = last_name { + updated.last_name = if l.is_empty() { None } else { Some(l.into()) }; + } + + let result = UserDAO::find_or_create_by_telegram_user(&pool, updated.clone()) + .await + .unwrap(); + + assert_eq!(created.persisted.id, result.persisted.id); + assert_eq!(result.username, updated.username); + assert_eq!(result.first_name, updated.first_name); + assert_eq!(result.last_name, updated.last_name); + } + + #[tokio::test] + async fn test_multiple_users_separate() { + let pool = create_test_pool().await; + + let user1 = teloxide::types::User { + id: UserId(111), + is_bot: false, + first_name: "One".into(), + last_name: None, + username: Some("one".into()), + language_code: None, + is_premium: false, + added_to_attachment_menu: false, + }; + let user2 = teloxide::types::User { + id: UserId(222), + is_bot: false, + first_name: "Two".into(), + last_name: Some("Last".into()), + username: None, + language_code: None, + is_premium: false, + added_to_attachment_menu: false, + }; + + let p1 = UserDAO::find_or_create_by_telegram_user(&pool, user1) + .await + .unwrap(); + let p2 = UserDAO::find_or_create_by_telegram_user(&pool, user2) + .await + .unwrap(); + + assert_ne!(p1.persisted.id, p2.persisted.id); + assert_eq!(p1.telegram_id, UserId(111).into()); + assert_eq!(p2.telegram_id, UserId(222).into()); + } + + #[tokio::test] + async fn test_upsert_preserves_id_and_timestamps() { + let pool = create_test_pool().await; + + let user = teloxide::types::User { + id: UserId(333), + is_bot: false, + first_name: "Original".into(), + last_name: None, + username: Some("orig".into()), + language_code: None, + is_premium: false, + added_to_attachment_menu: false, + }; + + let created = UserDAO::find_or_create_by_telegram_user(&pool, user) + .await + .unwrap(); + + let updated_user = teloxide::types::User { + id: UserId(333), + is_bot: false, + first_name: "Original".into(), + last_name: None, + username: Some("updated".into()), + language_code: None, + is_premium: false, + added_to_attachment_menu: false, + }; + + let updated = UserDAO::find_or_create_by_telegram_user(&pool, updated_user) + .await + .unwrap(); + + assert_eq!(created.persisted.id, updated.persisted.id); + assert_eq!(created.persisted.created_at, updated.persisted.created_at); + assert_eq!(updated.username, Some("updated".to_string())); + } + } } diff --git a/src/db/models/listing.rs b/src/db/models/listing.rs index c9dd770..019877f 100644 --- a/src/db/models/listing.rs +++ b/src/db/models/listing.rs @@ -89,7 +89,6 @@ pub enum ListingFields { #[cfg(test)] mod tests { use super::*; - use crate::db::ListingDbId; use crate::db::{ListingDAO, TelegramUserDbId}; use rstest::rstest; use sqlx::SqlitePool; diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 3f185b4..aa87e1b 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -20,6 +20,7 @@ pub struct User { } #[derive(Debug, Clone)] +#[allow(unused)] pub struct PersistedUserFields { pub id: UserDbId, pub created_at: DateTime, diff --git a/src/keyboard_utils.rs b/src/keyboard_utils.rs index d8d5b24..05940a3 100644 --- a/src/keyboard_utils.rs +++ b/src/keyboard_utils.rs @@ -34,6 +34,13 @@ macro_rules! keyboard_buttons { )* markup } + + #[allow(unused)] + pub fn to_button(self) -> teloxide::types::InlineKeyboardButton { + match self { + $($($name::$variant => teloxide::types::InlineKeyboardButton::callback($text, $callback_data)),*),* + } + } } impl From<$name> for teloxide::types::InlineKeyboardButton { fn from(value: $name) -> Self { diff --git a/src/message_utils.rs b/src/message_utils.rs index 4531d67..4c5ff39 100644 --- a/src/message_utils.rs +++ b/src/message_utils.rs @@ -1,5 +1,6 @@ use crate::HandlerResult; use anyhow::bail; +use chrono::{DateTime, Utc}; use num::One; use std::fmt::Display; use teloxide::{ @@ -209,3 +210,7 @@ pub fn pluralize_with_count + Display + Copy>( ) -> String { format!("{} {}", count, pluralize(count, singular, plural)) } + +pub fn format_datetime(dt: DateTime) -> String { + dt.format("%b %d, %Y %H:%M UTC").to_string() +}