littleshop/TeleBot/TeleBot/Services/BotManagerService.cs

604 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace TeleBot.Services
{
public class BotManagerService : IHostedService, IDisposable
{
private readonly IConfiguration _configuration;
private readonly ILogger<BotManagerService> _logger;
private readonly HttpClient _httpClient;
private readonly SessionManager _sessionManager;
private Timer? _heartbeatTimer;
private Timer? _metricsTimer;
private Timer? _settingsSyncTimer;
private string? _botKey;
private Guid? _botId;
private readonly Dictionary<string, decimal> _metricsBuffer;
private TelegramBotService? _telegramBotService;
private string? _lastKnownBotToken;
public BotManagerService(
IConfiguration configuration,
ILogger<BotManagerService> logger,
HttpClient httpClient,
SessionManager sessionManager)
{
_configuration = configuration;
_logger = logger;
_httpClient = httpClient;
_sessionManager = sessionManager;
_metricsBuffer = new Dictionary<string, decimal>();
}
public void SetTelegramBotService(TelegramBotService telegramBotService)
{
_telegramBotService = telegramBotService;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
// Check if bot key exists in configuration
_botKey = _configuration["BotManager:ApiKey"];
if (string.IsNullOrEmpty(_botKey))
{
// Try to find existing bot registration by Telegram username first
var botUsername = await GetTelegramBotUsernameAsync();
if (!string.IsNullOrEmpty(botUsername))
{
var existingBot = await FindExistingBotByPlatformAsync(botUsername);
if (existingBot != null)
{
_logger.LogInformation("Found existing bot registration for @{Username} (ID: {BotId}). Using existing bot.",
botUsername, existingBot.Id);
_botKey = existingBot.BotKey;
_botId = existingBot.Id;
// Update platform info in case it changed
await UpdatePlatformInfoAsync();
}
else
{
_logger.LogInformation("No existing bot found for @{Username}. Registering new bot.", botUsername);
await RegisterBotAsync();
}
}
else
{
_logger.LogWarning("Could not determine bot username. Registering new bot.");
await RegisterBotAsync();
}
}
else
{
// Authenticate existing bot
await AuthenticateBotAsync();
}
// Sync settings from server
await SyncSettingsAsync();
// Start heartbeat timer (every 30 seconds)
_heartbeatTimer = new Timer(SendHeartbeat, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
// Start metrics timer (every 60 seconds)
_metricsTimer = new Timer(SendMetrics, null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
// Start settings sync timer (every 5 minutes)
_settingsSyncTimer = new Timer(SyncSettingsWithBotUpdate, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(5));
_logger.LogInformation("Bot manager service started successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start bot manager service");
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_heartbeatTimer?.Change(Timeout.Infinite, 0);
_metricsTimer?.Change(Timeout.Infinite, 0);
_settingsSyncTimer?.Change(Timeout.Infinite, 0);
// Send final metrics before stopping
SendMetrics(null);
_logger.LogInformation("Bot manager service stopped");
return Task.CompletedTask;
}
private async Task RegisterBotAsync()
{
var apiUrl = _configuration["LittleShop:ApiUrl"];
var registrationData = new
{
Name = _configuration["BotInfo:Name"] ?? "TeleBot",
Description = _configuration["BotInfo:Description"] ?? "Telegram E-commerce Bot",
Type = 0, // Telegram
Version = _configuration["BotInfo:Version"] ?? "1.0.0",
InitialSettings = new Dictionary<string, object>
{
["telegram"] = new
{
botToken = _configuration["Telegram:BotToken"],
webhookUrl = _configuration["Telegram:WebhookUrl"]
},
["privacy"] = new
{
mode = _configuration["Privacy:Mode"],
enableTor = _configuration.GetValue<bool>("Privacy:EnableTor")
}
}
};
var json = JsonSerializer.Serialize(registrationData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{apiUrl}/api/bots/register", content);
if (response.IsSuccessStatusCode)
{
var responseJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<BotRegistrationResponse>(responseJson);
_botKey = result?.BotKey;
_botId = result?.BotId;
_logger.LogInformation("Bot registered successfully. Bot ID: {BotId}", _botId);
_logger.LogWarning("IMPORTANT: Save this bot key securely: {BotKey}", _botKey);
// Update platform info immediately after registration
await UpdatePlatformInfoAsync();
// Save bot key to configuration or secure storage
// In production, this should be saved securely
}
else
{
_logger.LogError("Failed to register bot: {StatusCode}", response.StatusCode);
}
}
private async Task AuthenticateBotAsync()
{
var apiUrl = _configuration["LittleShop:ApiUrl"];
var authData = new { BotKey = _botKey };
var json = JsonSerializer.Serialize(authData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{apiUrl}/api/bots/authenticate", content);
if (response.IsSuccessStatusCode)
{
var responseJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<BotDto>(responseJson);
_botId = result?.Id;
_logger.LogInformation("Bot authenticated successfully. Bot ID: {BotId}", _botId);
}
else
{
_logger.LogError("Failed to authenticate bot: {StatusCode}", response.StatusCode);
}
}
private async Task SyncSettingsAsync()
{
if (string.IsNullOrEmpty(_botKey)) return;
var settings = await GetSettingsAsync();
if (settings != null)
{
// Apply settings to configuration
// This would update the running configuration with server settings
_logger.LogInformation("Settings synced from server");
}
}
public async Task<Dictionary<string, object>?> GetSettingsAsync()
{
if (string.IsNullOrEmpty(_botKey)) return null;
try
{
var apiUrl = _configuration["LittleShop:ApiUrl"];
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
var response = await _httpClient.GetAsync($"{apiUrl}/api/bots/settings");
if (response.IsSuccessStatusCode)
{
var settingsJson = await response.Content.ReadAsStringAsync();
var settings = JsonSerializer.Deserialize<Dictionary<string, object>>(settingsJson);
return settings;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch settings from API");
}
return null;
}
private async void SendHeartbeat(object? state)
{
if (string.IsNullOrEmpty(_botKey)) return;
try
{
var apiUrl = _configuration["LittleShop:ApiUrl"];
var activeSessions = _sessionManager.GetActiveSessions().Count();
var heartbeatData = new
{
Version = _configuration["BotInfo:Version"] ?? "1.0.0",
IpAddress = "REDACTED", // SECURITY: Never send real IP address
ActiveSessions = activeSessions,
Status = new Dictionary<string, object>
{
["healthy"] = true,
["uptime"] = DateTime.UtcNow.Subtract(AppDomain.CurrentDomain.BaseDirectory != null
? new System.IO.DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory).CreationTimeUtc
: DateTime.UtcNow).TotalSeconds
}
};
var json = JsonSerializer.Serialize(heartbeatData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
await _httpClient.PostAsync($"{apiUrl}/api/bots/heartbeat", content);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send heartbeat");
}
}
private async void SendMetrics(object? state)
{
if (string.IsNullOrEmpty(_botKey)) return;
try
{
var apiUrl = _configuration["LittleShop:ApiUrl"];
var metrics = new List<object>();
// Collect metrics from buffer
lock (_metricsBuffer)
{
foreach (var metric in _metricsBuffer)
{
metrics.Add(new
{
MetricType = GetMetricType(metric.Key),
Value = metric.Value,
Category = "Bot",
Description = metric.Key
});
}
_metricsBuffer.Clear();
}
if (!metrics.Any()) return;
var metricsData = new { Metrics = metrics };
var json = JsonSerializer.Serialize(metricsData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
await _httpClient.PostAsync($"{apiUrl}/api/bots/metrics/batch", content);
_logger.LogDebug("Sent {Count} metrics to server", metrics.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send metrics");
}
}
public void RecordMetric(string name, decimal value)
{
lock (_metricsBuffer)
{
if (_metricsBuffer.ContainsKey(name))
_metricsBuffer[name] += value;
else
_metricsBuffer[name] = value;
}
}
public async Task<Guid?> StartSessionAsync(string sessionIdentifier, string platform = "Telegram")
{
if (string.IsNullOrEmpty(_botKey)) return null;
try
{
var apiUrl = _configuration["LittleShop:ApiUrl"];
var sessionData = new
{
SessionIdentifier = sessionIdentifier,
Platform = platform,
Language = "en",
Country = "",
IsAnonymous = true,
Metadata = new Dictionary<string, object>()
};
var json = JsonSerializer.Serialize(sessionData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
var response = await _httpClient.PostAsync($"{apiUrl}/api/bots/sessions/start", content);
if (response.IsSuccessStatusCode)
{
var responseJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<SessionDto>(responseJson);
return result?.Id;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start session");
}
return null;
}
public async Task UpdateSessionAsync(Guid sessionId, int? orderCount = null, int? messageCount = null, decimal? totalSpent = null)
{
if (string.IsNullOrEmpty(_botKey)) return;
try
{
var apiUrl = _configuration["LittleShop:ApiUrl"];
var updateData = new
{
OrderCount = orderCount,
MessageCount = messageCount,
TotalSpent = totalSpent
};
var json = JsonSerializer.Serialize(updateData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
await _httpClient.PutAsync($"{apiUrl}/api/bots/sessions/{sessionId}", content);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update session");
}
}
private int GetMetricType(string metricName)
{
return metricName.ToLower() switch
{
"message" => 4,
"order" => 2,
"error" => 6,
"command" => 5,
_ => 7 // ApiCall
};
}
private async void SyncSettingsWithBotUpdate(object? state)
{
try
{
var settings = await GetSettingsAsync();
if (settings != null && settings.ContainsKey("telegram"))
{
if (settings["telegram"] is JsonElement telegramElement)
{
var telegramSettings = telegramElement.EnumerateObject().ToDictionary(p => p.Name, p => p.Value.ToString());
if (telegramSettings.TryGetValue("botToken", out var token))
{
// Check if token has changed
if (!string.IsNullOrEmpty(token) && token != _lastKnownBotToken)
{
_logger.LogInformation("Bot token has changed. Updating bot...");
_lastKnownBotToken = token;
// Update the TelegramBotService if available
if (_telegramBotService != null)
{
await _telegramBotService.UpdateBotTokenAsync(token);
}
}
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to sync settings with bot update");
}
}
public void Dispose()
{
_heartbeatTimer?.Dispose();
_metricsTimer?.Dispose();
_settingsSyncTimer?.Dispose();
}
private async Task<string?> GetTelegramBotUsernameAsync()
{
try
{
var botToken = _configuration["Telegram:BotToken"];
if (string.IsNullOrEmpty(botToken) || botToken == "YOUR_BOT_TOKEN_HERE")
{
_logger.LogWarning("Bot token not configured in appsettings.json");
return null;
}
// Call Telegram API to get bot info
var response = await _httpClient.GetAsync($"https://api.telegram.org/bot{botToken}/getMe");
if (response.IsSuccessStatusCode)
{
var responseJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<TelegramGetMeResponse>(responseJson);
return result?.Result?.Username;
}
else
{
_logger.LogWarning("Failed to get bot info from Telegram: {StatusCode}", response.StatusCode);
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting Telegram bot username");
return null;
}
}
private async Task<BotDto?> FindExistingBotByPlatformAsync(string platformUsername)
{
try
{
var apiUrl = _configuration["LittleShop:ApiUrl"];
const int telegramBotType = 0; // BotType.Telegram enum value
var response = await _httpClient.GetAsync($"{apiUrl}/api/bots/by-platform/{telegramBotType}/{platformUsername}");
if (response.IsSuccessStatusCode)
{
var responseJson = await response.Content.ReadAsStringAsync();
var bot = JsonSerializer.Deserialize<BotDto>(responseJson);
return bot;
}
else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null; // Bot not found - this is expected for first registration
}
else
{
_logger.LogWarning("Failed to check for existing bot: {StatusCode}", response.StatusCode);
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error finding existing bot by platform username");
return null;
}
}
private async Task UpdatePlatformInfoAsync()
{
try
{
var apiUrl = _configuration["LittleShop:ApiUrl"];
var botToken = _configuration["Telegram:BotToken"];
if (string.IsNullOrEmpty(botToken) || string.IsNullOrEmpty(_botKey))
return;
// Get bot info from Telegram
var telegramResponse = await _httpClient.GetAsync($"https://api.telegram.org/bot{botToken}/getMe");
if (!telegramResponse.IsSuccessStatusCode)
return;
var telegramJson = await telegramResponse.Content.ReadAsStringAsync();
var telegramResult = JsonSerializer.Deserialize<TelegramGetMeResponse>(telegramJson);
if (telegramResult?.Result == null)
return;
// Update platform info in LittleShop
var updateData = new
{
PlatformUsername = telegramResult.Result.Username,
PlatformDisplayName = telegramResult.Result.FirstName ?? telegramResult.Result.Username,
PlatformId = telegramResult.Result.Id.ToString()
};
var json = JsonSerializer.Serialize(updateData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
var response = await _httpClient.PutAsync($"{apiUrl}/api/bots/platform-info", content);
if (response.IsSuccessStatusCode)
{
_logger.LogInformation("Updated platform info for @{Username}", telegramResult.Result.Username);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update platform info");
}
}
// DTOs for API responses
private class BotRegistrationResponse
{
public Guid BotId { get; set; }
public string BotKey { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}
private class BotDto
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string BotKey { get; set; } = string.Empty;
}
private class SessionDto
{
public Guid Id { get; set; }
}
private class TelegramGetMeResponse
{
public bool Ok { get; set; }
public TelegramBotInfo? Result { get; set; }
}
private class TelegramBotInfo
{
public long Id { get; set; }
public bool IsBot { get; set; }
public string FirstName { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public bool? CanJoinGroups { get; set; }
public bool? CanReadAllGroupMessages { get; set; }
public bool? SupportsInlineQueries { get; set; }
}
}
}