From ac4fe688d9364ce080cd088dd4ee71ebb4f7800b Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Thu, 25 Sep 2025 01:28:56 +0100 Subject: [PATCH] Add bot activity tracking system --- LittleShop/Controllers/BotController.cs | 132 ++++++++++++++++++ TeleBot/TeleBot/Handlers/CallbackHandler.cs | 74 +++++++++- .../TeleBot/Services/BotActivityTracker.cs | 102 ++++++++++++++ 3 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 LittleShop/Controllers/BotController.cs create mode 100644 TeleBot/TeleBot/Services/BotActivityTracker.cs diff --git a/LittleShop/Controllers/BotController.cs b/LittleShop/Controllers/BotController.cs new file mode 100644 index 0000000..fd258d7 --- /dev/null +++ b/LittleShop/Controllers/BotController.cs @@ -0,0 +1,132 @@ +using System; +using System.Threading.Tasks; +using LittleShop.Data; +using LittleShop.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace LittleShop.Controllers +{ + [ApiController] + [Route("api/bot")] + public class BotController : ControllerBase + { + private readonly LittleShopContext _context; + private readonly ILogger _logger; + + public BotController(LittleShopContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + [HttpPost("activity")] + public async Task TrackActivity([FromBody] BotActivityDto activity) + { + try + { + // Get the default bot ID (TeleBot) - we can enhance this to support multiple bots later + var bot = await _context.Bots.FirstOrDefaultAsync(b => b.Type == Enums.BotType.Telegram); + if (bot == null) + { + // Create a default TeleBot entry if not exists + bot = new Bot + { + Id = Guid.NewGuid(), + BotKey = "telebot-default", + Name = "TeleBot", + Type = Enums.BotType.Telegram, + Status = Enums.BotStatus.Active, + IsActive = true, + LastSeenAt = DateTime.UtcNow, + CreatedAt = DateTime.UtcNow, + PlatformUsername = "telebot", + PlatformDisplayName = "TeleBot", + Version = "1.0.0" + }; + _context.Bots.Add(bot); + await _context.SaveChangesAsync(); + } + + var botActivity = new BotActivity + { + Id = Guid.NewGuid(), + BotId = bot.Id, + SessionIdentifier = activity.SessionIdentifier, + UserDisplayName = activity.UserDisplayName, + ActivityType = activity.ActivityType, + ActivityDescription = activity.ActivityDescription, + ProductId = activity.ProductId, + ProductName = activity.ProductName ?? string.Empty, + CategoryName = activity.CategoryName ?? string.Empty, + Value = activity.Value, + Quantity = activity.Quantity, + Platform = activity.Platform, + Location = activity.Location, + Timestamp = activity.Timestamp ?? DateTime.UtcNow + }; + + _context.BotActivities.Add(botActivity); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Tracked bot activity: {Type} by {User} on {Platform}", + activity.ActivityType, activity.UserDisplayName, activity.Platform); + + return Ok(new { success = true, activityId = botActivity.Id }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error tracking bot activity"); + return StatusCode(500, new { success = false, error = "Failed to track activity" }); + } + } + + [HttpGet("activity/stats")] + public async Task GetActivityStats() + { + var now = DateTime.UtcNow; + var oneDayAgo = now.AddDays(-1); + var oneHourAgo = now.AddHours(-1); + var fiveMinutesAgo = now.AddMinutes(-5); + + var stats = new + { + TotalActivities = await _context.BotActivities.CountAsync(), + Last24Hours = await _context.BotActivities.CountAsync(a => a.Timestamp >= oneDayAgo), + LastHour = await _context.BotActivities.CountAsync(a => a.Timestamp >= oneHourAgo), + ActiveNow = await _context.BotActivities.CountAsync(a => a.Timestamp >= fiveMinutesAgo), + UniqueUsersToday = await _context.BotActivities + .Where(a => a.Timestamp >= oneDayAgo) + .Select(a => a.SessionIdentifier) + .Distinct() + .CountAsync(), + TopActivities = await _context.BotActivities + .Where(a => a.Timestamp >= oneDayAgo) + .GroupBy(a => a.ActivityType) + .Select(g => new { Type = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .Take(5) + .ToListAsync() + }; + + return Ok(stats); + } + } + + public class BotActivityDto + { + public string SessionIdentifier { get; set; } = string.Empty; + public string UserDisplayName { get; set; } = string.Empty; + public string ActivityType { get; set; } = string.Empty; + public string ActivityDescription { get; set; } = string.Empty; + public Guid? ProductId { get; set; } + public string? ProductName { get; set; } + public string? CategoryName { get; set; } + public decimal? Value { get; set; } + public int? Quantity { get; set; } + public string Platform { get; set; } = "Unknown"; + public string Location { get; set; } = "Unknown"; + public DateTime? Timestamp { get; set; } + } +} \ No newline at end of file diff --git a/TeleBot/TeleBot/Handlers/CallbackHandler.cs b/TeleBot/TeleBot/Handlers/CallbackHandler.cs index 4c01349..3acb91c 100644 --- a/TeleBot/TeleBot/Handlers/CallbackHandler.cs +++ b/TeleBot/TeleBot/Handlers/CallbackHandler.cs @@ -26,14 +26,16 @@ namespace TeleBot.Handlers private readonly ILittleShopService _shopService; private readonly IPrivacyService _privacyService; private readonly IProductCarouselService _carouselService; + private readonly IBotActivityTracker _activityTracker; private readonly IConfiguration _configuration; private readonly ILogger _logger; - + public CallbackHandler( ISessionManager sessionManager, ILittleShopService shopService, IPrivacyService privacyService, IProductCarouselService carouselService, + IBotActivityTracker activityTracker, IConfiguration configuration, ILogger logger) { @@ -41,6 +43,7 @@ namespace TeleBot.Handlers _shopService = shopService; _privacyService = privacyService; _carouselService = carouselService; + _activityTracker = activityTracker; _configuration = configuration; _logger = logger; } @@ -232,6 +235,13 @@ namespace TeleBot.Handlers private async Task HandleBrowse(ITelegramBotClient bot, Message message, UserSession session) { + // Track browsing activity + await _activityTracker.TrackActivityAsync( + message.Chat, + ActivityTypes.Browse, + "Browsing categories" + ); + var categories = await _shopService.GetCategoriesAsync(); await bot.EditMessageTextAsync( @@ -249,6 +259,13 @@ namespace TeleBot.Handlers var categories = await _shopService.GetCategoriesAsync(); var category = categories.FirstOrDefault(c => c.Id == categoryId); var products = await _shopService.GetProductsAsync(categoryId, 1); + + // Track category view + await _activityTracker.TrackActivityAsync( + message.Chat, + ActivityTypes.ViewProduct, + $"Viewing category: {category?.Name ?? "Unknown"}" + ); // Edit the original message to show category header (bigger, more prominent) var headerText = $"**{category?.Name ?? "Unknown Category"} PRODUCTS**\n\nBrowse individual products below:"; @@ -334,7 +351,15 @@ namespace TeleBot.Handlers await bot.AnswerCallbackQueryAsync("", "Product not found", showAlert: true); return; } - + + // Track product view + await _activityTracker.TrackActivityAsync( + message.Chat, + ActivityTypes.ViewProduct, + $"Viewing product: {product.Name}", + product + ); + // Store current product in temp data for quantity selection session.TempData["current_product"] = productId; session.TempData["current_quantity"] = 1; @@ -407,6 +432,16 @@ namespace TeleBot.Handlers session.Cart.AddItem(productId, itemName, price, quantity, multiBuyId, selectedVariant); + // Track add to cart action + await _activityTracker.TrackActivityAsync( + callbackQuery.Message!.Chat, + ActivityTypes.AddToCart, + $"Added to cart: {itemName}", + product, + price * quantity, + quantity + ); + await bot.AnswerCallbackQueryAsync( callbackQuery.Id, $"✅ Added {quantity}x {itemName} to cart", @@ -419,6 +454,15 @@ namespace TeleBot.Handlers private async Task HandleViewCart(ITelegramBotClient bot, Message message, UserSession session) { + // Track cart view + await _activityTracker.TrackActivityAsync( + message.Chat, + ActivityTypes.ViewCart, + $"Viewing cart with {session.Cart.Items.Count} items", + null, + session.Cart.GetTotalAmount() + ); + await bot.EditMessageTextAsync( message.Chat.Id, message.MessageId, @@ -499,6 +543,16 @@ namespace TeleBot.Handlers // Add to cart with base product session.Cart.AddItem(productId, product.Name, product.Price, quantity, null, null); + // Track quick buy action + await _activityTracker.TrackActivityAsync( + callbackQuery.Message!.Chat, + ActivityTypes.AddToCart, + $"Quick buy: {product.Name}", + product, + product.Price * quantity, + quantity + ); + await bot.AnswerCallbackQueryAsync( callbackQuery.Id, $"✅ Added {quantity}x {product.Name} to cart", @@ -623,6 +677,15 @@ namespace TeleBot.Handlers return; } + // Track checkout initiation + await _activityTracker.TrackActivityAsync( + message.Chat, + ActivityTypes.Checkout, + $"Starting checkout with {session.Cart.Items.Count} items", + null, + session.Cart.GetTotalAmount() + ); + // Initialize order flow session.OrderFlow = new OrderFlowData { @@ -876,6 +939,13 @@ namespace TeleBot.Handlers private async Task HandleViewOrders(ITelegramBotClient bot, Message message, UserSession session, User telegramUser) { + // Track view orders action + await _activityTracker.TrackActivityAsync( + message.Chat, + ActivityTypes.ViewOrders, + "Viewing order history" + ); + // Use new customer-based order lookup var orders = await _shopService.GetCustomerOrdersAsync( telegramUser.Id, diff --git a/TeleBot/TeleBot/Services/BotActivityTracker.cs b/TeleBot/TeleBot/Services/BotActivityTracker.cs new file mode 100644 index 0000000..2dd76c3 --- /dev/null +++ b/TeleBot/TeleBot/Services/BotActivityTracker.cs @@ -0,0 +1,102 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Telegram.Bot.Types; +using LittleShop.Client.Models; + +namespace TeleBot.Services +{ + public interface IBotActivityTracker + { + Task TrackActivityAsync( + Chat chat, + string activityType, + string description, + Product? product = null, + decimal? value = null, + int? quantity = null); + } + + public class BotActivityTracker : IBotActivityTracker + { + private readonly HttpClient _httpClient; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly string _littleShopUrl; + + public BotActivityTracker( + HttpClient httpClient, + IConfiguration configuration, + ILogger logger) + { + _httpClient = httpClient; + _configuration = configuration; + _logger = logger; + _littleShopUrl = configuration["LittleShop:BaseUrl"] ?? "http://littleshop-admin:8080"; + } + + public async Task TrackActivityAsync( + Chat chat, + string activityType, + string description, + Product? product = null, + decimal? value = null, + int? quantity = null) + { + try + { + var activity = new + { + SessionIdentifier = $"telegram_{chat.Id}", + UserDisplayName = chat.Username ?? $"{chat.FirstName} {chat.LastName}".Trim(), + ActivityType = activityType, + ActivityDescription = description, + ProductId = product?.Id, + ProductName = product?.Name, + CategoryName = product?.CategoryName, + Value = value, + Quantity = quantity, + Platform = "Telegram", + Location = "Unknown", + Timestamp = DateTime.UtcNow + }; + + var json = JsonSerializer.Serialize(activity); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync( + $"{_littleShopUrl}/api/bot/activity", + content); + + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Failed to track activity: {StatusCode}", response.StatusCode); + } + } + catch (Exception ex) + { + // Don't let tracking errors break the main flow + _logger.LogError(ex, "Error tracking bot activity"); + } + } + } + + public class ActivityTypes + { + public const string Browse = "Browse"; + public const string ViewProduct = "ViewProduct"; + public const string AddToCart = "AddToCart"; + public const string RemoveFromCart = "RemoveFromCart"; + public const string ViewCart = "ViewCart"; + public const string Checkout = "Checkout"; + public const string SelectVariant = "SelectVariant"; + public const string ViewOrders = "ViewOrders"; + public const string Search = "Search"; + public const string Help = "Help"; + public const string Start = "Start"; + } +} \ No newline at end of file