using System.Net.Http.Headers; using System.Text; using System.Text.Json; using LittleShop.DTOs; using LittleShop.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace LittleShop.Services; /// /// Service for discovering and configuring remote TeleBot instances. /// Handles communication with TeleBot's discovery API endpoints. /// public class BotDiscoveryService : IBotDiscoveryService { private readonly ILogger _logger; private readonly IConfiguration _configuration; private readonly HttpClient _httpClient; private readonly IBotService _botService; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; public BotDiscoveryService( ILogger logger, IConfiguration configuration, HttpClient httpClient, IBotService botService) { _logger = logger; _configuration = configuration; _httpClient = httpClient; _botService = botService; // Configure default timeout var timeoutSeconds = _configuration.GetValue("BotDiscovery:ConnectionTimeoutSeconds", 10); _httpClient.Timeout = TimeSpan.FromSeconds(timeoutSeconds); } public async Task ProbeRemoteBotAsync(string ipAddress, int port) { var endpoint = BuildEndpoint(ipAddress, port, "/api/discovery/probe"); _logger.LogInformation("Probing remote TeleBot at {Endpoint}", endpoint); try { using var request = new HttpRequestMessage(HttpMethod.Get, endpoint); AddDiscoverySecret(request); var response = await _httpClient.SendAsync(request); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); var probeResponse = JsonSerializer.Deserialize(content, JsonOptions); _logger.LogInformation("Successfully probed TeleBot: {InstanceId}, Status: {Status}", probeResponse?.InstanceId, probeResponse?.Status); return new DiscoveryResult { Success = true, Message = "Discovery successful", ProbeResponse = probeResponse }; } else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { _logger.LogWarning("Discovery probe rejected: invalid discovery secret"); return new DiscoveryResult { Success = false, Message = "Invalid discovery secret. Ensure the shared secret matches on both LittleShop and TeleBot." }; } else { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogWarning("Discovery probe failed: {StatusCode} - {Content}", response.StatusCode, errorContent); return new DiscoveryResult { Success = false, Message = $"Discovery failed: {response.StatusCode}" }; } } catch (TaskCanceledException) { _logger.LogWarning("Discovery probe timed out for {IpAddress}:{Port}", ipAddress, port); return new DiscoveryResult { Success = false, Message = "Connection timed out. Ensure the TeleBot instance is running and accessible." }; } catch (HttpRequestException ex) { _logger.LogWarning(ex, "Discovery probe connection failed for {IpAddress}:{Port}", ipAddress, port); return new DiscoveryResult { Success = false, Message = $"Connection failed: {ex.Message}" }; } catch (Exception ex) { _logger.LogError(ex, "Unexpected error during discovery probe"); return new DiscoveryResult { Success = false, Message = $"Unexpected error: {ex.Message}" }; } } public async Task InitializeRemoteBotAsync(Guid botId, string ipAddress, int port) { var endpoint = BuildEndpoint(ipAddress, port, "/api/discovery/initialize"); _logger.LogInformation("Initializing remote TeleBot at {Endpoint} for bot {BotId}", endpoint, botId); try { // Get the bot to retrieve the BotKey var bot = await _botService.GetBotByIdAsync(botId); if (bot == null) { return new InitializeResult { Success = false, Message = "Bot not found" }; } // Get the BotKey securely var botKey = await _botService.GetBotKeyAsync(botId); if (string.IsNullOrEmpty(botKey)) { return new InitializeResult { Success = false, Message = "Bot key not found" }; } var payload = new { BotKey = botKey, WebhookSecret = _configuration["BotDiscovery:WebhookSecret"] ?? "", LittleShopUrl = GetLittleShopUrl() }; using var request = new HttpRequestMessage(HttpMethod.Post, endpoint); AddDiscoverySecret(request); request.Content = new StringContent( JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json"); var response = await _httpClient.SendAsync(request); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); var initResponse = JsonSerializer.Deserialize(content, JsonOptions); _logger.LogInformation("Successfully initialized TeleBot: {InstanceId}", initResponse?.InstanceId); // Update bot's discovery status await UpdateBotDiscoveryStatus(botId, DiscoveryStatus.Initialized, ipAddress, port, initResponse?.InstanceId); return new InitializeResult { Success = true, Message = "TeleBot initialized successfully", InstanceId = initResponse?.InstanceId }; } else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { return new InitializeResult { Success = false, Message = "Invalid discovery secret" }; } else { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogWarning("Initialization failed: {StatusCode} - {Content}", response.StatusCode, errorContent); return new InitializeResult { Success = false, Message = $"Initialization failed: {response.StatusCode}" }; } } catch (Exception ex) { _logger.LogError(ex, "Error during TeleBot initialization"); return new InitializeResult { Success = false, Message = $"Initialization error: {ex.Message}" }; } } public async Task PushConfigurationAsync(Guid botId, string botToken, Dictionary? settings = null) { _logger.LogInformation("Pushing configuration to bot {BotId}", botId); try { // Get the bot details var bot = await _botService.GetBotByIdAsync(botId); if (bot == null) { return new ConfigureResult { Success = false, Message = "Bot not found" }; } if (string.IsNullOrEmpty(bot.RemoteAddress) || !bot.RemotePort.HasValue) { return new ConfigureResult { Success = false, Message = "Bot does not have remote address configured" }; } // Get the BotKey securely var botKey = await _botService.GetBotKeyAsync(botId); if (string.IsNullOrEmpty(botKey)) { return new ConfigureResult { Success = false, Message = "Bot key not found" }; } var endpoint = BuildEndpoint(bot.RemoteAddress, bot.RemotePort.Value, "/api/discovery/configure"); var payload = new { BotToken = botToken, Settings = settings }; using var request = new HttpRequestMessage(HttpMethod.Post, endpoint); request.Headers.Add("X-Bot-Key", botKey); request.Content = new StringContent( JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json"); var response = await _httpClient.SendAsync(request); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); var configResponse = JsonSerializer.Deserialize(content, JsonOptions); _logger.LogInformation("Successfully configured TeleBot: @{Username}", configResponse?.TelegramUsername); // Update bot's discovery status and platform info await UpdateBotDiscoveryStatus(botId, DiscoveryStatus.Configured, bot.RemoteAddress, bot.RemotePort.Value, bot.RemoteInstanceId); // Update platform info if (!string.IsNullOrEmpty(configResponse?.TelegramUsername)) { await _botService.UpdatePlatformInfoAsync(botId, new UpdatePlatformInfoDto { PlatformUsername = configResponse.TelegramUsername, PlatformDisplayName = configResponse.TelegramDisplayName ?? configResponse.TelegramUsername, PlatformId = configResponse.TelegramId }); } return new ConfigureResult { Success = true, Message = "Configuration pushed successfully", TelegramUsername = configResponse?.TelegramUsername, TelegramDisplayName = configResponse?.TelegramDisplayName, TelegramId = configResponse?.TelegramId }; } else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { return new ConfigureResult { Success = false, Message = "Invalid bot key. The bot may need to be re-initialized." }; } else { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogWarning("Configuration push failed: {StatusCode} - {Content}", response.StatusCode, errorContent); return new ConfigureResult { Success = false, Message = $"Configuration failed: {response.StatusCode}" }; } } catch (Exception ex) { _logger.LogError(ex, "Error pushing configuration to TeleBot"); return new ConfigureResult { Success = false, Message = $"Configuration error: {ex.Message}" }; } } public async Task TestConnectivityAsync(string ipAddress, int port) { try { var endpoint = BuildEndpoint(ipAddress, port, "/api/discovery/probe"); using var request = new HttpRequestMessage(HttpMethod.Get, endpoint); AddDiscoverySecret(request); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var response = await _httpClient.SendAsync(request, cts.Token); return response.IsSuccessStatusCode; } catch { return false; } } public async Task GetRemoteStatusAsync(string ipAddress, int port, string botKey) { try { var endpoint = BuildEndpoint(ipAddress, port, "/api/discovery/status"); using var request = new HttpRequestMessage(HttpMethod.Get, endpoint); request.Headers.Add("X-Bot-Key", botKey); var response = await _httpClient.SendAsync(request); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(content, JsonOptions); } return null; } catch { return null; } } #region Private Methods private string BuildEndpoint(string ipAddress, int port, string path) { // Use HTTP for local/private networks, HTTPS for public var scheme = IsPrivateNetwork(ipAddress) ? "http" : "https"; return $"{scheme}://{ipAddress}:{port}{path}"; } private bool IsPrivateNetwork(string ipAddress) { // Check if IP is in private ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x, localhost) if (ipAddress == "localhost" || ipAddress == "127.0.0.1") return true; if (System.Net.IPAddress.TryParse(ipAddress, out var ip)) { var bytes = ip.GetAddressBytes(); if (bytes.Length == 4) { if (bytes[0] == 10) return true; if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) return true; if (bytes[0] == 192 && bytes[1] == 168) return true; } } return false; } private void AddDiscoverySecret(HttpRequestMessage request) { var secret = _configuration["BotDiscovery:SharedSecret"]; if (!string.IsNullOrEmpty(secret)) { request.Headers.Add("X-Discovery-Secret", secret); } } private string GetLittleShopUrl() { // Return the public URL for LittleShop API return _configuration["BotDiscovery:LittleShopApiUrl"] ?? _configuration["Kestrel:Endpoints:Https:Url"] ?? "http://localhost:5000"; } private async Task UpdateBotDiscoveryStatus(Guid botId, string status, string ipAddress, int port, string? instanceId) { var success = await _botService.UpdateRemoteInfoAsync(botId, ipAddress, port, instanceId, status); if (success) { _logger.LogInformation("Updated bot {BotId} discovery status to {Status} at {Address}:{Port}", botId, status, ipAddress, port); } else { _logger.LogWarning("Failed to update discovery status for bot {BotId}", botId); } } #endregion #region Response DTOs private class InitializeResponse { public bool Success { get; set; } public string Message { get; set; } = string.Empty; public string? InstanceId { get; set; } } private class ConfigureResponse { public bool Success { get; set; } public string Message { get; set; } = string.Empty; public string? TelegramUsername { get; set; } public string? TelegramDisplayName { get; set; } public string? TelegramId { get; set; } } #endregion }