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
}