604 lines
23 KiB
C#
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; }
|
|
}
|
|
}
|
|
} |