littleshop/TeleBot/TeleBot/TelegramBotService.cs
SysAdmin 86f19ba044
All checks were successful
Build and Deploy LittleShop / Deploy to Pre-Production (CT109) (push) Successful in 59s
Build and Deploy LittleShop / Deploy to Production VPS (Manual Only) (push) Has been skipped
feat: Add AlexHost deployment pipeline and bot control functionality
- Add Gitea Actions workflow for manual AlexHost deployment
- Add docker-compose.alexhost.yml for production deployment
- Add deploy-alexhost.sh script with server-side build support
- Add Bot Control feature (Start/Stop/Restart) for remote bot management
- Add discovery control endpoint in TeleBot
- Update TeleBot with StartPollingAsync/StopPolling/RestartPollingAsync
- Fix platform architecture issues by building on target server
- Update docker-compose configurations for all environments

Deployment tested successfully:
- TeleShop: healthy at https://teleshop.silentmary.mywire.org
- TeleBot: healthy with discovery integration
- SilverPay: connectivity verified

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 12:33:46 +00:00

421 lines
16 KiB
C#

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types;
using Telegram.Bot.Exceptions;
using Telegram.Bot.Types.Enums;
using TeleBot.Handlers;
using TeleBot.Services;
using TeleBot.Http;
namespace TeleBot
{
public class TelegramBotService : IHostedService
{
private readonly IConfiguration _configuration;
private readonly ILogger<TelegramBotService> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly ICommandHandler _commandHandler;
private readonly ICallbackHandler _callbackHandler;
private readonly IMessageHandler _messageHandler;
private readonly IMessageDeliveryService _messageDeliveryService;
private readonly BotManagerService _botManagerService;
private ITelegramBotClient? _botClient;
private CancellationTokenSource? _cancellationTokenSource;
private string? _currentBotToken;
private bool _isRunning;
/// <summary>
/// Indicates whether the Telegram bot polling is currently running
/// </summary>
public bool IsRunning => _isRunning && _botClient != null;
public TelegramBotService(
IConfiguration configuration,
ILogger<TelegramBotService> logger,
IServiceProvider serviceProvider,
ICommandHandler commandHandler,
ICallbackHandler callbackHandler,
IMessageHandler messageHandler,
IMessageDeliveryService messageDeliveryService,
BotManagerService botManagerService)
{
_configuration = configuration;
_logger = logger;
_serviceProvider = serviceProvider;
_commandHandler = commandHandler;
_callbackHandler = callbackHandler;
_messageHandler = messageHandler;
_messageDeliveryService = messageDeliveryService;
_botManagerService = botManagerService;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
// Try to get bot token from API first via BotManagerService
var botToken = await GetBotTokenAsync();
// Fallback to configuration if API doesn't provide token
if (string.IsNullOrEmpty(botToken))
{
botToken = _configuration["Telegram:BotToken"];
}
if (string.IsNullOrEmpty(botToken) || botToken == "YOUR_BOT_TOKEN_HERE")
{
_logger.LogError("Bot token not configured. Please either register via admin panel or set Telegram:BotToken in appsettings.json");
return;
}
_currentBotToken = botToken;
// Configure TelegramBotClient with TOR support if enabled
var torEnabled = _configuration.GetValue<bool>("Privacy:EnableTor");
if (torEnabled)
{
var torSocksHost = _configuration.GetValue<string>("Privacy:TorSocksHost") ?? "127.0.0.1";
var torSocksPort = _configuration.GetValue<int>("Privacy:TorSocksPort", 9050);
var proxyUri = $"socks5://{torSocksHost}:{torSocksPort}";
_logger.LogInformation("Telegram Bot API: Using SOCKS5 proxy at {ProxyUri}", proxyUri);
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy(proxyUri)
{
BypassProxyOnLocal = false,
UseDefaultCredentials = false
},
UseProxy = true,
AllowAutoRedirect = false,
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2)
};
var httpClient = new HttpClient(handler);
_botClient = new TelegramBotClient(botToken, httpClient);
}
else
{
_logger.LogWarning("Telegram Bot API: TOR is DISABLED - bot location will be exposed");
_botClient = new TelegramBotClient(botToken);
}
_cancellationTokenSource = new CancellationTokenSource();
var receiverOptions = new ReceiverOptions
{
AllowedUpdates = Array.Empty<UpdateType>(),
ThrowPendingUpdates = true
};
_botClient.StartReceiving(
HandleUpdateAsync,
HandleErrorAsync,
receiverOptions,
cancellationToken: _cancellationTokenSource.Token
);
_isRunning = true;
var me = await _botClient.GetMeAsync(cancellationToken);
_logger.LogInformation("Bot started: @{Username} ({Id})", me.Username, me.Id);
// Set bot client for message delivery service
if (_messageDeliveryService is MessageDeliveryService deliveryService)
{
deliveryService.SetBotClient(_botClient);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_isRunning = false;
_cancellationTokenSource?.Cancel();
_logger.LogInformation("Bot stopped");
return Task.CompletedTask;
}
private async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
{
try
{
if (update.Type == UpdateType.Message && update.Message != null)
{
var message = update.Message;
// Handle commands
if (message.Text != null && message.Text.StartsWith("/"))
{
var parts = message.Text.Split(' ', 2);
var command = parts[0].ToLower();
var args = parts.Length > 1 ? parts[1] : null;
await _commandHandler.HandleCommandAsync(botClient, message, command, args);
}
else
{
// Handle regular messages (for checkout flow, etc.)
await _messageHandler.HandleMessageAsync(botClient, message);
}
}
else if (update.Type == UpdateType.CallbackQuery && update.CallbackQuery != null)
{
await _callbackHandler.HandleCallbackAsync(botClient, update.CallbackQuery);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling update {UpdateId}", update.Id);
}
}
private Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken)
{
var errorMessage = exception switch
{
ApiRequestException apiException => $"Telegram API Error: [{apiException.ErrorCode}] {apiException.Message}",
_ => exception.ToString()
};
_logger.LogError(exception, "Bot error: {ErrorMessage}", errorMessage);
return Task.CompletedTask;
}
private async Task<string?> GetBotTokenAsync()
{
try
{
// Check if we have a bot key stored
var botKey = _configuration["BotManager:ApiKey"];
if (string.IsNullOrEmpty(botKey))
{
_logger.LogInformation("No bot key configured. Bot will need to register first or use local token.");
return null;
}
// Fetch settings from API
var settings = await _botManagerService.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))
{
_logger.LogInformation("Bot token fetched from admin panel successfully");
return token;
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch bot token from API. Will use local configuration.");
}
return null;
}
/// <summary>
/// Start Telegram polling (if not already running)
/// </summary>
public async Task<bool> StartPollingAsync()
{
if (_isRunning)
{
_logger.LogWarning("Bot polling is already running");
return false;
}
if (string.IsNullOrEmpty(_currentBotToken))
{
_currentBotToken = _configuration["Telegram:BotToken"];
}
if (string.IsNullOrEmpty(_currentBotToken) || _currentBotToken == "YOUR_BOT_TOKEN_HERE")
{
_logger.LogError("Cannot start: No bot token configured");
return false;
}
try
{
// Create bot client with TOR support if enabled
var torEnabled = _configuration.GetValue<bool>("Privacy:EnableTor");
if (torEnabled)
{
var torSocksHost = _configuration.GetValue<string>("Privacy:TorSocksHost") ?? "127.0.0.1";
var torSocksPort = _configuration.GetValue<int>("Privacy:TorSocksPort", 9050);
var proxyUri = $"socks5://{torSocksHost}:{torSocksPort}";
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy(proxyUri)
{
BypassProxyOnLocal = false,
UseDefaultCredentials = false
},
UseProxy = true,
AllowAutoRedirect = false,
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2)
};
var httpClient = new HttpClient(handler);
_botClient = new TelegramBotClient(_currentBotToken, httpClient);
}
else
{
_botClient = new TelegramBotClient(_currentBotToken);
}
_cancellationTokenSource = new CancellationTokenSource();
var receiverOptions = new ReceiverOptions
{
AllowedUpdates = Array.Empty<UpdateType>(),
ThrowPendingUpdates = true
};
_botClient.StartReceiving(
HandleUpdateAsync,
HandleErrorAsync,
receiverOptions,
cancellationToken: _cancellationTokenSource.Token
);
_isRunning = true;
var me = await _botClient.GetMeAsync();
_logger.LogInformation("Bot polling started: @{Username} ({Id})", me.Username, me.Id);
// Update message delivery service
if (_messageDeliveryService is MessageDeliveryService deliveryService)
{
deliveryService.SetBotClient(_botClient);
}
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start bot polling");
_isRunning = false;
return false;
}
}
/// <summary>
/// Stop Telegram polling
/// </summary>
public void StopPolling()
{
if (!_isRunning)
{
_logger.LogWarning("Bot polling is not running");
return;
}
_cancellationTokenSource?.Cancel();
_isRunning = false;
_logger.LogInformation("Bot polling stopped");
}
/// <summary>
/// Restart Telegram polling
/// </summary>
public async Task<bool> RestartPollingAsync()
{
_logger.LogInformation("Restarting bot polling...");
StopPolling();
// Brief pause to ensure clean shutdown
await Task.Delay(500);
return await StartPollingAsync();
}
public async Task UpdateBotTokenAsync(string newToken)
{
// If bot wasn't started or token changed, start/restart
if (_currentBotToken != newToken || _botClient == null)
{
_logger.LogInformation("Starting/updating bot with new token...");
// Stop current bot if running
if (_botClient != null)
{
_cancellationTokenSource?.Cancel();
}
// Create new bot client with new token and TOR support
_currentBotToken = newToken;
var torEnabled = _configuration.GetValue<bool>("Privacy:EnableTor");
if (torEnabled)
{
var torSocksHost = _configuration.GetValue<string>("Privacy:TorSocksHost") ?? "127.0.0.1";
var torSocksPort = _configuration.GetValue<int>("Privacy:TorSocksPort", 9050);
var proxyUri = $"socks5://{torSocksHost}:{torSocksPort}";
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy(proxyUri)
{
BypassProxyOnLocal = false,
UseDefaultCredentials = false
},
UseProxy = true,
AllowAutoRedirect = false,
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2)
};
var httpClient = new HttpClient(handler);
_botClient = new TelegramBotClient(newToken, httpClient);
}
else
{
_botClient = new TelegramBotClient(newToken);
}
_cancellationTokenSource = new CancellationTokenSource();
var receiverOptions = new ReceiverOptions
{
AllowedUpdates = Array.Empty<UpdateType>(),
ThrowPendingUpdates = true
};
_botClient.StartReceiving(
HandleUpdateAsync,
HandleErrorAsync,
receiverOptions,
cancellationToken: _cancellationTokenSource.Token
);
_isRunning = true;
var me = await _botClient.GetMeAsync();
_logger.LogInformation("Bot restarted with new token: @{Username} ({Id})", me.Username, me.Id);
// Update message delivery service
if (_messageDeliveryService is MessageDeliveryService deliveryService)
{
deliveryService.SetBotClient(_botClient);
}
}
}
}
}