feat: Add Remote TeleBot Discovery & Configuration
- Add discovery API endpoints to TeleBot (probe, initialize, configure, status) - Add LivenessService for LittleShop connectivity monitoring with 5min shutdown - Add BotDiscoveryService to LittleShop for remote bot management - Add Admin UI: DiscoverRemote wizard, RepushConfig page, status badges - Add remote discovery fields to Bot model (RemoteAddress, RemotePort, etc.) - Add CheckRemoteStatus and RepushConfig controller actions - Update Index/Details views to show remote bot indicators - Shared secret authentication for discovery, BotKey for post-init 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
255
TeleBot/TeleBot/Controllers/DiscoveryController.cs
Normal file
255
TeleBot/TeleBot/Controllers/DiscoveryController.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TeleBot.DTOs;
|
||||
using TeleBot.Services;
|
||||
|
||||
namespace TeleBot.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// API controller for remote discovery and configuration from LittleShop.
|
||||
/// Enables server-initiated bot registration and configuration.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DiscoveryController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<DiscoveryController> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly BotManagerService _botManagerService;
|
||||
private readonly TelegramBotService _telegramBotService;
|
||||
|
||||
public DiscoveryController(
|
||||
ILogger<DiscoveryController> logger,
|
||||
IConfiguration configuration,
|
||||
BotManagerService botManagerService,
|
||||
TelegramBotService telegramBotService)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
_botManagerService = botManagerService;
|
||||
_telegramBotService = telegramBotService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Probe endpoint for LittleShop to discover this TeleBot instance.
|
||||
/// Returns current status and configuration state.
|
||||
/// </summary>
|
||||
[HttpGet("probe")]
|
||||
public IActionResult Probe()
|
||||
{
|
||||
// Validate discovery secret
|
||||
if (!ValidateDiscoverySecret())
|
||||
{
|
||||
_logger.LogWarning("Discovery probe rejected: invalid or missing X-Discovery-Secret");
|
||||
return Unauthorized(new { error = "Invalid discovery secret" });
|
||||
}
|
||||
|
||||
_logger.LogInformation("Discovery probe received from {RemoteIp}", GetRemoteIp());
|
||||
|
||||
var response = new DiscoveryProbeResponse
|
||||
{
|
||||
InstanceId = _botManagerService.InstanceId,
|
||||
Name = _configuration["BotInfo:Name"] ?? "TeleBot",
|
||||
Version = _configuration["BotInfo:Version"] ?? "1.0.0",
|
||||
Status = _botManagerService.CurrentStatus,
|
||||
HasToken = _botManagerService.HasBotToken,
|
||||
IsConfigured = _botManagerService.IsConfigured,
|
||||
IsInitialized = _botManagerService.IsInitialized,
|
||||
TelegramUsername = _botManagerService.TelegramUsername,
|
||||
Timestamp = DateTime.UtcNow
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize this TeleBot instance with a BotKey from LittleShop.
|
||||
/// This is the first step after discovery - assigns the bot to LittleShop.
|
||||
/// </summary>
|
||||
[HttpPost("initialize")]
|
||||
public async Task<IActionResult> Initialize([FromBody] DiscoveryInitializeRequest request)
|
||||
{
|
||||
// Validate discovery secret
|
||||
if (!ValidateDiscoverySecret())
|
||||
{
|
||||
_logger.LogWarning("Discovery initialize rejected: invalid or missing X-Discovery-Secret");
|
||||
return Unauthorized(new { error = "Invalid discovery secret" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.BotKey))
|
||||
{
|
||||
return BadRequest(new DiscoveryInitializeResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "BotKey is required"
|
||||
});
|
||||
}
|
||||
|
||||
_logger.LogInformation("Initializing TeleBot from LittleShop discovery. Remote IP: {RemoteIp}", GetRemoteIp());
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _botManagerService.InitializeFromDiscoveryAsync(
|
||||
request.BotKey,
|
||||
request.WebhookSecret,
|
||||
request.LittleShopUrl);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
_logger.LogInformation("TeleBot initialized successfully with BotKey");
|
||||
return Ok(new DiscoveryInitializeResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "TeleBot initialized successfully",
|
||||
InstanceId = _botManagerService.InstanceId
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("TeleBot initialization failed: {Message}", result.Message);
|
||||
return BadRequest(new DiscoveryInitializeResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = result.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during TeleBot initialization");
|
||||
return StatusCode(500, new DiscoveryInitializeResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "Internal server error during initialization"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure this TeleBot instance with Telegram credentials.
|
||||
/// Requires prior initialization (valid BotKey).
|
||||
/// </summary>
|
||||
[HttpPost("configure")]
|
||||
public async Task<IActionResult> Configure([FromBody] DiscoveryConfigureRequest request)
|
||||
{
|
||||
// After initialization, use X-Bot-Key for authentication
|
||||
if (!ValidateBotKey())
|
||||
{
|
||||
_logger.LogWarning("Discovery configure rejected: invalid or missing X-Bot-Key");
|
||||
return Unauthorized(new { error = "Invalid bot key" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.BotToken))
|
||||
{
|
||||
return BadRequest(new DiscoveryConfigureResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "BotToken is required"
|
||||
});
|
||||
}
|
||||
|
||||
_logger.LogInformation("Configuring TeleBot with Telegram credentials");
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _botManagerService.ApplyRemoteConfigurationAsync(
|
||||
request.BotToken,
|
||||
request.Settings);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
_logger.LogInformation("TeleBot configured successfully. Telegram: @{Username}", result.TelegramUsername);
|
||||
return Ok(new DiscoveryConfigureResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "TeleBot configured and operational",
|
||||
TelegramUsername = result.TelegramUsername,
|
||||
TelegramDisplayName = result.TelegramDisplayName,
|
||||
TelegramId = result.TelegramId
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("TeleBot configuration failed: {Message}", result.Message);
|
||||
return BadRequest(new DiscoveryConfigureResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = result.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during TeleBot configuration");
|
||||
return StatusCode(500, new DiscoveryConfigureResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "Internal server error during configuration"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current status of the bot (requires BotKey after initialization)
|
||||
/// </summary>
|
||||
[HttpGet("status")]
|
||||
public IActionResult Status()
|
||||
{
|
||||
// Allow both discovery secret (pre-init) and bot key (post-init)
|
||||
if (!ValidateDiscoverySecret() && !ValidateBotKey())
|
||||
{
|
||||
return Unauthorized(new { error = "Invalid credentials" });
|
||||
}
|
||||
|
||||
return Ok(new BotStatusUpdate
|
||||
{
|
||||
Status = _botManagerService.CurrentStatus,
|
||||
IsOperational = _botManagerService.IsConfigured && _telegramBotService.IsRunning,
|
||||
ActiveSessions = _botManagerService.ActiveSessionCount,
|
||||
LastActivityAt = _botManagerService.LastActivityAt,
|
||||
Metadata = new Dictionary<string, object>
|
||||
{
|
||||
["instanceId"] = _botManagerService.InstanceId,
|
||||
["version"] = _configuration["BotInfo:Version"] ?? "1.0.0",
|
||||
["telegramUsername"] = _botManagerService.TelegramUsername ?? "",
|
||||
["hasToken"] = _botManagerService.HasBotToken,
|
||||
["isInitialized"] = _botManagerService.IsInitialized
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool ValidateDiscoverySecret()
|
||||
{
|
||||
var providedSecret = Request.Headers["X-Discovery-Secret"].ToString();
|
||||
var expectedSecret = _configuration["Discovery:Secret"];
|
||||
|
||||
if (string.IsNullOrEmpty(expectedSecret))
|
||||
{
|
||||
_logger.LogWarning("Discovery secret not configured in appsettings.json");
|
||||
return false;
|
||||
}
|
||||
|
||||
return !string.IsNullOrEmpty(providedSecret) &&
|
||||
string.Equals(providedSecret, expectedSecret, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private bool ValidateBotKey()
|
||||
{
|
||||
var providedKey = Request.Headers["X-Bot-Key"].ToString();
|
||||
var storedKey = _botManagerService.BotKey;
|
||||
|
||||
if (string.IsNullOrEmpty(storedKey))
|
||||
{
|
||||
return false; // Not initialized yet
|
||||
}
|
||||
|
||||
return !string.IsNullOrEmpty(providedKey) &&
|
||||
string.Equals(providedKey, storedKey, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private string GetRemoteIp()
|
||||
{
|
||||
return HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
}
|
||||
}
|
||||
137
TeleBot/TeleBot/DTOs/DiscoveryDtos.cs
Normal file
137
TeleBot/TeleBot/DTOs/DiscoveryDtos.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace TeleBot.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Response returned when LittleShop probes this TeleBot instance
|
||||
/// </summary>
|
||||
public class DiscoveryProbeResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this TeleBot instance (generated on first startup)
|
||||
/// </summary>
|
||||
public string InstanceId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Configured name of this bot
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// TeleBot version
|
||||
/// </summary>
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Current operational status
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "Bootstrap";
|
||||
|
||||
/// <summary>
|
||||
/// Whether a Telegram bot token has been configured
|
||||
/// </summary>
|
||||
public bool HasToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the bot is fully configured and operational
|
||||
/// </summary>
|
||||
public bool IsConfigured { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this instance has been initialized (has BotKey)
|
||||
/// </summary>
|
||||
public bool IsInitialized { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Telegram username if configured and operational
|
||||
/// </summary>
|
||||
public string? TelegramUsername { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of probe response
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to initialize this TeleBot instance from LittleShop
|
||||
/// </summary>
|
||||
public class DiscoveryInitializeRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Bot key assigned by LittleShop for authentication
|
||||
/// </summary>
|
||||
public string BotKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Secret for webhook authentication
|
||||
/// </summary>
|
||||
public string WebhookSecret { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// LittleShop API URL (if different from discovery source)
|
||||
/// </summary>
|
||||
public string? LittleShopUrl { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response after initialization
|
||||
/// </summary>
|
||||
public class DiscoveryInitializeResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public string? InstanceId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to configure this TeleBot instance with Telegram credentials
|
||||
/// </summary>
|
||||
public class DiscoveryConfigureRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Telegram Bot token from BotFather
|
||||
/// </summary>
|
||||
public string BotToken { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Additional settings to apply
|
||||
/// </summary>
|
||||
public Dictionary<string, object>? Settings { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response after configuration
|
||||
/// </summary>
|
||||
public class DiscoveryConfigureResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Telegram bot username (e.g., @MyBot)
|
||||
/// </summary>
|
||||
public string? TelegramUsername { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Telegram bot display name
|
||||
/// </summary>
|
||||
public string? TelegramDisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Telegram bot ID
|
||||
/// </summary>
|
||||
public string? TelegramId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status update sent to indicate bot operational state
|
||||
/// </summary>
|
||||
public class BotStatusUpdate
|
||||
{
|
||||
public string Status { get; set; } = "Unknown";
|
||||
public bool IsOperational { get; set; }
|
||||
public int ActiveSessions { get; set; }
|
||||
public DateTime LastActivityAt { get; set; }
|
||||
public Dictionary<string, object>? Metadata { get; set; }
|
||||
}
|
||||
@@ -106,6 +106,16 @@ builder.Services.AddHttpClient<BotManagerService>()
|
||||
builder.Services.AddSingleton<BotManagerService>();
|
||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<BotManagerService>());
|
||||
|
||||
// Liveness Service - Monitors LittleShop connectivity and triggers shutdown on failure
|
||||
builder.Services.AddHttpClient<LivenessService>()
|
||||
.ConfigurePrimaryHttpMessageHandler(sp =>
|
||||
{
|
||||
var logger = sp.GetRequiredService<ILoggerFactory>().CreateLogger("Liveness");
|
||||
return Socks5HttpHandler.CreateDirect(logger);
|
||||
});
|
||||
builder.Services.AddSingleton<LivenessService>();
|
||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<LivenessService>());
|
||||
|
||||
// Message Delivery Service - Single instance
|
||||
builder.Services.AddSingleton<MessageDeliveryService>();
|
||||
builder.Services.AddSingleton<IMessageDeliveryService>(sp => sp.GetRequiredService<MessageDeliveryService>());
|
||||
@@ -155,6 +165,8 @@ try
|
||||
Log.Information("Privacy Mode: {PrivacyMode}", builder.Configuration["Privacy:Mode"]);
|
||||
Log.Information("Ephemeral by Default: {Ephemeral}", builder.Configuration["Privacy:EphemeralByDefault"]);
|
||||
Log.Information("Tor Enabled: {Tor}", builder.Configuration["Privacy:EnableTor"]);
|
||||
Log.Information("Discovery endpoint: GET /api/discovery/probe");
|
||||
Log.Information("LittleShop API: {ApiUrl}", builder.Configuration["LittleShop:ApiUrl"]);
|
||||
Log.Information("Webhook endpoints available at /api/webhook");
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
200
TeleBot/TeleBot/Services/LivenessService.cs
Normal file
200
TeleBot/TeleBot/Services/LivenessService.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace TeleBot.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Background service that monitors LittleShop connectivity.
|
||||
/// Triggers application shutdown after consecutive connectivity failures.
|
||||
/// </summary>
|
||||
public class LivenessService : BackgroundService
|
||||
{
|
||||
private readonly ILogger<LivenessService> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IHostApplicationLifetime _applicationLifetime;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly BotManagerService _botManagerService;
|
||||
|
||||
private int _consecutiveFailures;
|
||||
private DateTime? _firstFailureAt;
|
||||
|
||||
public LivenessService(
|
||||
ILogger<LivenessService> logger,
|
||||
IConfiguration configuration,
|
||||
IHostApplicationLifetime applicationLifetime,
|
||||
HttpClient httpClient,
|
||||
BotManagerService botManagerService)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
_applicationLifetime = applicationLifetime;
|
||||
_httpClient = httpClient;
|
||||
_botManagerService = botManagerService;
|
||||
_consecutiveFailures = 0;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("LivenessService started");
|
||||
|
||||
// Wait for bot to be initialized before starting liveness checks
|
||||
while (!_botManagerService.IsInitialized && !stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
||||
}
|
||||
|
||||
if (stoppingToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
_logger.LogInformation("Bot initialized, starting LittleShop connectivity monitoring");
|
||||
|
||||
var checkIntervalSeconds = _configuration.GetValue<int>("Liveness:CheckIntervalSeconds", 30);
|
||||
var failureThreshold = _configuration.GetValue<int>("Liveness:FailureThreshold", 10);
|
||||
|
||||
_logger.LogInformation("Liveness configuration: CheckInterval={CheckInterval}s, FailureThreshold={Threshold}",
|
||||
checkIntervalSeconds, failureThreshold);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(checkIntervalSeconds), stoppingToken);
|
||||
|
||||
var isConnected = await CheckLittleShopConnectivityAsync(stoppingToken);
|
||||
|
||||
if (isConnected)
|
||||
{
|
||||
if (_consecutiveFailures > 0)
|
||||
{
|
||||
_logger.LogInformation("LittleShop connectivity restored after {Failures} failures", _consecutiveFailures);
|
||||
}
|
||||
_consecutiveFailures = 0;
|
||||
_firstFailureAt = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_consecutiveFailures++;
|
||||
_firstFailureAt ??= DateTime.UtcNow;
|
||||
|
||||
var totalDowntime = DateTime.UtcNow - _firstFailureAt.Value;
|
||||
|
||||
if (_consecutiveFailures >= failureThreshold)
|
||||
{
|
||||
_logger.LogCritical(
|
||||
"LittleShop unreachable for {Downtime:F0} seconds ({Failures} consecutive failures). Initiating shutdown.",
|
||||
totalDowntime.TotalSeconds, _consecutiveFailures);
|
||||
|
||||
// Trigger application shutdown
|
||||
_applicationLifetime.StopApplication();
|
||||
return;
|
||||
}
|
||||
else if (_consecutiveFailures == 1)
|
||||
{
|
||||
_logger.LogWarning("LittleShop connectivity check failed. Failure 1/{Threshold}", failureThreshold);
|
||||
}
|
||||
else if (_consecutiveFailures % 3 == 0) // Log every 3rd failure to avoid spam
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"LittleShop connectivity check failed. Failure {Failures}/{Threshold}. Downtime: {Downtime:F0}s",
|
||||
_consecutiveFailures, failureThreshold, totalDowntime.TotalSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Normal shutdown
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during liveness check");
|
||||
_consecutiveFailures++;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("LivenessService stopped");
|
||||
}
|
||||
|
||||
private async Task<bool> CheckLittleShopConnectivityAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
||||
if (string.IsNullOrEmpty(apiUrl))
|
||||
{
|
||||
_logger.LogWarning("LittleShop:ApiUrl not configured");
|
||||
return false;
|
||||
}
|
||||
|
||||
var botKey = _botManagerService.BotKey;
|
||||
if (string.IsNullOrEmpty(botKey))
|
||||
{
|
||||
// Not initialized yet, skip check
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use the health endpoint or a lightweight endpoint
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"{apiUrl}/health");
|
||||
request.Headers.Add("X-Bot-Key", botKey);
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(10)); // 10 second timeout
|
||||
|
||||
var response = await _httpClient.SendAsync(request, cts.Token);
|
||||
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Timeout
|
||||
_logger.LogDebug("LittleShop connectivity check timed out");
|
||||
return false;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogDebug("LittleShop connectivity check failed: {Message}", ex.Message);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "LittleShop connectivity check error");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current liveness status
|
||||
/// </summary>
|
||||
public LivenessStatus GetStatus()
|
||||
{
|
||||
var failureThreshold = _configuration.GetValue<int>("Liveness:FailureThreshold", 10);
|
||||
|
||||
return new LivenessStatus
|
||||
{
|
||||
IsHealthy = _consecutiveFailures == 0,
|
||||
ConsecutiveFailures = _consecutiveFailures,
|
||||
FailureThreshold = failureThreshold,
|
||||
FirstFailureAt = _firstFailureAt,
|
||||
DowntimeSeconds = _firstFailureAt.HasValue
|
||||
? (DateTime.UtcNow - _firstFailureAt.Value).TotalSeconds
|
||||
: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the current liveness status
|
||||
/// </summary>
|
||||
public class LivenessStatus
|
||||
{
|
||||
public bool IsHealthy { get; set; }
|
||||
public int ConsecutiveFailures { get; set; }
|
||||
public int FailureThreshold { get; set; }
|
||||
public DateTime? FirstFailureAt { get; set; }
|
||||
public double DowntimeSeconds { get; set; }
|
||||
}
|
||||
@@ -33,6 +33,12 @@ namespace TeleBot
|
||||
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,
|
||||
@@ -119,7 +125,9 @@ namespace TeleBot
|
||||
receiverOptions,
|
||||
cancellationToken: _cancellationTokenSource.Token
|
||||
);
|
||||
|
||||
|
||||
_isRunning = true;
|
||||
|
||||
var me = await _botClient.GetMeAsync(cancellationToken);
|
||||
_logger.LogInformation("Bot started: @{Username} ({Id})", me.Username, me.Id);
|
||||
|
||||
@@ -132,6 +140,7 @@ namespace TeleBot
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_isRunning = false;
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_logger.LogInformation("Bot stopped");
|
||||
return Task.CompletedTask;
|
||||
@@ -273,6 +282,8 @@ namespace TeleBot
|
||||
cancellationToken: _cancellationTokenSource.Token
|
||||
);
|
||||
|
||||
_isRunning = true;
|
||||
|
||||
var me = await _botClient.GetMeAsync();
|
||||
_logger.LogInformation("Bot restarted with new token: @{Username} ({Id})", me.Username, me.Id);
|
||||
|
||||
|
||||
@@ -2,18 +2,29 @@
|
||||
"BotInfo": {
|
||||
"Name": "LittleShop TeleBot",
|
||||
"Description": "Privacy-focused e-commerce Telegram bot",
|
||||
"Version": "1.0.0"
|
||||
"Version": "1.0.0",
|
||||
"InstanceId": ""
|
||||
},
|
||||
"BotManager": {
|
||||
"ApiKey": "",
|
||||
"Comment": "This will be populated after first registration with admin panel"
|
||||
"Comment": "Populated by LittleShop during discovery initialization"
|
||||
},
|
||||
"Telegram": {
|
||||
"BotToken": "8496279616:AAE7kV_riICbWxn6-MPFqcrWx7K8b4_NKq0",
|
||||
"AdminChatId": "123456789",
|
||||
"BotToken": "",
|
||||
"AdminChatId": "",
|
||||
"WebhookUrl": "",
|
||||
"UseWebhook": false,
|
||||
"Comment": "Bot token will be fetched from admin panel API if BotManager:ApiKey is set"
|
||||
"Comment": "Bot token pushed from LittleShop during configuration"
|
||||
},
|
||||
"Discovery": {
|
||||
"Secret": "CHANGE_THIS_SHARED_SECRET_32_CHARS",
|
||||
"Enabled": true,
|
||||
"Comment": "Shared secret for LittleShop discovery. Must match LittleShop BotDiscovery:SharedSecret"
|
||||
},
|
||||
"Liveness": {
|
||||
"CheckIntervalSeconds": 30,
|
||||
"FailureThreshold": 10,
|
||||
"Comment": "Shutdown after 10 consecutive failures (5 minutes total)"
|
||||
},
|
||||
"Webhook": {
|
||||
"Secret": "",
|
||||
|
||||
Reference in New Issue
Block a user