littleshop/TeleBot/TeleBot/Handlers/CallbackHandler.cs
SysAdmin 524f0639e1 Fix multiple TeleBot and admin panel issues
- Fix admin panel to show all pending orders (PendingPayment + PaymentReceived)
- Fix currency display from USD ($) to GBP (£) throughout TeleBot
- Update payment methods to use dynamic SilverPay currency list
- Consolidate shipping address collection into single message
- Implement cart backup/restore on payment failure
- Remove unsupported XMR from TeleBot config

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 15:04:24 +01:00

1079 lines
45 KiB
C#

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using QRCoder;
using Telegram.Bot;
using Telegram.Bot.Exceptions;
using Telegram.Bot.Types;
using Telegram.Bot.Types.ReplyMarkups;
using TeleBot.Models;
using TeleBot.Services;
using TeleBot.UI;
using LittleShop.Client.Models;
namespace TeleBot.Handlers
{
public interface ICallbackHandler
{
Task HandleCallbackAsync(ITelegramBotClient bot, CallbackQuery callbackQuery);
}
public class CallbackHandler : ICallbackHandler
{
private readonly ISessionManager _sessionManager;
private readonly ILittleShopService _shopService;
private readonly IPrivacyService _privacyService;
private readonly IProductCarouselService _carouselService;
private readonly IConfiguration _configuration;
private readonly ILogger<CallbackHandler> _logger;
public CallbackHandler(
ISessionManager sessionManager,
ILittleShopService shopService,
IPrivacyService privacyService,
IProductCarouselService carouselService,
IConfiguration configuration,
ILogger<CallbackHandler> logger)
{
_sessionManager = sessionManager;
_shopService = shopService;
_privacyService = privacyService;
_carouselService = carouselService;
_configuration = configuration;
_logger = logger;
}
public async Task HandleCallbackAsync(ITelegramBotClient bot, CallbackQuery callbackQuery)
{
if (callbackQuery.Message == null || callbackQuery.Data == null)
return;
var session = await _sessionManager.GetOrCreateSessionAsync(callbackQuery.From.Id);
bool callbackAnswered = false;
try
{
// Answer callback immediately to prevent timeout
await bot.AnswerCallbackQueryAsync(callbackQuery.Id);
callbackAnswered = true;
var data = callbackQuery.Data.Split(':');
var action = data[0];
switch (action)
{
case "menu":
await HandleMainMenu(bot, callbackQuery.Message, session);
break;
case "browse":
await HandleBrowse(bot, callbackQuery.Message, session);
break;
case "category":
await HandleCategory(bot, callbackQuery.Message, session, Guid.Parse(data[1]));
break;
case "products":
if (data.Length > 1 && data[1] == "page")
{
await HandleProductsPage(bot, callbackQuery.Message, session, data);
}
else
{
await HandleProductList(bot, callbackQuery.Message, session, data);
}
break;
case "product":
await HandleProductDetail(bot, callbackQuery.Message, session, Guid.Parse(data[1]));
break;
case "qty":
await HandleQuantityChange(bot, callbackQuery.Message, session, data);
break;
case "add":
await HandleAddToCart(bot, callbackQuery, session, data);
break;
case "quickbuy":
await HandleQuickBuy(bot, callbackQuery, session, data);
break;
case "quickbuyvar":
await HandleQuickBuyWithVariation(bot, callbackQuery, session, data);
break;
case "cart":
await HandleViewCart(bot, callbackQuery.Message, session);
break;
case "remove":
await HandleRemoveFromCart(bot, callbackQuery, session, Guid.Parse(data[1]));
break;
case "clear_cart":
await HandleClearCart(bot, callbackQuery, session);
break;
case "checkout":
await HandleCheckout(bot, callbackQuery.Message, session);
break;
case "confirm_order":
await HandleConfirmOrder(bot, callbackQuery.Message, session, callbackQuery.From);
break;
case "pay":
await HandlePayment(bot, callbackQuery.Message, session, data[1]);
break;
case "orders":
await HandleViewOrders(bot, callbackQuery.Message, session, callbackQuery.From);
break;
case "order":
await HandleViewOrder(bot, callbackQuery.Message, session, Guid.Parse(data[1]), callbackQuery.From);
break;
case "privacy":
await HandlePrivacySettings(bot, callbackQuery.Message, session, data.Length > 1 ? data[1] : null);
break;
case "help":
await HandleHelp(bot, callbackQuery.Message);
break;
case "support":
await HandleSupportCallback(bot, callbackQuery, session);
break;
case "refresh_conversation":
await HandleRefreshConversation(bot, callbackQuery, session);
break;
case "exit_chat":
await HandleExitChat(bot, callbackQuery, session);
break;
case "noop":
// No operation - used for display-only buttons
break;
case "cancel_support":
await HandleCancelSupport(bot, callbackQuery, session);
break;
default:
_logger.LogWarning("Unknown callback action: {Action}", action);
break;
}
await _sessionManager.UpdateSessionAsync(session);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling callback {Data}", callbackQuery.Data);
// Only try to answer callback if not already answered
if (!callbackAnswered)
{
try
{
await bot.AnswerCallbackQueryAsync(
callbackQuery.Id,
"An error occurred. Please try again.",
showAlert: true
);
}
catch (ApiRequestException apiEx) when (apiEx.Message.Contains("query is too old"))
{
// Callback already expired, ignore
_logger.LogDebug("Callback query already expired");
}
}
}
}
private async Task HandleMainMenu(ITelegramBotClient bot, Message message, UserSession session)
{
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
MessageFormatter.FormatWelcome(true),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.MainMenu()
);
session.State = SessionState.MainMenu;
}
private async Task HandleBrowse(ITelegramBotClient bot, Message message, UserSession session)
{
var categories = await _shopService.GetCategoriesAsync();
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
MessageFormatter.FormatCategories(categories),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.CategoryMenu(categories)
);
session.State = SessionState.BrowsingCategories;
}
private async Task HandleCategory(ITelegramBotClient bot, Message message, UserSession session, Guid categoryId)
{
var categories = await _shopService.GetCategoriesAsync();
var category = categories.FirstOrDefault(c => c.Id == categoryId);
var products = await _shopService.GetProductsAsync(categoryId, 1);
// Edit the original message to show category header (bigger, more prominent)
var headerText = $"**{category?.Name ?? "Unknown Category"} PRODUCTS**\n\nBrowse individual products below:";
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
headerText,
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.CategoryNavigationMenu(categoryId)
);
// Send individual product bubbles
if (products.Items.Any())
{
foreach (var product in products.Items)
{
await bot.SendTextMessageAsync(
message.Chat.Id,
MessageFormatter.FormatSingleProduct(product),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.SingleProductMenu(product.Id)
);
}
// Send navigation buttons after all products
await bot.SendTextMessageAsync(
message.Chat.Id,
".",
replyMarkup: MenuBuilder.ProductNavigationMenu(categoryId)
);
}
else
{
await bot.SendTextMessageAsync(
message.Chat.Id,
"No products available in this category.",
replyMarkup: MenuBuilder.BackToCategoriesMenu()
);
}
session.State = SessionState.ViewingProducts;
}
private async Task HandleProductList(ITelegramBotClient bot, Message message, UserSession session, string[] data)
{
// Format: products:categoryId:page or products:all:page
var page = int.Parse(data[2]);
Guid? categoryId = data[1] != "all" ? Guid.Parse(data[1]) : null;
var products = await _shopService.GetProductsAsync(categoryId, page);
string? categoryName = null;
if (categoryId.HasValue)
{
var categories = await _shopService.GetCategoriesAsync();
categoryName = categories.FirstOrDefault(c => c.Id == categoryId)?.Name;
}
// Use carousel service to send products with images
await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, categoryName, page);
session.State = SessionState.ViewingProducts;
}
private async Task HandleProductsPage(ITelegramBotClient bot, Message message, UserSession session, string[] data)
{
// Format: products:page:pageNumber
var page = int.Parse(data[2]);
// Get products for all categories (no specific category filter)
var products = await _shopService.GetProductsAsync(null, page);
// Use carousel service to send products with images
await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, "All Categories", page);
session.State = SessionState.ViewingProducts;
}
private async Task HandleProductDetail(ITelegramBotClient bot, Message message, UserSession session, Guid productId)
{
var product = await _shopService.GetProductAsync(productId);
if (product == null)
{
await bot.AnswerCallbackQueryAsync("", "Product not found", showAlert: true);
return;
}
// Store current product in temp data for quantity selection
session.TempData["current_product"] = productId;
session.TempData["current_quantity"] = 1;
// Use carousel service to send product with image
await _carouselService.SendSingleProductWithImageAsync(bot, message.Chat.Id, product);
session.State = SessionState.ViewingProduct;
}
private async Task HandleQuantityChange(ITelegramBotClient bot, Message message, UserSession session, string[] data)
{
// Format: qty:productId:newQuantity
var productId = Guid.Parse(data[1]);
var quantity = int.Parse(data[2]);
var product = await _shopService.GetProductAsync(productId);
if (product == null)
return;
session.TempData["current_quantity"] = quantity;
await bot.EditMessageReplyMarkupAsync(
message.Chat.Id,
message.MessageId,
MenuBuilder.ProductDetailMenu(product, quantity)
);
}
private async Task HandleAddToCart(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data)
{
// Format: add:productId:quantity or add:productId:quantity:multiBuyId or add:productId:quantity:multiBuyId:variant
var productId = Guid.Parse(data[1]);
var quantity = int.Parse(data[2]);
Guid? multiBuyId = data.Length > 3 && !data[3].Contains(":") ? Guid.Parse(data[3]) : null;
string? selectedVariant = data.Length > 4 ? data[4] : (data.Length > 3 && data[3].Contains(":") ? data[3] : null);
var product = await _shopService.GetProductAsync(productId);
if (product == null)
{
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
return;
}
// If product has variants but none selected, show variant selection
if (selectedVariant == null && product.Variants?.Any() == true)
{
await ShowVariantSelection(bot, callbackQuery.Message!, session, product, quantity, multiBuyId);
return;
}
// Get price based on multi-buy or base product
decimal price = product.Price;
string itemName = product.Name;
if (multiBuyId.HasValue && product.MultiBuys != null)
{
var multiBuy = product.MultiBuys.FirstOrDefault(mb => mb.Id == multiBuyId);
if (multiBuy != null)
{
price = multiBuy.Price;
itemName = $"{product.Name} ({multiBuy.Name})";
quantity = multiBuy.Quantity; // Use multi-buy quantity
}
}
// Add variant to item name if selected
if (!string.IsNullOrEmpty(selectedVariant))
{
itemName += $" - {selectedVariant}";
}
session.Cart.AddItem(productId, itemName, price, quantity, multiBuyId, selectedVariant);
await bot.AnswerCallbackQueryAsync(
callbackQuery.Id,
$"✅ Added {quantity}x {itemName} to cart",
showAlert: false
);
// Send new cart message instead of editing
await SendNewCartMessage(bot, callbackQuery.Message!.Chat.Id, session);
}
private async Task HandleViewCart(ITelegramBotClient bot, Message message, UserSession session)
{
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
MessageFormatter.FormatCart(session.Cart),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.CartMenu(session.Cart)
);
session.State = SessionState.ViewingCart;
}
private async Task SendNewCartMessage(ITelegramBotClient bot, long chatId, UserSession session)
{
await bot.SendTextMessageAsync(
chatId,
MessageFormatter.FormatCart(session.Cart),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.CartMenu(session.Cart)
);
session.State = SessionState.ViewingCart;
}
private async Task ShowVariantSelection(ITelegramBotClient bot, Message message, UserSession session, Product product, int quantity, Guid? multiBuyId)
{
var text = $"**{product.Name}**\n\n";
text += "Please select a variant:\n";
var buttons = new List<InlineKeyboardButton[]>();
if (product.Variants?.Any() == true)
{
foreach (var variant in product.Variants.OrderBy(v => v.SortOrder))
{
var callbackData = multiBuyId.HasValue
? $"add:{product.Id}:{quantity}:{multiBuyId}:{variant.Name}"
: $"add:{product.Id}:{quantity}::{variant.Name}";
buttons.Add(new[]
{
InlineKeyboardButton.WithCallbackData(variant.Name, callbackData)
});
}
}
buttons.Add(new[]
{
InlineKeyboardButton.WithCallbackData("« Back", $"product:{product.Id}")
});
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
text,
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: new InlineKeyboardMarkup(buttons)
);
}
private async Task HandleQuickBuy(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data)
{
// Format: quickbuy:productId:quantity
var productId = Guid.Parse(data[1]);
var quantity = int.Parse(data[2]);
var product = await _shopService.GetProductAsync(productId);
if (product == null)
{
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
return;
}
// If variants exist, show variant selection with quickbuy flow
if (product.Variants?.Any() == true)
{
await ShowVariantSelectionForQuickBuy(bot, callbackQuery.Message!, session, product, quantity);
return;
}
// Add to cart with base product
session.Cart.AddItem(productId, product.Name, product.Price, quantity, null, null);
await bot.AnswerCallbackQueryAsync(
callbackQuery.Id,
$"✅ Added {quantity}x {product.Name} to cart",
showAlert: false
);
// Send cart summary in new message
await bot.SendTextMessageAsync(
callbackQuery.Message!.Chat.Id,
MessageFormatter.FormatCart(session.Cart),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.CartMenu(session.Cart)
);
// Immediately proceed to checkout
await Task.Delay(500); // Small delay for better UX
await HandleCheckout(bot, callbackQuery.Message, session);
}
private async Task ShowVariantSelectionForQuickBuy(ITelegramBotClient bot, Message message, UserSession session, Product product, int quantity)
{
var text = $"**Quick Buy: {product.Name}**\n\n";
text += "Please select a variant:\n";
var buttons = new List<InlineKeyboardButton[]>();
if (product.Variants?.Any() == true)
{
// Add buttons for each variant with quickbuy flow
foreach (var variant in product.Variants.OrderBy(v => v.SortOrder))
{
buttons.Add(new[]
{
InlineKeyboardButton.WithCallbackData(variant.Name, $"quickbuyvar:{product.Id}:{quantity}:{variant.Name}")
});
}
}
// Add back button
buttons.Add(new[]
{
InlineKeyboardButton.WithCallbackData("⬅️ Back", "menu")
});
await bot.SendTextMessageAsync(
message.Chat.Id,
text + "\n\n*Select an option for quick checkout:*",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: new InlineKeyboardMarkup(buttons)
);
}
private async Task HandleQuickBuyWithVariation(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data)
{
// Format: quickbuyvar:productId:quantity:variantName
var productId = Guid.Parse(data[1]);
var quantity = int.Parse(data[2]);
var variantName = data[3];
var product = await _shopService.GetProductAsync(productId);
if (product == null)
{
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
return;
}
// Add to cart with variant
var itemName = $"{product.Name} - {variantName}";
session.Cart.AddItem(productId, itemName, product.Price, quantity, null, variantName);
await bot.AnswerCallbackQueryAsync(
callbackQuery.Id,
$"✅ Added {quantity}x {itemName} to cart",
showAlert: false
);
// Send cart summary in new message
await bot.SendTextMessageAsync(
callbackQuery.Message!.Chat.Id,
MessageFormatter.FormatCart(session.Cart),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.CartMenu(session.Cart)
);
// Immediately proceed to checkout
await Task.Delay(500); // Small delay for better UX
await HandleCheckout(bot, callbackQuery.Message, session);
}
private async Task HandleRemoveFromCart(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, Guid productId)
{
var item = session.Cart.Items.FirstOrDefault(i => i.ProductId == productId);
if (item != null)
{
session.Cart.RemoveItem(productId);
await bot.AnswerCallbackQueryAsync(
callbackQuery.Id,
$"❌ Removed {item.ProductName} from cart",
showAlert: false
);
}
await HandleViewCart(bot, callbackQuery.Message!, session);
}
private async Task HandleClearCart(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
{
session.Cart.Clear();
await bot.AnswerCallbackQueryAsync(
callbackQuery.Id,
"🗑️ Cart cleared",
showAlert: false
);
await HandleViewCart(bot, callbackQuery.Message!, session);
}
private async Task HandleCheckout(ITelegramBotClient bot, Message message, UserSession session)
{
if (session.Cart.IsEmpty())
{
await bot.AnswerCallbackQueryAsync("", "Your cart is empty", showAlert: true);
return;
}
// Initialize order flow
session.OrderFlow = new OrderFlowData
{
UsePGPEncryption = session.Privacy.RequirePGP
};
session.State = SessionState.CheckoutFlow;
// Send new message for checkout instead of editing
await bot.SendTextMessageAsync(
message.Chat.Id,
"📦 *Checkout - Step 1/5*\n\n" +
"Please enter your shipping name:\n\n" +
"_Reply to this message with your name_",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown
);
}
private async Task HandleConfirmOrder(ITelegramBotClient bot, Message message, UserSession session, User telegramUser)
{
if (session.OrderFlow == null || session.Cart.IsEmpty())
{
await bot.AnswerCallbackQueryAsync("", "Invalid order state", showAlert: true);
return;
}
// Create the order with customer information
var order = await _shopService.CreateOrderAsync(
session,
telegramUser.Id,
telegramUser.Username ?? "",
$"{telegramUser.FirstName} {telegramUser.LastName}".Trim(),
telegramUser.FirstName ?? "",
telegramUser.LastName ?? "");
if (order == null)
{
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
"❌ Failed to create order. Please try again.",
replyMarkup: MenuBuilder.CartMenu(session.Cart)
);
return;
}
// Backup cart in case payment fails - we'll need to restore it
var cartBackup = System.Text.Json.JsonSerializer.Serialize(session.Cart);
session.TempData["cart_backup"] = cartBackup;
// Clear cart after successful order (will restore if payment fails)
session.Cart.Clear();
// Store order ID for payment
session.TempData["current_order_id"] = order.Id;
// Show payment options - get currencies dynamically from SilverPay support + admin settings
var currencies = await _shopService.GetAvailableCurrenciesAsync();
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
$"✅ *Order Created Successfully!*\n\n" +
$"Order ID: `{order.Id}`\n" +
$"Total: ${order.TotalAmount:F2}\n\n" +
"Select your preferred payment method:",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.PaymentMethodMenu(currencies)
);
}
private async Task HandlePayment(ITelegramBotClient bot, Message message, UserSession session, string currency)
{
if (!session.TempData.TryGetValue("current_order_id", out var orderIdObj) || orderIdObj is not Guid orderId)
{
await SafeEditMessageAsync(
bot,
message.Chat.Id,
message.MessageId,
"❌ Order not found. Please start a new order.",
Telegram.Bot.Types.Enums.ParseMode.Markdown,
MenuBuilder.MainMenu()
);
return;
}
// Show processing message
await SafeEditMessageAsync(
bot,
message.Chat.Id,
message.MessageId,
$"🔄 Creating {currency} payment...\n\nPlease wait...",
Telegram.Bot.Types.Enums.ParseMode.Markdown
);
try
{
var payment = await _shopService.CreatePaymentAsync(orderId, currency);
if (payment == null)
{
// Restore cart from backup since payment failed
if (session.TempData.TryGetValue("cart_backup", out var cartBackupObj) && cartBackupObj is string cartBackupJson)
{
try
{
session.Cart = System.Text.Json.JsonSerializer.Deserialize<ShoppingCart>(cartBackupJson) ?? new ShoppingCart();
session.TempData.Remove("cart_backup");
}
catch
{
// If restoration fails, cart remains empty
}
}
await SafeEditMessageAsync(
bot,
message.Chat.Id,
message.MessageId,
$"❌ *Payment Creation Failed*\n\n" +
$"Unable to create {currency} payment.\n" +
$"This might be due to:\n" +
$"• Payment gateway temporarily unavailable\n" +
$"• Network connectivity issues\n\n" +
$"Your cart has been restored. Please try again.",
Telegram.Bot.Types.Enums.ParseMode.Markdown,
MenuBuilder.CartMenu(session.Cart)
);
return;
}
// Payment created successfully, remove cart backup
session.TempData.Remove("cart_backup");
// Continue with display
var paymentText = MessageFormatter.FormatPayment(payment);
await DisplayPaymentInfo(bot, message, payment, paymentText);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create payment for order {OrderId} with currency {Currency}", orderId, currency);
// Restore cart from backup since payment failed
if (session.TempData.TryGetValue("cart_backup", out var cartBackupObj) && cartBackupObj is string cartBackupJson)
{
try
{
session.Cart = System.Text.Json.JsonSerializer.Deserialize<ShoppingCart>(cartBackupJson) ?? new ShoppingCart();
session.TempData.Remove("cart_backup");
}
catch
{
// If restoration fails, cart remains empty
}
}
await SafeEditMessageAsync(
bot,
message.Chat.Id,
message.MessageId,
$"❌ *Payment System Error*\n\n" +
$"Sorry, there was a technical issue creating your {currency} payment.\n\n" +
$"Our payment system may be undergoing maintenance.\n" +
$"Your cart has been restored. Please try again later.",
Telegram.Bot.Types.Enums.ParseMode.Markdown,
MenuBuilder.CartMenu(session.Cart)
);
return;
}
}
/// <summary>
/// Safely edit a message only if the content has changed
/// </summary>
private async Task SafeEditMessageAsync(ITelegramBotClient bot, ChatId chatId, int messageId, string newText,
Telegram.Bot.Types.Enums.ParseMode parseMode = Telegram.Bot.Types.Enums.ParseMode.Html,
InlineKeyboardMarkup? replyMarkup = null)
{
try
{
await bot.EditMessageTextAsync(chatId, messageId, newText, parseMode: parseMode, replyMarkup: replyMarkup);
}
catch (ApiRequestException apiEx) when (apiEx.Message.Contains("message is not modified"))
{
// Message content hasn't changed, this is fine
_logger.LogDebug("Attempted to edit message with identical content");
}
}
private async Task DisplayPaymentInfo(ITelegramBotClient bot, Message message, dynamic payment, string paymentText)
{
// Generate QR code if enabled
if (_configuration.GetValue<bool>("Features:EnableQRCodes"))
{
try
{
using var qrGenerator = new QRCodeGenerator();
var qrCodeData = qrGenerator.CreateQrCode(payment.WalletAddress, QRCodeGenerator.ECCLevel.Q);
using var qrCode = new PngByteQRCode(qrCodeData);
var qrCodeBytes = qrCode.GetGraphic(10);
using var stream = new System.IO.MemoryStream(qrCodeBytes);
await bot.SendPhotoAsync(
message.Chat.Id,
InputFile.FromStream(stream, "payment_qr.png"),
caption: paymentText,
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown
);
// Delete the original message
await bot.DeleteMessageAsync(message.Chat.Id, message.MessageId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate QR code");
// Fall back to text-only
await SafeEditMessageAsync(
bot,
message.Chat.Id,
message.MessageId,
paymentText,
Telegram.Bot.Types.Enums.ParseMode.Markdown,
MenuBuilder.MainMenu()
);
}
}
else
{
await SafeEditMessageAsync(
bot,
message.Chat.Id,
message.MessageId,
paymentText,
Telegram.Bot.Types.Enums.ParseMode.Markdown,
MenuBuilder.MainMenu()
);
}
}
private async Task HandleViewOrders(ITelegramBotClient bot, Message message, UserSession session, User telegramUser)
{
// Use new customer-based order lookup
var orders = await _shopService.GetCustomerOrdersAsync(
telegramUser.Id,
telegramUser.Username ?? "",
$"{telegramUser.FirstName} {telegramUser.LastName}".Trim(),
telegramUser.FirstName ?? "",
telegramUser.LastName ?? ""
);
if (!orders.Any())
{
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
"📦 *Your Orders*\n\nYou have no orders yet.",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.MainMenu()
);
}
else
{
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
$"📦 *Your Orders*\n\nFound {orders.Count} order(s):",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.OrderListMenu(orders)
);
}
session.State = SessionState.ViewingOrders;
}
private async Task HandleViewOrder(ITelegramBotClient bot, Message message, UserSession session, Guid orderId, User telegramUser)
{
var order = await _shopService.GetCustomerOrderAsync(
orderId,
telegramUser.Id,
telegramUser.Username ?? "",
$"{telegramUser.FirstName} {telegramUser.LastName}".Trim(),
telegramUser.FirstName ?? "",
telegramUser.LastName ?? ""
);
if (order == null)
{
await bot.AnswerCallbackQueryAsync("", "Order not found", showAlert: true);
return;
}
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
MessageFormatter.FormatOrder(order),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.MainMenu()
);
session.State = SessionState.ViewingOrder;
}
private async Task HandlePrivacySettings(ITelegramBotClient bot, Message message, UserSession session, string? setting)
{
if (setting != null)
{
switch (setting)
{
case "ephemeral":
session.Privacy.UseEphemeralMode = !session.Privacy.UseEphemeralMode;
session.IsEphemeral = session.Privacy.UseEphemeralMode;
break;
case "tor":
session.Privacy.UseTorOnly = !session.Privacy.UseTorOnly;
break;
case "pgp":
session.Privacy.RequirePGP = !session.Privacy.RequirePGP;
if (session.Privacy.RequirePGP && string.IsNullOrEmpty(session.Privacy.PGPPublicKey))
{
await bot.SendTextMessageAsync(
message.Chat.Id,
"Please set your PGP public key using:\n`/pgpkey YOUR_PUBLIC_KEY`",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown
);
}
break;
case "analytics":
session.Privacy.DisableAnalytics = !session.Privacy.DisableAnalytics;
break;
case "disappearing":
session.Privacy.EnableDisappearingMessages = !session.Privacy.EnableDisappearingMessages;
break;
case "delete":
await _sessionManager.DeleteUserDataAsync(message.From!.Id);
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
"✅ *All your data has been deleted*\n\nYou can start fresh with /start",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown
);
return;
}
}
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
MessageFormatter.FormatPrivacyPolicy(),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.PrivacyMenu(session.Privacy)
);
session.State = SessionState.PrivacySettings;
}
private async Task HandleHelp(ITelegramBotClient bot, Message message)
{
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
MessageFormatter.FormatHelp(),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.MainMenu()
);
}
private async Task HandleSupportCallback(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
{
session.State = SessionState.CustomerSupport;
await ShowCustomerConversationInCallback(bot, callbackQuery, session);
await bot.AnswerCallbackQueryAsync(callbackQuery.Id);
}
private async Task HandleRefreshConversation(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
{
await ShowCustomerConversationInCallback(bot, callbackQuery, session);
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Messages refreshed");
}
private async Task HandleExitChat(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
{
session.State = SessionState.MainMenu;
await bot.EditMessageTextAsync(
callbackQuery.Message!.Chat.Id,
callbackQuery.Message.MessageId,
"Chat ended. How can I help you today?",
replyMarkup: MenuBuilder.MainMenu()
);
await bot.AnswerCallbackQueryAsync(callbackQuery.Id);
}
private async Task ShowCustomerConversationInCallback(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
{
try
{
var user = callbackQuery.From;
// Get conversation history for this customer
var messages = await _shopService.GetCustomerConversationAsync(
user.Id,
user.Username ?? "",
$"{user.FirstName} {user.LastName}".Trim(),
user.FirstName ?? "",
user.LastName ?? ""
);
var conversationText = "💬 *Your Messages*\n\n";
if (messages?.Any() == true)
{
conversationText += "Recent conversation:\n\n";
foreach (var msg in messages.OrderBy(m => m.CreatedAt).TakeLast(8)) // Show last 8 messages
{
var isFromBusiness = msg.Direction == 0; // AdminToCustomer
var sender = isFromBusiness ? "🏪 Shop" : "👤 You";
var time = msg.CreatedAt.ToString("MMM dd, HH:mm");
conversationText += $"*{sender}* _{time}_\n{msg.Content}\n\n";
}
}
else
{
conversationText += "No messages yet. Start a conversation by typing below!\n\n";
}
conversationText += "_Type your message or use buttons below._";
await bot.EditMessageTextAsync(
callbackQuery.Message!.Chat.Id,
callbackQuery.Message.MessageId,
conversationText,
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.ConversationMenu()
);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error showing customer conversation in callback");
await bot.EditMessageTextAsync(
callbackQuery.Message!.Chat.Id,
callbackQuery.Message.MessageId,
"💬 *Customer Messages*\n\nReady to chat with our support team!\n\n_Type your message below._",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.ConversationMenu()
);
}
}
private async Task HandleCancelSupport(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
{
session.State = SessionState.MainMenu;
await bot.EditMessageTextAsync(
callbackQuery.Message!.Chat.Id,
callbackQuery.Message.MessageId,
"Customer support cancelled. How can I help you today?",
replyMarkup: MenuBuilder.MainMenu()
);
await bot.AnswerCallbackQueryAsync(callbackQuery.Id);
}
}
}