Implement bidirectional customer conversations with customer-based grouping and order tagging

This commit is contained in:
sysadmin
2025-08-27 19:18:46 +01:00
parent 3f4789730c
commit 027a3fd0c4
25 changed files with 794 additions and 70 deletions

View File

@@ -124,10 +124,18 @@ namespace TeleBot.Handlers
await HandleHelp(bot, callbackQuery.Message);
break;
case "support":
await HandleSupportCallback(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;
@@ -592,5 +600,36 @@ namespace TeleBot.Handlers
replyMarkup: MenuBuilder.MainMenu()
);
}
private async Task HandleSupportCallback(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
{
session.State = SessionState.CustomerSupport;
await bot.EditMessageTextAsync(
callbackQuery.Message!.Chat.Id,
callbackQuery.Message.MessageId,
"🎧 *Customer Support*\n\n" +
"You can now send a message to our support team. Simply type your message and we'll respond as soon as possible.\n\n" +
"_Type your message below, or use the Cancel button to return to the main menu._",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.SupportMenu()
);
await bot.AnswerCallbackQueryAsync(callbackQuery.Id);
}
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);
}
}
}

View File

@@ -84,6 +84,14 @@ namespace TeleBot.Handlers
await HandleClearCommand(bot, message, session);
break;
case "/support":
await HandleSupportCommand(bot, message, session);
break;
case "/cancel":
await HandleCancelCommand(bot, message, session);
break;
default:
await bot.SendTextMessageAsync(
message.Chat.Id,
@@ -297,5 +305,30 @@ namespace TeleBot.Handlers
replyMarkup: MenuBuilder.MainMenu()
);
}
private async Task HandleSupportCommand(ITelegramBotClient bot, Message message, Models.UserSession session)
{
session.State = Models.SessionState.CustomerSupport;
await bot.SendTextMessageAsync(
message.Chat.Id,
"🎧 *Customer Support*\n\n" +
"You can now send a message to our support team. Simply type your message and we'll respond as soon as possible.\n\n" +
"_Type your message below, or use /cancel to return to the main menu._",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.SupportMenu()
);
}
private async Task HandleCancelCommand(ITelegramBotClient bot, Message message, Models.UserSession session)
{
session.State = Models.SessionState.MainMenu;
await bot.SendTextMessageAsync(
message.Chat.Id,
"Operation cancelled. How can I help you today?",
replyMarkup: MenuBuilder.MainMenu()
);
}
}
}

View File

@@ -18,13 +18,16 @@ namespace TeleBot.Handlers
{
private readonly ISessionManager _sessionManager;
private readonly ILogger<MessageHandler> _logger;
private readonly ILittleShopService _shopService;
public MessageHandler(
ISessionManager sessionManager,
ILogger<MessageHandler> logger)
ILogger<MessageHandler> logger,
ILittleShopService shopService)
{
_sessionManager = sessionManager;
_logger = logger;
_shopService = shopService;
}
public async Task HandleMessageAsync(ITelegramBotClient bot, Message message)
@@ -41,6 +44,11 @@ namespace TeleBot.Handlers
{
await HandleCheckoutInput(bot, message, session);
}
// Handle customer support messages
else if (session.State == SessionState.CustomerSupport)
{
await HandleCustomerSupportMessage(bot, message, session);
}
else if (message.Text.StartsWith("/pgpkey "))
{
// Handle PGP key input
@@ -221,5 +229,53 @@ namespace TeleBot.Handlers
break;
}
}
private async Task HandleCustomerSupportMessage(ITelegramBotClient bot, Message message, UserSession session)
{
try
{
// Send the customer's message to the business
var success = await _shopService.SendCustomerMessageAsync(
message.From!.Id,
message.From.Username ?? "",
$"{message.From.FirstName} {message.From.LastName}".Trim(),
message.From.FirstName ?? "",
message.From.LastName ?? "",
"Customer Message", // Simple subject
message.Text!
);
if (success)
{
await bot.SendTextMessageAsync(
message.Chat.Id,
"✅ *Message sent to support team*\n\n" +
"We've received your message and will respond as soon as possible.\n\n" +
"You can continue shopping or send additional messages.",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.MainMenu()
);
session.State = SessionState.MainMenu;
}
else
{
await bot.SendTextMessageAsync(
message.Chat.Id,
"❌ Sorry, there was an error sending your message. Please try again.",
replyMarkup: MenuBuilder.SupportMenu()
);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling customer support message");
await bot.SendTextMessageAsync(
message.Chat.Id,
"❌ An error occurred. Please try again or use /cancel to return to the menu.",
replyMarkup: MenuBuilder.SupportMenu()
);
}
}
}
}

View File

@@ -97,6 +97,7 @@ namespace TeleBot.Models
ViewingOrders,
ViewingOrder,
PrivacySettings,
CustomerSupport,
Help
}
}

View File

@@ -83,6 +83,11 @@ builder.Services.AddHttpClient<BotManagerService>();
builder.Services.AddSingleton<BotManagerService>();
builder.Services.AddHostedService<BotManagerService>();
// Message Delivery Service - Single instance
builder.Services.AddSingleton<MessageDeliveryService>();
builder.Services.AddSingleton<IMessageDeliveryService>(sp => sp.GetRequiredService<MessageDeliveryService>());
builder.Services.AddHostedService<MessageDeliveryService>(sp => sp.GetRequiredService<MessageDeliveryService>());
// Bot Service
builder.Services.AddHostedService<TelegramBotService>();

View File

@@ -23,6 +23,7 @@ namespace TeleBot.Services
Task<List<CustomerMessage>?> GetPendingMessagesAsync();
Task<bool> MarkMessageAsSentAsync(Guid messageId, string? platformMessageId = null);
Task<bool> MarkMessageAsFailedAsync(Guid messageId, string reason);
Task<bool> SendCustomerMessageAsync(long telegramUserId, string telegramUsername, string displayName, string firstName, string lastName, string subject, string content);
}
public class LittleShopService : ILittleShopService
@@ -331,30 +332,22 @@ namespace TeleBot.Services
if (!await AuthenticateAsync())
return null;
// Call API to get pending messages for Telegram platform
var response = await _client.HttpClient.GetAsync("api/messages/pending?platform=Telegram");
// Use the client messages service
var messages = await _client.Messages.GetPendingMessagesAsync("Telegram");
if (response.IsSuccessStatusCode)
// Convert to TeleBot CustomerMessage format
return messages.Select(m => new CustomerMessage
{
var messages = await response.Content.ReadFromJsonAsync<List<PendingMessageDto>>();
// Convert to simplified CustomerMessage format
return messages?.Select(m => new CustomerMessage
{
Id = m.Id,
CustomerId = m.CustomerId,
TelegramUserId = m.TelegramUserId,
Subject = m.Subject,
Content = m.Content,
Type = (MessageType)m.Type,
IsUrgent = m.IsUrgent,
OrderReference = m.OrderReference,
CreatedAt = m.CreatedAt
}).ToList();
}
_logger.LogWarning("Failed to get pending messages: {StatusCode}", response.StatusCode);
return new List<CustomerMessage>();
Id = m.Id,
CustomerId = m.CustomerId,
TelegramUserId = m.TelegramUserId,
Subject = m.Subject,
Content = m.Content,
Type = (MessageType)m.Type,
IsUrgent = m.IsUrgent,
OrderReference = m.OrderReference,
CreatedAt = m.CreatedAt
}).ToList();
}
catch (Exception ex)
{
@@ -370,14 +363,7 @@ namespace TeleBot.Services
if (!await AuthenticateAsync())
return false;
var url = $"api/messages/{messageId}/mark-sent";
if (!string.IsNullOrEmpty(platformMessageId))
{
url += $"?platformMessageId={platformMessageId}";
}
var response = await _client.HttpClient.PostAsync(url, null);
return response.IsSuccessStatusCode;
return await _client.Messages.MarkMessageAsSentAsync(messageId, platformMessageId);
}
catch (Exception ex)
{
@@ -393,8 +379,7 @@ namespace TeleBot.Services
if (!await AuthenticateAsync())
return false;
var response = await _client.HttpClient.PostAsJsonAsync($"api/messages/{messageId}/mark-failed", reason);
return response.IsSuccessStatusCode;
return await _client.Messages.MarkMessageAsFailedAsync(messageId, reason);
}
catch (Exception ex)
{
@@ -418,5 +403,52 @@ namespace TeleBot.Services
_ => 0 // Default to BTC
};
}
public async Task<bool> SendCustomerMessageAsync(long telegramUserId, string telegramUsername, string displayName, string firstName, string lastName, string subject, string content)
{
try
{
if (!await AuthenticateAsync())
return false;
// First, get or create the customer
var customer = await _client.Customers.GetOrCreateCustomerAsync(new CreateCustomerRequest
{
TelegramUserId = telegramUserId,
TelegramUsername = telegramUsername,
TelegramDisplayName = displayName,
TelegramFirstName = firstName,
TelegramLastName = lastName,
AllowOrderUpdates = true,
AllowMarketing = false
});
if (!customer.IsSuccess || customer.Data == null)
{
_logger.LogError("Failed to get or create customer for support message");
return false;
}
// Create the customer message
var messageData = new
{
CustomerId = customer.Data.Id,
Type = 3, // CustomerService
Subject = subject,
Content = content,
Direction = 1, // CustomerToAdmin
Priority = 5,
IsUrgent = false
};
var response = await _client.Messages.CreateCustomerMessageAsync(messageData);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending customer message");
return false;
}
}
}
}

View File

@@ -13,6 +13,7 @@ namespace TeleBot.Services
public interface IMessageDeliveryService
{
Task DeliverPendingMessagesAsync(ITelegramBotClient bot);
void SetBotClient(ITelegramBotClient botClient);
}
public class MessageDeliveryService : IMessageDeliveryService, IHostedService
@@ -22,6 +23,7 @@ namespace TeleBot.Services
private readonly IConfiguration _configuration;
private Timer? _deliveryTimer;
private readonly int _pollIntervalSeconds;
private ITelegramBotClient? _botClient;
public MessageDeliveryService(
ILittleShopService shopService,
@@ -34,6 +36,12 @@ namespace TeleBot.Services
_pollIntervalSeconds = configuration.GetValue<int>("MessageDelivery:PollIntervalSeconds", 30);
}
public void SetBotClient(ITelegramBotClient botClient)
{
_botClient = botClient;
_logger.LogInformation("Bot client set for message delivery service");
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting Message Delivery Service - polling every {Interval} seconds", _pollIntervalSeconds);
@@ -55,12 +63,16 @@ namespace TeleBot.Services
{
try
{
// This will be called by the timer, but we need bot instance
// For now, just log that we're checking
_logger.LogDebug("Checking for pending messages to deliver...");
_logger.LogInformation("=== Checking for pending messages to deliver ===");
// Note: We'll need to integrate this with the bot instance
// This is a placeholder for the polling mechanism
if (_botClient != null)
{
await DeliverPendingMessagesAsync(_botClient);
}
else
{
_logger.LogWarning("Bot client not available for message delivery");
}
}
catch (Exception ex)
{
@@ -77,7 +89,7 @@ namespace TeleBot.Services
if (pendingMessages?.Any() != true)
{
_logger.LogDebug("No pending messages to deliver");
_logger.LogInformation("No pending messages to deliver");
return;
}

View File

@@ -23,6 +23,7 @@ namespace TeleBot
private readonly ICommandHandler _commandHandler;
private readonly ICallbackHandler _callbackHandler;
private readonly IMessageHandler _messageHandler;
private readonly IMessageDeliveryService _messageDeliveryService;
private ITelegramBotClient? _botClient;
private CancellationTokenSource? _cancellationTokenSource;
@@ -32,7 +33,8 @@ namespace TeleBot
IServiceProvider serviceProvider,
ICommandHandler commandHandler,
ICallbackHandler callbackHandler,
IMessageHandler messageHandler)
IMessageHandler messageHandler,
IMessageDeliveryService messageDeliveryService)
{
_configuration = configuration;
_logger = logger;
@@ -40,6 +42,7 @@ namespace TeleBot
_commandHandler = commandHandler;
_callbackHandler = callbackHandler;
_messageHandler = messageHandler;
_messageDeliveryService = messageDeliveryService;
}
public async Task StartAsync(CancellationToken cancellationToken)
@@ -69,6 +72,12 @@ namespace TeleBot
var me = await _botClient.GetMeAsync(cancellationToken);
_logger.LogInformation("Bot started: @{Username} ({Id})", me.Username, me.Id);
// Set bot client for message delivery service
if (_messageDeliveryService is MessageDeliveryService deliveryService)
{
deliveryService.SetBotClient(_botClient);
}
}
public Task StopAsync(CancellationToken cancellationToken)

View File

@@ -16,6 +16,7 @@ namespace TeleBot.UI
new[] { InlineKeyboardButton.WithCallbackData("🛍️ Browse Products", "browse") },
new[] { InlineKeyboardButton.WithCallbackData("🛒 View Cart", "cart") },
new[] { InlineKeyboardButton.WithCallbackData("📦 My Orders", "orders") },
new[] { InlineKeyboardButton.WithCallbackData("🎧 Customer Support", "support") },
new[] { InlineKeyboardButton.WithCallbackData("🔒 Privacy Settings", "privacy") },
new[] { InlineKeyboardButton.WithCallbackData("❓ Help", "help") }
});
@@ -277,6 +278,19 @@ namespace TeleBot.UI
};
}
public static InlineKeyboardMarkup SupportMenu()
{
var markup = new InlineKeyboardMarkup(new[]
{
new[]
{
InlineKeyboardButton.WithCallbackData("❌ Cancel", "cancel_support")
}
});
return markup;
}
private static string GetOrderStatusEmoji(int status)
{
return status switch

View File

@@ -256,9 +256,11 @@ namespace TeleBot.UI
"/browse - Browse products\n" +
"/cart - View shopping cart\n" +
"/orders - View your orders\n" +
"/support - Contact customer support\n" +
"/privacy - Privacy settings\n" +
"/pgpkey - Set PGP public key\n" +
"/ephemeral - Toggle ephemeral mode\n" +
"/cancel - Cancel current operation\n" +
"/delete - Delete all your data\n" +
"/tor - Get Tor onion address\n" +
"/help - Show this help message\n\n" +