using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using LittleShop.DTOs; using LittleShop.Enums; using LittleShop.Services; using LittleShop.Models; namespace LittleShop.Areas.Admin.Controllers; [Area("Admin")] [Authorize(Policy = "AdminOnly")] public class BotsController : Controller { private readonly IBotService _botService; private readonly IBotMetricsService _metricsService; private readonly ITelegramBotManagerService _telegramManager; private readonly IBotDiscoveryService _discoveryService; private readonly ILogger _logger; public BotsController( IBotService botService, IBotMetricsService metricsService, ITelegramBotManagerService telegramManager, IBotDiscoveryService discoveryService, ILogger logger) { _botService = botService; _metricsService = metricsService; _telegramManager = telegramManager; _discoveryService = discoveryService; _logger = logger; } // GET: Admin/Bots public async Task Index() { var bots = await _botService.GetAllBotsAsync(); return View(bots); } // GET: Admin/Bots/Details/5 public async Task Details(Guid id) { var bot = await _botService.GetBotByIdAsync(id); if (bot == null) return NotFound(); // Get metrics summary for the last 30 days var metricsSummary = await _metricsService.GetMetricsSummaryAsync(id, DateTime.UtcNow.AddDays(-30), DateTime.UtcNow); ViewData["MetricsSummary"] = metricsSummary; // Get active sessions var activeSessions = await _metricsService.GetBotSessionsAsync(id, activeOnly: true); ViewData["ActiveSessions"] = activeSessions; return View(bot); } // GET: Admin/Bots/Create public IActionResult Create() { return View(new BotRegistrationDto()); } // GET: Admin/Bots/Wizard public IActionResult Wizard() { return View(new BotWizardDto()); } // POST: Admin/Bots/Wizard [HttpPost] [ValidateAntiForgeryToken] public async Task Wizard(BotWizardDto dto) { _logger.LogInformation("Wizard POST received - BotName: '{BotName}', BotUsername: '{BotUsername}'", dto.BotName, dto.BotUsername); if (!ModelState.IsValid) { _logger.LogWarning("Validation failed"); foreach (var error in ModelState) { _logger.LogWarning("Field {Field}: {Errors}", error.Key, string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage))); } return View(dto); } // Auto-assign personality if not selected if (string.IsNullOrEmpty(dto.PersonalityName)) { var personalities = new[] { "Alan", "Dave", "Sarah", "Mike", "Emma", "Tom" }; var random = new Random(); dto.PersonalityName = personalities[random.Next(personalities.Length)]; } // Generate BotFather commands var commands = GenerateBotFatherCommands(dto); ViewData["BotFatherCommands"] = commands; ViewData["ShowCommands"] = true; _logger.LogInformation("Generated BotFather commands successfully for bot '{BotName}' with personality '{PersonalityName}'", dto.BotName, dto.PersonalityName); return View(dto); } // POST: Admin/Bots/CompleteWizard [HttpPost] [ValidateAntiForgeryToken] public async Task CompleteWizard(BotWizardDto dto) { if (string.IsNullOrEmpty(dto.BotToken)) { ModelState.AddModelError("BotToken", "Bot token is required"); ViewData["BotFatherCommands"] = GenerateBotFatherCommands(dto); ViewData["ShowCommands"] = true; return View("Wizard", dto); } // Validate token first if (!await ValidateTelegramToken(dto.BotToken)) { ModelState.AddModelError("BotToken", "Invalid bot token"); ViewData["BotFatherCommands"] = GenerateBotFatherCommands(dto); ViewData["ShowCommands"] = true; return View("Wizard", dto); } // Create the bot var registrationDto = new BotRegistrationDto { Name = dto.BotName, Description = dto.Description, Type = BotType.Telegram, Version = "1.0.0", PersonalityName = dto.PersonalityName, InitialSettings = new Dictionary { ["telegram"] = new { botToken = dto.BotToken }, ["personality"] = new { name = dto.PersonalityName } } }; try { var result = await _botService.RegisterBotAsync(registrationDto); // Add bot to Telegram manager var telegramAdded = await _telegramManager.AddBotAsync(result.BotId, dto.BotToken); if (telegramAdded) { TempData["Success"] = $"Bot '{result.Name}' created successfully and is now running on Telegram!"; } else { TempData["Warning"] = $"Bot '{result.Name}' created but failed to connect to Telegram. Check token."; } return RedirectToAction(nameof(Details), new { id = result.BotId }); } catch (Exception ex) { _logger.LogError(ex, "Failed to create bot"); ModelState.AddModelError("", $"Failed to create bot: {ex.Message}"); return View("Wizard", dto); } } // POST: Admin/Bots/Create [HttpPost] [ValidateAntiForgeryToken] public async Task Create(BotRegistrationDto dto) { _logger.LogInformation("Received bot registration: Name={Name}, Type={Type}, Version={Version}", dto?.Name, dto?.Type, dto?.Version); if (!ModelState.IsValid) { _logger.LogWarning("Model validation failed for bot registration"); foreach (var error in ModelState.Values.SelectMany(v => v.Errors)) { _logger.LogWarning("Validation error: {Error}", error.ErrorMessage); } return View(dto); } try { var result = await _botService.RegisterBotAsync(dto); _logger.LogInformation("Bot registered successfully: {BotId}, Key: {KeyPrefix}...", result.BotId, result.BotKey.Substring(0, 8)); TempData["BotKey"] = result.BotKey; TempData["Success"] = $"Bot '{result.Name}' created successfully. Save the API key securely!"; return RedirectToAction(nameof(Details), new { id = result.BotId }); } catch (Exception ex) { _logger.LogError(ex, "Failed to create bot"); ModelState.AddModelError("", $"Failed to create bot: {ex.Message}"); return View(dto); } } // GET: Admin/Bots/Edit/5 public async Task Edit(Guid id) { var bot = await _botService.GetBotByIdAsync(id); if (bot == null) return NotFound(); ViewData["BotSettings"] = JsonSerializer.Serialize(bot.Settings, new JsonSerializerOptions { WriteIndented = true }); return View(bot); } // POST: Admin/Bots/Edit/5 [HttpPost] [ValidateAntiForgeryToken] public async Task Edit(Guid id, string settingsJson, BotStatus status) { try { // Parse and update settings var settings = JsonSerializer.Deserialize>(settingsJson) ?? new Dictionary(); var updateDto = new UpdateBotSettingsDto { Settings = settings }; await _botService.UpdateBotSettingsAsync(id, updateDto); await _botService.UpdateBotStatusAsync(id, status); TempData["Success"] = "Bot updated successfully"; return RedirectToAction(nameof(Details), new { id }); } catch (Exception ex) { _logger.LogError(ex, "Failed to update bot"); TempData["Error"] = "Failed to update bot"; return RedirectToAction(nameof(Edit), new { id }); } } // GET: Admin/Bots/Metrics/5 public async Task Metrics(Guid id, DateTime? startDate, DateTime? endDate) { var bot = await _botService.GetBotByIdAsync(id); if (bot == null) return NotFound(); var start = startDate ?? DateTime.UtcNow.AddDays(-7); var end = endDate ?? DateTime.UtcNow; var metricsSummary = await _metricsService.GetMetricsSummaryAsync(id, start, end); var sessionSummary = await _metricsService.GetSessionSummaryAsync(id, start, end); ViewData["Bot"] = bot; ViewData["SessionSummary"] = sessionSummary; ViewData["StartDate"] = start; ViewData["EndDate"] = end; return View(metricsSummary); } // POST: Admin/Bots/Delete/5 [HttpPost] [ValidateAntiForgeryToken] public async Task Delete(Guid id) { var success = await _botService.DeleteBotAsync(id); if (!success) { TempData["Error"] = "Failed to delete bot"; return RedirectToAction(nameof(Index)); } TempData["Success"] = "Bot deleted successfully"; return RedirectToAction(nameof(Index)); } // POST: Admin/Bots/Suspend/5 [HttpPost] [ValidateAntiForgeryToken] public async Task Suspend(Guid id) { await _botService.UpdateBotStatusAsync(id, BotStatus.Suspended); TempData["Success"] = "Bot suspended"; return RedirectToAction(nameof(Details), new { id }); } // POST: Admin/Bots/Activate/5 [HttpPost] [ValidateAntiForgeryToken] public async Task Activate(Guid id) { await _botService.UpdateBotStatusAsync(id, BotStatus.Active); TempData["Success"] = "Bot activated"; return RedirectToAction(nameof(Details), new { id }); } // POST: Admin/Bots/UpdateWallets/5 [HttpPost] [ValidateAntiForgeryToken] public async Task UpdateWallets(Guid id, Dictionary wallets) { try { var bot = await _botService.GetBotByIdAsync(id); if (bot == null) { TempData["Error"] = "Bot not found"; return RedirectToAction(nameof(Index)); } // Get current settings var settings = bot.Settings ?? new Dictionary(); // Filter out empty wallet addresses var validWallets = wallets .Where(w => !string.IsNullOrWhiteSpace(w.Value)) .ToDictionary(w => w.Key, w => w.Value.Trim()); // Update or add wallets section settings["wallets"] = validWallets; // Save settings await _botService.UpdateBotSettingsAsync(id, new UpdateBotSettingsDto { Settings = settings }); _logger.LogInformation("Updated {Count} wallet addresses for bot {BotId}", validWallets.Count, id); TempData["Success"] = $"Updated {validWallets.Count} wallet address(es) successfully"; return RedirectToAction(nameof(Edit), new { id }); } catch (Exception ex) { _logger.LogError(ex, "Failed to update wallets for bot {BotId}", id); TempData["Error"] = "Failed to update wallet addresses"; return RedirectToAction(nameof(Edit), new { id }); } } // GET: Admin/Bots/RegenerateKey/5 public IActionResult RegenerateKey(Guid id) { // This would require updating the bot model to support key regeneration TempData["Error"] = "Key regeneration not yet implemented"; return RedirectToAction(nameof(Details), new { id }); } private string GenerateBotFatherCommands(BotWizardDto dto) { var commands = new List { "1. Open Telegram and find @BotFather", "2. Send: /newbot", $"3. Send bot name: {dto.BotName}", $"4. Send bot username: {dto.BotUsername}", "5. Copy the token from BotFather's response", "6. Paste the token in the field below" }; if (!string.IsNullOrEmpty(dto.Description)) { commands.Add($"7. Optional: Send /setdescription and then: {dto.Description}"); } return string.Join("\n", commands); } private async Task ValidateTelegramToken(string token) { try { using var httpClient = new HttpClient(); var response = await httpClient.GetAsync($"https://api.telegram.org/bot{token}/getMe"); return response.IsSuccessStatusCode; } catch { return false; } } #region Remote Bot Discovery // GET: Admin/Bots/DiscoverRemote public IActionResult DiscoverRemote() { return View(new DiscoveryWizardViewModel()); } // POST: Admin/Bots/ProbeRemote [HttpPost] [ValidateAntiForgeryToken] public async Task ProbeRemote(DiscoveryWizardViewModel model) { _logger.LogInformation("Probing remote TeleBot at {IpAddress}:{Port}", model.IpAddress, model.Port); var result = await _discoveryService.ProbeRemoteBotAsync(model.IpAddress, model.Port); if (result.Success && result.ProbeResponse != null) { model.ProbeResponse = result.ProbeResponse; model.BotName = result.ProbeResponse.Name; model.CurrentStep = 2; model.SuccessMessage = "TeleBot discovered successfully!"; // Auto-select a personality if not already configured if (string.IsNullOrEmpty(model.PersonalityName)) { var personalities = new[] { "Alan", "Dave", "Sarah", "Mike", "Emma", "Tom" }; model.PersonalityName = personalities[new Random().Next(personalities.Length)]; } } else { model.ErrorMessage = result.Message; model.CurrentStep = 1; } return View("DiscoverRemote", model); } // POST: Admin/Bots/RegisterRemote [HttpPost] [ValidateAntiForgeryToken] public async Task RegisterRemote(DiscoveryWizardViewModel model) { _logger.LogInformation("Registering remote bot: {BotName} at {IpAddress}:{Port}", model.BotName, model.IpAddress, model.Port); if (string.IsNullOrEmpty(model.BotName)) { model.ErrorMessage = "Bot name is required"; model.CurrentStep = 2; return View("DiscoverRemote", model); } try { // Create the bot in the database var registrationDto = new BotRegistrationDto { Name = model.BotName, Description = model.Description, Type = BotType.Telegram, Version = model.ProbeResponse?.Version ?? "1.0.0", PersonalityName = model.PersonalityName, InitialSettings = new Dictionary { ["discovery"] = new { remoteAddress = model.IpAddress, remotePort = model.Port } } }; var botResult = await _botService.RegisterBotAsync(registrationDto); // Update the bot with remote discovery info var bot = await _botService.GetBotByIdAsync(botResult.BotId); if (bot != null) { // Update remote fields directly (we'll need to add this method to IBotService) await UpdateBotRemoteInfoAsync(botResult.BotId, model.IpAddress, model.Port, model.ProbeResponse?.InstanceId, DiscoveryStatus.Discovered); } // Initialize the remote TeleBot with the BotKey var initResult = await _discoveryService.InitializeRemoteBotAsync(botResult.BotId, model.IpAddress, model.Port); if (initResult.Success) { // Update status to Initialized await UpdateBotRemoteInfoAsync(botResult.BotId, model.IpAddress, model.Port, model.ProbeResponse?.InstanceId, DiscoveryStatus.Initialized); model.BotId = botResult.BotId; model.BotKey = botResult.BotKey; model.CurrentStep = 3; model.SuccessMessage = "Bot registered and initialized! Now enter the Telegram bot token."; _logger.LogInformation("Remote bot registered and initialized: {BotId}", botResult.BotId); } else { model.ErrorMessage = $"Bot registered but initialization failed: {initResult.Message}"; model.BotId = botResult.BotId; model.BotKey = botResult.BotKey; model.CurrentStep = 3; } return View("DiscoverRemote", model); } catch (Exception ex) { _logger.LogError(ex, "Failed to register remote bot"); model.ErrorMessage = $"Registration failed: {ex.Message}"; model.CurrentStep = 2; return View("DiscoverRemote", model); } } // POST: Admin/Bots/ConfigureRemote [HttpPost] [ValidateAntiForgeryToken] public async Task ConfigureRemote(DiscoveryWizardViewModel model) { _logger.LogInformation("Configuring remote bot {BotId} with Telegram token", model.BotId); if (!model.BotId.HasValue) { model.ErrorMessage = "Bot ID is missing"; model.CurrentStep = 1; return View("DiscoverRemote", model); } if (string.IsNullOrEmpty(model.BotToken)) { model.ErrorMessage = "Telegram bot token is required"; model.CurrentStep = 3; return View("DiscoverRemote", model); } // Validate the token first if (!await ValidateTelegramToken(model.BotToken)) { model.ErrorMessage = "Invalid Telegram bot token. Please check and try again."; model.CurrentStep = 3; return View("DiscoverRemote", model); } try { // Push configuration to the remote TeleBot var configResult = await _discoveryService.PushConfigurationAsync(model.BotId.Value, model.BotToken); if (configResult.Success) { // Update bot settings with the token var bot = await _botService.GetBotByIdAsync(model.BotId.Value); if (bot != null) { var settings = bot.Settings ?? new Dictionary(); settings["telegram"] = new { botToken = model.BotToken }; await _botService.UpdateBotSettingsAsync(model.BotId.Value, new UpdateBotSettingsDto { Settings = settings }); // Update discovery status to Configured await UpdateBotRemoteInfoAsync(model.BotId.Value, bot.RemoteAddress ?? model.IpAddress, bot.RemotePort ?? model.Port, bot.RemoteInstanceId, DiscoveryStatus.Configured); // Activate the bot await _botService.UpdateBotStatusAsync(model.BotId.Value, BotStatus.Active); } TempData["Success"] = $"Remote bot configured successfully! Telegram: @{configResult.TelegramUsername}"; return RedirectToAction(nameof(Details), new { id = model.BotId.Value }); } else { model.ErrorMessage = $"Configuration failed: {configResult.Message}"; model.CurrentStep = 3; return View("DiscoverRemote", model); } } catch (Exception ex) { _logger.LogError(ex, "Failed to configure remote bot"); model.ErrorMessage = $"Configuration failed: {ex.Message}"; model.CurrentStep = 3; return View("DiscoverRemote", model); } } // POST: Admin/Bots/CheckRemoteStatus/5 [HttpPost] [ValidateAntiForgeryToken] public async Task CheckRemoteStatus(Guid id) { _logger.LogInformation("Checking remote status for bot {BotId}", id); var bot = await _botService.GetBotByIdAsync(id); if (bot == null) { TempData["Error"] = "Bot not found"; return RedirectToAction(nameof(Index)); } if (!bot.IsRemote) { TempData["Error"] = "This is not a remote bot"; return RedirectToAction(nameof(Details), new { id }); } try { var result = await _discoveryService.ProbeRemoteBotAsync(bot.RemoteAddress!, bot.RemotePort ?? 5000); if (result.Success && result.ProbeResponse != null) { // Update discovery status await UpdateBotRemoteInfoAsync(id, bot.RemoteAddress!, bot.RemotePort ?? 5000, result.ProbeResponse.InstanceId, result.ProbeResponse.Status); var statusMessage = result.ProbeResponse.IsConfigured ? $"Bot is online and configured. Telegram: @{result.ProbeResponse.TelegramUsername}" : "Bot is online but not yet configured with a Telegram token."; TempData["Success"] = statusMessage; } else { // Update status to indicate offline await UpdateBotRemoteInfoAsync(id, bot.RemoteAddress!, bot.RemotePort ?? 5000, bot.RemoteInstanceId, DiscoveryStatus.Offline); TempData["Error"] = $"Remote bot is not responding: {result.Message}"; } } catch (Exception ex) { _logger.LogError(ex, "Failed to check remote status for bot {BotId}", id); TempData["Error"] = $"Failed to check status: {ex.Message}"; } return RedirectToAction(nameof(Details), new { id }); } // GET: Admin/Bots/RepushConfig/5 public async Task RepushConfig(Guid id) { var bot = await _botService.GetBotByIdAsync(id); if (bot == null) return NotFound(); if (!bot.IsRemote) { TempData["Error"] = "This is not a remote bot"; return RedirectToAction(nameof(Details), new { id }); } // Try to get existing token from settings string? existingToken = null; if (bot.Settings.TryGetValue("telegram", out var telegramObj)) { try { var telegramJson = JsonSerializer.Serialize(telegramObj); var telegramDict = JsonSerializer.Deserialize>(telegramJson); if (telegramDict?.TryGetValue("botToken", out var tokenObj) == true) { existingToken = tokenObj?.ToString(); } } catch { } } ViewData["ExistingToken"] = existingToken; ViewData["HasExistingToken"] = !string.IsNullOrEmpty(existingToken); return View(bot); } // POST: Admin/Bots/RepushConfig/5 [HttpPost] [ValidateAntiForgeryToken] public async Task RepushConfig(Guid id, string botToken, bool useExistingToken = false) { _logger.LogInformation("Re-pushing configuration to remote bot {BotId}", id); var bot = await _botService.GetBotByIdAsync(id); if (bot == null) { TempData["Error"] = "Bot not found"; return RedirectToAction(nameof(Index)); } if (!bot.IsRemote) { TempData["Error"] = "This is not a remote bot"; return RedirectToAction(nameof(Details), new { id }); } // If using existing token, retrieve it if (useExistingToken) { if (bot.Settings.TryGetValue("telegram", out var telegramObj)) { try { var telegramJson = JsonSerializer.Serialize(telegramObj); var telegramDict = JsonSerializer.Deserialize>(telegramJson); if (telegramDict?.TryGetValue("botToken", out var tokenObj) == true) { botToken = tokenObj?.ToString() ?? string.Empty; } } catch { } } } if (string.IsNullOrEmpty(botToken)) { TempData["Error"] = "Bot token is required"; return RedirectToAction(nameof(RepushConfig), new { id }); } // Validate the token if (!await ValidateTelegramToken(botToken)) { TempData["Error"] = "Invalid Telegram bot token"; return RedirectToAction(nameof(RepushConfig), new { id }); } try { // First, re-initialize if needed var probeResult = await _discoveryService.ProbeRemoteBotAsync(bot.RemoteAddress!, bot.RemotePort ?? 5000); if (!probeResult.Success) { TempData["Error"] = $"Cannot reach remote bot: {probeResult.Message}"; return RedirectToAction(nameof(Details), new { id }); } // If bot is not initialized, initialize first if (probeResult.ProbeResponse?.Status == DiscoveryStatus.AwaitingDiscovery || probeResult.ProbeResponse?.Status == DiscoveryStatus.Discovered) { var initResult = await _discoveryService.InitializeRemoteBotAsync(id, bot.RemoteAddress!, bot.RemotePort ?? 5000); if (!initResult.Success) { TempData["Error"] = $"Failed to initialize remote bot: {initResult.Message}"; return RedirectToAction(nameof(Details), new { id }); } } // Push the configuration var configResult = await _discoveryService.PushConfigurationAsync(id, botToken); if (configResult.Success) { // Update bot settings with the token var settings = bot.Settings ?? new Dictionary(); settings["telegram"] = new { botToken = botToken }; await _botService.UpdateBotSettingsAsync(id, new UpdateBotSettingsDto { Settings = settings }); // Update discovery status await UpdateBotRemoteInfoAsync(id, bot.RemoteAddress!, bot.RemotePort ?? 5000, probeResult.ProbeResponse?.InstanceId, DiscoveryStatus.Configured); TempData["Success"] = $"Configuration pushed successfully! Telegram: @{configResult.TelegramUsername}"; } else { TempData["Error"] = $"Failed to push configuration: {configResult.Message}"; } } catch (Exception ex) { _logger.LogError(ex, "Failed to re-push configuration to bot {BotId}", id); TempData["Error"] = $"Failed to push configuration: {ex.Message}"; } return RedirectToAction(nameof(Details), new { id }); } // Helper method to update bot remote info private async Task UpdateBotRemoteInfoAsync(Guid botId, string ipAddress, int port, string? instanceId, string status) { await _botService.UpdateRemoteInfoAsync(botId, ipAddress, port, instanceId, status); } #endregion }