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:
parent
cdef6f04e1
commit
521bff2c7d
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -8,6 +9,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using LittleShop.DTOs;
|
using LittleShop.DTOs;
|
||||||
using LittleShop.Enums;
|
using LittleShop.Enums;
|
||||||
using LittleShop.Services;
|
using LittleShop.Services;
|
||||||
|
using LittleShop.Models;
|
||||||
|
|
||||||
namespace LittleShop.Areas.Admin.Controllers;
|
namespace LittleShop.Areas.Admin.Controllers;
|
||||||
|
|
||||||
@ -18,17 +20,20 @@ public class BotsController : Controller
|
|||||||
private readonly IBotService _botService;
|
private readonly IBotService _botService;
|
||||||
private readonly IBotMetricsService _metricsService;
|
private readonly IBotMetricsService _metricsService;
|
||||||
private readonly ITelegramBotManagerService _telegramManager;
|
private readonly ITelegramBotManagerService _telegramManager;
|
||||||
|
private readonly IBotDiscoveryService _discoveryService;
|
||||||
private readonly ILogger<BotsController> _logger;
|
private readonly ILogger<BotsController> _logger;
|
||||||
|
|
||||||
public BotsController(
|
public BotsController(
|
||||||
IBotService botService,
|
IBotService botService,
|
||||||
IBotMetricsService metricsService,
|
IBotMetricsService metricsService,
|
||||||
ITelegramBotManagerService telegramManager,
|
ITelegramBotManagerService telegramManager,
|
||||||
|
IBotDiscoveryService discoveryService,
|
||||||
ILogger<BotsController> logger)
|
ILogger<BotsController> logger)
|
||||||
{
|
{
|
||||||
_botService = botService;
|
_botService = botService;
|
||||||
_metricsService = metricsService;
|
_metricsService = metricsService;
|
||||||
_telegramManager = telegramManager;
|
_telegramManager = telegramManager;
|
||||||
|
_discoveryService = discoveryService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,4 +384,397 @@ public class BotsController : Controller
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Remote Bot Discovery
|
||||||
|
|
||||||
|
// GET: Admin/Bots/DiscoverRemote
|
||||||
|
public IActionResult DiscoverRemote()
|
||||||
|
{
|
||||||
|
return View(new DiscoveryWizardViewModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Admin/Bots/ProbeRemote
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> ProbeRemote(DiscoveryWizardViewModel model)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Probing remote TeleBot at {IpAddress}:{Port}", model.IpAddress, model.Port);
|
||||||
|
|
||||||
|
var result = await _discoveryService.ProbeRemoteBotAsync(model.IpAddress, model.Port);
|
||||||
|
|
||||||
|
if (result.Success && result.ProbeResponse != null)
|
||||||
|
{
|
||||||
|
model.ProbeResponse = result.ProbeResponse;
|
||||||
|
model.BotName = result.ProbeResponse.Name;
|
||||||
|
model.CurrentStep = 2;
|
||||||
|
model.SuccessMessage = "TeleBot discovered successfully!";
|
||||||
|
|
||||||
|
// Auto-select a personality if not already configured
|
||||||
|
if (string.IsNullOrEmpty(model.PersonalityName))
|
||||||
|
{
|
||||||
|
var personalities = new[] { "Alan", "Dave", "Sarah", "Mike", "Emma", "Tom" };
|
||||||
|
model.PersonalityName = personalities[new Random().Next(personalities.Length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
model.ErrorMessage = result.Message;
|
||||||
|
model.CurrentStep = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return View("DiscoverRemote", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Admin/Bots/RegisterRemote
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RegisterRemote(DiscoveryWizardViewModel model)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Registering remote bot: {BotName} at {IpAddress}:{Port}",
|
||||||
|
model.BotName, model.IpAddress, model.Port);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(model.BotName))
|
||||||
|
{
|
||||||
|
model.ErrorMessage = "Bot name is required";
|
||||||
|
model.CurrentStep = 2;
|
||||||
|
return View("DiscoverRemote", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Create the bot in the database
|
||||||
|
var registrationDto = new BotRegistrationDto
|
||||||
|
{
|
||||||
|
Name = model.BotName,
|
||||||
|
Description = model.Description,
|
||||||
|
Type = BotType.Telegram,
|
||||||
|
Version = model.ProbeResponse?.Version ?? "1.0.0",
|
||||||
|
PersonalityName = model.PersonalityName,
|
||||||
|
InitialSettings = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["discovery"] = new { remoteAddress = model.IpAddress, remotePort = model.Port }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var botResult = await _botService.RegisterBotAsync(registrationDto);
|
||||||
|
|
||||||
|
// Update the bot with remote discovery info
|
||||||
|
var bot = await _botService.GetBotByIdAsync(botResult.BotId);
|
||||||
|
if (bot != null)
|
||||||
|
{
|
||||||
|
// Update remote fields directly (we'll need to add this method to IBotService)
|
||||||
|
await UpdateBotRemoteInfoAsync(botResult.BotId, model.IpAddress, model.Port,
|
||||||
|
model.ProbeResponse?.InstanceId, DiscoveryStatus.Discovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the remote TeleBot with the BotKey
|
||||||
|
var initResult = await _discoveryService.InitializeRemoteBotAsync(botResult.BotId, model.IpAddress, model.Port);
|
||||||
|
|
||||||
|
if (initResult.Success)
|
||||||
|
{
|
||||||
|
// Update status to Initialized
|
||||||
|
await UpdateBotRemoteInfoAsync(botResult.BotId, model.IpAddress, model.Port,
|
||||||
|
model.ProbeResponse?.InstanceId, DiscoveryStatus.Initialized);
|
||||||
|
|
||||||
|
model.BotId = botResult.BotId;
|
||||||
|
model.BotKey = botResult.BotKey;
|
||||||
|
model.CurrentStep = 3;
|
||||||
|
model.SuccessMessage = "Bot registered and initialized! Now enter the Telegram bot token.";
|
||||||
|
|
||||||
|
_logger.LogInformation("Remote bot registered and initialized: {BotId}", botResult.BotId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
model.ErrorMessage = $"Bot registered but initialization failed: {initResult.Message}";
|
||||||
|
model.BotId = botResult.BotId;
|
||||||
|
model.BotKey = botResult.BotKey;
|
||||||
|
model.CurrentStep = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return View("DiscoverRemote", model);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to register remote bot");
|
||||||
|
model.ErrorMessage = $"Registration failed: {ex.Message}";
|
||||||
|
model.CurrentStep = 2;
|
||||||
|
return View("DiscoverRemote", model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Admin/Bots/ConfigureRemote
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> ConfigureRemote(DiscoveryWizardViewModel model)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Configuring remote bot {BotId} with Telegram token", model.BotId);
|
||||||
|
|
||||||
|
if (!model.BotId.HasValue)
|
||||||
|
{
|
||||||
|
model.ErrorMessage = "Bot ID is missing";
|
||||||
|
model.CurrentStep = 1;
|
||||||
|
return View("DiscoverRemote", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(model.BotToken))
|
||||||
|
{
|
||||||
|
model.ErrorMessage = "Telegram bot token is required";
|
||||||
|
model.CurrentStep = 3;
|
||||||
|
return View("DiscoverRemote", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the token first
|
||||||
|
if (!await ValidateTelegramToken(model.BotToken))
|
||||||
|
{
|
||||||
|
model.ErrorMessage = "Invalid Telegram bot token. Please check and try again.";
|
||||||
|
model.CurrentStep = 3;
|
||||||
|
return View("DiscoverRemote", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Push configuration to the remote TeleBot
|
||||||
|
var configResult = await _discoveryService.PushConfigurationAsync(model.BotId.Value, model.BotToken);
|
||||||
|
|
||||||
|
if (configResult.Success)
|
||||||
|
{
|
||||||
|
// Update bot settings with the token
|
||||||
|
var bot = await _botService.GetBotByIdAsync(model.BotId.Value);
|
||||||
|
if (bot != null)
|
||||||
|
{
|
||||||
|
var settings = bot.Settings ?? new Dictionary<string, object>();
|
||||||
|
settings["telegram"] = new { botToken = model.BotToken };
|
||||||
|
await _botService.UpdateBotSettingsAsync(model.BotId.Value,
|
||||||
|
new UpdateBotSettingsDto { Settings = settings });
|
||||||
|
|
||||||
|
// Update discovery status to Configured
|
||||||
|
await UpdateBotRemoteInfoAsync(model.BotId.Value,
|
||||||
|
bot.RemoteAddress ?? model.IpAddress,
|
||||||
|
bot.RemotePort ?? model.Port,
|
||||||
|
bot.RemoteInstanceId,
|
||||||
|
DiscoveryStatus.Configured);
|
||||||
|
|
||||||
|
// Activate the bot
|
||||||
|
await _botService.UpdateBotStatusAsync(model.BotId.Value, BotStatus.Active);
|
||||||
|
}
|
||||||
|
|
||||||
|
TempData["Success"] = $"Remote bot configured successfully! Telegram: @{configResult.TelegramUsername}";
|
||||||
|
return RedirectToAction(nameof(Details), new { id = model.BotId.Value });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
model.ErrorMessage = $"Configuration failed: {configResult.Message}";
|
||||||
|
model.CurrentStep = 3;
|
||||||
|
return View("DiscoverRemote", model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to configure remote bot");
|
||||||
|
model.ErrorMessage = $"Configuration failed: {ex.Message}";
|
||||||
|
model.CurrentStep = 3;
|
||||||
|
return View("DiscoverRemote", model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Admin/Bots/CheckRemoteStatus/5
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> CheckRemoteStatus(Guid id)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Checking remote status for bot {BotId}", id);
|
||||||
|
|
||||||
|
var bot = await _botService.GetBotByIdAsync(id);
|
||||||
|
if (bot == null)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Bot not found";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bot.IsRemote)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "This is not a remote bot";
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _discoveryService.ProbeRemoteBotAsync(bot.RemoteAddress!, bot.RemotePort ?? 5000);
|
||||||
|
|
||||||
|
if (result.Success && result.ProbeResponse != null)
|
||||||
|
{
|
||||||
|
// Update discovery status
|
||||||
|
await UpdateBotRemoteInfoAsync(id, bot.RemoteAddress!, bot.RemotePort ?? 5000,
|
||||||
|
result.ProbeResponse.InstanceId, result.ProbeResponse.Status);
|
||||||
|
|
||||||
|
var statusMessage = result.ProbeResponse.IsConfigured
|
||||||
|
? $"Bot is online and configured. Telegram: @{result.ProbeResponse.TelegramUsername}"
|
||||||
|
: "Bot is online but not yet configured with a Telegram token.";
|
||||||
|
|
||||||
|
TempData["Success"] = statusMessage;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Update status to indicate offline
|
||||||
|
await UpdateBotRemoteInfoAsync(id, bot.RemoteAddress!, bot.RemotePort ?? 5000,
|
||||||
|
bot.RemoteInstanceId, DiscoveryStatus.Offline);
|
||||||
|
|
||||||
|
TempData["Error"] = $"Remote bot is not responding: {result.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to check remote status for bot {BotId}", id);
|
||||||
|
TempData["Error"] = $"Failed to check status: {ex.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: Admin/Bots/RepushConfig/5
|
||||||
|
public async Task<IActionResult> RepushConfig(Guid id)
|
||||||
|
{
|
||||||
|
var bot = await _botService.GetBotByIdAsync(id);
|
||||||
|
if (bot == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
if (!bot.IsRemote)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "This is not a remote bot";
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get existing token from settings
|
||||||
|
string? existingToken = null;
|
||||||
|
if (bot.Settings.TryGetValue("telegram", out var telegramObj))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var telegramJson = JsonSerializer.Serialize(telegramObj);
|
||||||
|
var telegramDict = JsonSerializer.Deserialize<Dictionary<string, object>>(telegramJson);
|
||||||
|
if (telegramDict?.TryGetValue("botToken", out var tokenObj) == true)
|
||||||
|
{
|
||||||
|
existingToken = tokenObj?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewData["ExistingToken"] = existingToken;
|
||||||
|
ViewData["HasExistingToken"] = !string.IsNullOrEmpty(existingToken);
|
||||||
|
|
||||||
|
return View(bot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Admin/Bots/RepushConfig/5
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RepushConfig(Guid id, string botToken, bool useExistingToken = false)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Re-pushing configuration to remote bot {BotId}", id);
|
||||||
|
|
||||||
|
var bot = await _botService.GetBotByIdAsync(id);
|
||||||
|
if (bot == null)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Bot not found";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bot.IsRemote)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "This is not a remote bot";
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If using existing token, retrieve it
|
||||||
|
if (useExistingToken)
|
||||||
|
{
|
||||||
|
if (bot.Settings.TryGetValue("telegram", out var telegramObj))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var telegramJson = JsonSerializer.Serialize(telegramObj);
|
||||||
|
var telegramDict = JsonSerializer.Deserialize<Dictionary<string, object>>(telegramJson);
|
||||||
|
if (telegramDict?.TryGetValue("botToken", out var tokenObj) == true)
|
||||||
|
{
|
||||||
|
botToken = tokenObj?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(botToken))
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Bot token is required";
|
||||||
|
return RedirectToAction(nameof(RepushConfig), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the token
|
||||||
|
if (!await ValidateTelegramToken(botToken))
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Invalid Telegram bot token";
|
||||||
|
return RedirectToAction(nameof(RepushConfig), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// First, re-initialize if needed
|
||||||
|
var probeResult = await _discoveryService.ProbeRemoteBotAsync(bot.RemoteAddress!, bot.RemotePort ?? 5000);
|
||||||
|
|
||||||
|
if (!probeResult.Success)
|
||||||
|
{
|
||||||
|
TempData["Error"] = $"Cannot reach remote bot: {probeResult.Message}";
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If bot is not initialized, initialize first
|
||||||
|
if (probeResult.ProbeResponse?.Status == DiscoveryStatus.AwaitingDiscovery ||
|
||||||
|
probeResult.ProbeResponse?.Status == DiscoveryStatus.Discovered)
|
||||||
|
{
|
||||||
|
var initResult = await _discoveryService.InitializeRemoteBotAsync(id, bot.RemoteAddress!, bot.RemotePort ?? 5000);
|
||||||
|
if (!initResult.Success)
|
||||||
|
{
|
||||||
|
TempData["Error"] = $"Failed to initialize remote bot: {initResult.Message}";
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the configuration
|
||||||
|
var configResult = await _discoveryService.PushConfigurationAsync(id, botToken);
|
||||||
|
|
||||||
|
if (configResult.Success)
|
||||||
|
{
|
||||||
|
// Update bot settings with the token
|
||||||
|
var settings = bot.Settings ?? new Dictionary<string, object>();
|
||||||
|
settings["telegram"] = new { botToken = botToken };
|
||||||
|
await _botService.UpdateBotSettingsAsync(id, new UpdateBotSettingsDto { Settings = settings });
|
||||||
|
|
||||||
|
// Update discovery status
|
||||||
|
await UpdateBotRemoteInfoAsync(id, bot.RemoteAddress!, bot.RemotePort ?? 5000,
|
||||||
|
probeResult.ProbeResponse?.InstanceId, DiscoveryStatus.Configured);
|
||||||
|
|
||||||
|
TempData["Success"] = $"Configuration pushed successfully! Telegram: @{configResult.TelegramUsername}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["Error"] = $"Failed to push configuration: {configResult.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to re-push configuration to bot {BotId}", id);
|
||||||
|
TempData["Error"] = $"Failed to push configuration: {ex.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to update bot remote info
|
||||||
|
private async Task UpdateBotRemoteInfoAsync(Guid botId, string ipAddress, int port, string? instanceId, string status)
|
||||||
|
{
|
||||||
|
await _botService.UpdateRemoteInfoAsync(botId, ipAddress, port, instanceId, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
@ -128,6 +128,77 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (Model.IsRemote)
|
||||||
|
{
|
||||||
|
<div class="card mb-3 border-info">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-satellite-dish"></i> Remote Connection</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-4">Remote Address</dt>
|
||||||
|
<dd class="col-sm-8"><code>@Model.RemoteAddress:@Model.RemotePort</code></dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-4">Instance ID</dt>
|
||||||
|
<dd class="col-sm-8"><code>@(Model.RemoteInstanceId ?? "N/A")</code></dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-4">Discovery Status</dt>
|
||||||
|
<dd class="col-sm-8">
|
||||||
|
@switch (Model.DiscoveryStatus)
|
||||||
|
{
|
||||||
|
case "Configured":
|
||||||
|
<span class="badge bg-success">@Model.DiscoveryStatus</span>
|
||||||
|
break;
|
||||||
|
case "Initialized":
|
||||||
|
<span class="badge bg-info">@Model.DiscoveryStatus</span>
|
||||||
|
break;
|
||||||
|
case "Discovered":
|
||||||
|
<span class="badge bg-warning">@Model.DiscoveryStatus</span>
|
||||||
|
break;
|
||||||
|
case "Offline":
|
||||||
|
case "Error":
|
||||||
|
<span class="badge bg-danger">@Model.DiscoveryStatus</span>
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
<span class="badge bg-secondary">@Model.DiscoveryStatus</span>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-4">Last Discovery</dt>
|
||||||
|
<dd class="col-sm-8">
|
||||||
|
@if (Model.LastDiscoveryAt.HasValue)
|
||||||
|
{
|
||||||
|
@Model.LastDiscoveryAt.Value.ToString("yyyy-MM-dd HH:mm:ss")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-muted">Never</span>
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<form asp-area="Admin" asp-controller="Bots" asp-action="CheckRemoteStatus" asp-route-id="@Model.Id" method="post" class="d-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-info">
|
||||||
|
<i class="fas fa-sync"></i> Check Status
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@if (Model.DiscoveryStatus == "Initialized" || Model.DiscoveryStatus == "Configured")
|
||||||
|
{
|
||||||
|
<a href="/Admin/Bots/RepushConfig/@Model.Id" class="btn btn-sm btn-outline-warning">
|
||||||
|
<i class="fas fa-upload"></i> Re-push Config
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">30-Day Metrics Summary</h5>
|
<h5 class="mb-0">30-Day Metrics Summary</h5>
|
||||||
|
|||||||
286
LittleShop/Areas/Admin/Views/Bots/DiscoverRemote.cshtml
Normal file
286
LittleShop/Areas/Admin/Views/Bots/DiscoverRemote.cshtml
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
@model LittleShop.DTOs.DiscoveryWizardViewModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Discover Remote TeleBot";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>Discover Remote TeleBot</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> @Model.ErrorMessage
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(Model.SuccessMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<i class="fas fa-check-circle"></i> @Model.SuccessMessage
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Model.CurrentStep == 1)
|
||||||
|
{
|
||||||
|
<!-- Step 1: Discovery -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-search"></i> Step 1: Discover TeleBot Instance</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted">
|
||||||
|
Enter the IP address and port of the TeleBot instance you want to connect.
|
||||||
|
The TeleBot must be running and configured with the same discovery secret.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form asp-area="Admin" asp-controller="Bots" asp-action="ProbeRemote" method="post">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label for="IpAddress" class="form-label">IP Address / Hostname</label>
|
||||||
|
<input name="IpAddress" id="IpAddress" value="@Model.IpAddress" class="form-control"
|
||||||
|
placeholder="e.g., 192.168.1.100 or telebot.example.com" required />
|
||||||
|
<small class="text-muted">The IP address or hostname where TeleBot is running</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="Port" class="form-label">Port</label>
|
||||||
|
<input name="Port" id="Port" type="number" value="@(Model.Port == 0 ? 5010 : Model.Port)" class="form-control"
|
||||||
|
min="1" max="65535" required />
|
||||||
|
<small class="text-muted">Default: 5010</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex">
|
||||||
|
<button type="submit" class="btn btn-primary me-md-2">
|
||||||
|
<i class="fas fa-satellite-dish"></i> Probe TeleBot
|
||||||
|
</button>
|
||||||
|
<a href="/Admin/Bots" class="btn btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (Model.CurrentStep == 2)
|
||||||
|
{
|
||||||
|
<!-- Step 2: Registration -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-check-circle"></i> TeleBot Discovered!</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<tr>
|
||||||
|
<th width="150">Instance ID:</th>
|
||||||
|
<td><code>@Model.ProbeResponse?.InstanceId</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Name:</th>
|
||||||
|
<td>@Model.ProbeResponse?.Name</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Version:</th>
|
||||||
|
<td>@Model.ProbeResponse?.Version</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status:</th>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-@(Model.ProbeResponse?.Status == "Bootstrap" ? "warning" : "info")">
|
||||||
|
@Model.ProbeResponse?.Status
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Address:</th>
|
||||||
|
<td>@Model.IpAddress:@Model.Port</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-robot"></i> Step 2: Register Bot</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form asp-area="Admin" asp-controller="Bots" asp-action="RegisterRemote" method="post">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
|
||||||
|
<!-- Hidden fields to preserve discovery data -->
|
||||||
|
<input type="hidden" name="IpAddress" value="@Model.IpAddress" />
|
||||||
|
<input type="hidden" name="Port" value="@Model.Port" />
|
||||||
|
<input type="hidden" name="ProbeResponse.InstanceId" value="@Model.ProbeResponse?.InstanceId" />
|
||||||
|
<input type="hidden" name="ProbeResponse.Name" value="@Model.ProbeResponse?.Name" />
|
||||||
|
<input type="hidden" name="ProbeResponse.Version" value="@Model.ProbeResponse?.Version" />
|
||||||
|
<input type="hidden" name="ProbeResponse.Status" value="@Model.ProbeResponse?.Status" />
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="BotName" class="form-label">Bot Name</label>
|
||||||
|
<input name="BotName" id="BotName" value="@Model.BotName" class="form-control"
|
||||||
|
placeholder="e.g., Production TeleBot" required />
|
||||||
|
<small class="text-muted">A friendly name to identify this bot</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="PersonalityName" class="form-label">Personality</label>
|
||||||
|
<select name="PersonalityName" id="PersonalityName" class="form-select">
|
||||||
|
<option value="Alan" @(Model.PersonalityName == "Alan" ? "selected" : "")>Alan (Professional)</option>
|
||||||
|
<option value="Dave" @(Model.PersonalityName == "Dave" ? "selected" : "")>Dave (Casual)</option>
|
||||||
|
<option value="Sarah" @(Model.PersonalityName == "Sarah" ? "selected" : "")>Sarah (Helpful)</option>
|
||||||
|
<option value="Mike" @(Model.PersonalityName == "Mike" ? "selected" : "")>Mike (Direct)</option>
|
||||||
|
<option value="Emma" @(Model.PersonalityName == "Emma" ? "selected" : "")>Emma (Friendly)</option>
|
||||||
|
<option value="Tom" @(Model.PersonalityName == "Tom" ? "selected" : "")>Tom (Efficient)</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Bot conversation style</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="Description" class="form-label">Description (Optional)</label>
|
||||||
|
<textarea name="Description" id="Description" class="form-control" rows="2"
|
||||||
|
placeholder="Brief description of this bot's purpose">@Model.Description</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex">
|
||||||
|
<button type="submit" class="btn btn-success me-md-2">
|
||||||
|
<i class="fas fa-link"></i> Register & Initialize
|
||||||
|
</button>
|
||||||
|
<a href="/Admin/Bots/DiscoverRemote" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (Model.CurrentStep == 3)
|
||||||
|
{
|
||||||
|
<!-- Step 3: Configuration -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-key"></i> Bot Registered - API Key</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if (!string.IsNullOrEmpty(Model.BotKey))
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<strong>Save this Bot Key securely!</strong> It won't be shown again.
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control font-monospace" value="@Model.BotKey" id="botKeyInput" readonly />
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="copyBotKey()">
|
||||||
|
<i class="fas fa-copy"></i> Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-telegram"></i> Step 3: Configure Telegram Token</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted">
|
||||||
|
Now enter the Telegram bot token from BotFather to activate this bot.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form asp-area="Admin" asp-controller="Bots" asp-action="ConfigureRemote" method="post">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
|
||||||
|
<!-- Hidden fields -->
|
||||||
|
<input type="hidden" name="BotId" value="@Model.BotId" />
|
||||||
|
<input type="hidden" name="BotKey" value="@Model.BotKey" />
|
||||||
|
<input type="hidden" name="IpAddress" value="@Model.IpAddress" />
|
||||||
|
<input type="hidden" name="Port" value="@Model.Port" />
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="BotToken" class="form-label">Telegram Bot Token</label>
|
||||||
|
<input name="BotToken" id="BotToken" value="@Model.BotToken" class="form-control font-monospace"
|
||||||
|
placeholder="123456789:ABCdefGHIjklMNOpqrsTUVwxyz" required />
|
||||||
|
<small class="text-muted">
|
||||||
|
Get this from <a href="https://t.me/BotFather" target="_blank">@@BotFather</a> on Telegram
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex">
|
||||||
|
<button type="submit" class="btn btn-success me-md-2">
|
||||||
|
<i class="fas fa-rocket"></i> Configure & Activate Bot
|
||||||
|
</button>
|
||||||
|
<a href="/Admin/Bots" class="btn btn-secondary">
|
||||||
|
Skip (configure later)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Wizard Progress</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li class="@(Model.CurrentStep == 1 ? "text-primary fw-bold" : Model.CurrentStep > 1 ? "text-success" : "text-muted")">
|
||||||
|
<i class="fas fa-@(Model.CurrentStep == 1 ? "search" : Model.CurrentStep > 1 ? "check" : "circle")"></i>
|
||||||
|
1. Discover TeleBot
|
||||||
|
</li>
|
||||||
|
<li class="@(Model.CurrentStep == 2 ? "text-primary fw-bold" : Model.CurrentStep > 2 ? "text-success" : "text-muted")">
|
||||||
|
<i class="fas fa-@(Model.CurrentStep == 2 ? "robot" : Model.CurrentStep > 2 ? "check" : "circle")"></i>
|
||||||
|
2. Register Bot
|
||||||
|
</li>
|
||||||
|
<li class="@(Model.CurrentStep == 3 ? "text-primary fw-bold" : "text-muted")">
|
||||||
|
<i class="fas fa-@(Model.CurrentStep == 3 ? "telegram" : "circle")"></i>
|
||||||
|
3. Configure Telegram
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Requirements</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="small">
|
||||||
|
<li>TeleBot must be running</li>
|
||||||
|
<li>Same discovery secret on both sides</li>
|
||||||
|
<li>Network connectivity to TeleBot</li>
|
||||||
|
<li>Valid Telegram bot token</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (Model.CurrentStep >= 2)
|
||||||
|
{
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Connection Info</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="small mb-1"><strong>Address:</strong> @Model.IpAddress</p>
|
||||||
|
<p class="small mb-0"><strong>Port:</strong> @Model.Port</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
function copyBotKey() {
|
||||||
|
var input = document.getElementById('botKeyInput');
|
||||||
|
input.select();
|
||||||
|
input.setSelectionRange(0, 99999);
|
||||||
|
navigator.clipboard.writeText(input.value).then(function() {
|
||||||
|
alert('Bot Key copied to clipboard!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
@ -7,6 +7,9 @@
|
|||||||
<h1>Bot Management</h1>
|
<h1>Bot Management</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
<a href="/Admin/Bots/DiscoverRemote" class="btn btn-success">
|
||||||
|
<i class="fas fa-satellite-dish"></i> Discover Remote Bot
|
||||||
|
</a>
|
||||||
<a href="/Admin/Bots/Wizard" class="btn btn-primary">
|
<a href="/Admin/Bots/Wizard" class="btn btn-primary">
|
||||||
<i class="fas fa-magic"></i> Create Telegram Bot (Wizard)
|
<i class="fas fa-magic"></i> Create Telegram Bot (Wizard)
|
||||||
</a>
|
</a>
|
||||||
@ -136,6 +139,12 @@
|
|||||||
{
|
{
|
||||||
<span class="badge bg-secondary ms-2">@bot.PersonalityName</span>
|
<span class="badge bg-secondary ms-2">@bot.PersonalityName</span>
|
||||||
}
|
}
|
||||||
|
@if (bot.IsRemote)
|
||||||
|
{
|
||||||
|
<span class="badge bg-info ms-1" title="Remote bot at @bot.RemoteAddress:@bot.RemotePort">
|
||||||
|
<i class="fas fa-satellite-dish"></i> Remote
|
||||||
|
</span>
|
||||||
|
}
|
||||||
@if (!string.IsNullOrEmpty(bot.Description))
|
@if (!string.IsNullOrEmpty(bot.Description))
|
||||||
{
|
{
|
||||||
<br />
|
<br />
|
||||||
@ -181,6 +190,37 @@
|
|||||||
<span class="badge bg-dark">@bot.Status</span>
|
<span class="badge bg-dark">@bot.Status</span>
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@if (bot.IsRemote)
|
||||||
|
{
|
||||||
|
<br />
|
||||||
|
@switch (bot.DiscoveryStatus)
|
||||||
|
{
|
||||||
|
case "Configured":
|
||||||
|
<span class="badge bg-success small mt-1" title="Remote bot is fully configured">
|
||||||
|
<i class="fas fa-check"></i> Configured
|
||||||
|
</span>
|
||||||
|
break;
|
||||||
|
case "Initialized":
|
||||||
|
<span class="badge bg-info small mt-1" title="Remote bot initialized, awaiting config">
|
||||||
|
<i class="fas fa-clock"></i> Initialized
|
||||||
|
</span>
|
||||||
|
break;
|
||||||
|
case "Discovered":
|
||||||
|
<span class="badge bg-warning small mt-1" title="Remote bot discovered, needs setup">
|
||||||
|
<i class="fas fa-exclamation"></i> Needs Setup
|
||||||
|
</span>
|
||||||
|
break;
|
||||||
|
case "Offline":
|
||||||
|
case "Error":
|
||||||
|
<span class="badge bg-danger small mt-1" title="Remote bot is offline or errored">
|
||||||
|
<i class="fas fa-times"></i> @bot.DiscoveryStatus
|
||||||
|
</span>
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
<span class="badge bg-secondary small mt-1">@bot.DiscoveryStatus</span>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-primary">@bot.ActiveSessions</span>
|
<span class="badge bg-primary">@bot.ActiveSessions</span>
|
||||||
|
|||||||
177
LittleShop/Areas/Admin/Views/Bots/RepushConfig.cshtml
Normal file
177
LittleShop/Areas/Admin/Views/Bots/RepushConfig.cshtml
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
@model LittleShop.DTOs.BotDto
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = $"Re-push Configuration - {Model.Name}";
|
||||||
|
var hasExistingToken = (bool)(ViewData["HasExistingToken"] ?? false);
|
||||||
|
var existingToken = ViewData["ExistingToken"] as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>Re-push Configuration</h1>
|
||||||
|
<h4 class="text-muted">@Model.Name</h4>
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/Admin/Bots">Bots</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/Admin/Bots/Details/@Model.Id">@Model.Name</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Re-push Config</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
@if (TempData["Error"] != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
@TempData["Error"]
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card border-info">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-upload"></i> Push Configuration to Remote Bot</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
This will push the Telegram bot token to the remote TeleBot instance at
|
||||||
|
<code>@Model.RemoteAddress:@Model.RemotePort</code>.
|
||||||
|
Use this when the remote bot has been restarted and needs reconfiguration.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (hasExistingToken)
|
||||||
|
{
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<h6><i class="fas fa-check-circle"></i> Existing Token Found</h6>
|
||||||
|
<p class="mb-2">A Telegram bot token is already stored for this bot.</p>
|
||||||
|
<form asp-area="Admin" asp-controller="Bots" asp-action="RepushConfig" asp-route-id="@Model.Id" method="post" class="d-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" name="useExistingToken" value="true" />
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="fas fa-sync"></i> Re-push Existing Token
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<p class="text-muted">Or provide a new token:</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<form asp-area="Admin" asp-controller="Bots" asp-action="RepushConfig" asp-route-id="@Model.Id" method="post">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="botToken" class="form-label">Telegram Bot Token</label>
|
||||||
|
<input type="text" class="form-control" id="botToken" name="botToken"
|
||||||
|
placeholder="123456789:ABCDefGHIjklMNOpqrsTUVwxyz" required />
|
||||||
|
<div class="form-text">
|
||||||
|
Get this from <a href="https://t.me/BotFather" target="_blank">@@BotFather</a> on Telegram.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-upload"></i> Push New Token
|
||||||
|
</button>
|
||||||
|
<a href="/Admin/Bots/Details/@Model.Id" class="btn btn-secondary">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Current Remote Status</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-4">Remote Address</dt>
|
||||||
|
<dd class="col-sm-8"><code>@Model.RemoteAddress:@Model.RemotePort</code></dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-4">Instance ID</dt>
|
||||||
|
<dd class="col-sm-8"><code>@(Model.RemoteInstanceId ?? "N/A")</code></dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-4">Discovery Status</dt>
|
||||||
|
<dd class="col-sm-8">
|
||||||
|
@switch (Model.DiscoveryStatus)
|
||||||
|
{
|
||||||
|
case "Configured":
|
||||||
|
<span class="badge bg-success">@Model.DiscoveryStatus</span>
|
||||||
|
break;
|
||||||
|
case "Initialized":
|
||||||
|
<span class="badge bg-info">@Model.DiscoveryStatus</span>
|
||||||
|
break;
|
||||||
|
case "Discovered":
|
||||||
|
<span class="badge bg-warning">@Model.DiscoveryStatus</span>
|
||||||
|
break;
|
||||||
|
case "Offline":
|
||||||
|
case "Error":
|
||||||
|
<span class="badge bg-danger">@Model.DiscoveryStatus</span>
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
<span class="badge bg-secondary">@Model.DiscoveryStatus</span>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-4">Last Discovery</dt>
|
||||||
|
<dd class="col-sm-8">
|
||||||
|
@if (Model.LastDiscoveryAt.HasValue)
|
||||||
|
{
|
||||||
|
@Model.LastDiscoveryAt.Value.ToString("yyyy-MM-dd HH:mm:ss")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-muted">Never</span>
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Instructions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ol class="ps-3">
|
||||||
|
<li class="mb-2">Ensure the remote TeleBot is running and accessible</li>
|
||||||
|
<li class="mb-2">If the bot was just restarted, it may be in "Awaiting Discovery" mode</li>
|
||||||
|
<li class="mb-2">Enter the Telegram bot token from @@BotFather</li>
|
||||||
|
<li class="mb-2">Click "Push New Token" to configure the remote bot</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h6>When to use this:</h6>
|
||||||
|
<ul class="ps-3 text-muted small">
|
||||||
|
<li>After TeleBot container restart</li>
|
||||||
|
<li>When changing the Telegram bot token</li>
|
||||||
|
<li>If the remote bot lost its configuration</li>
|
||||||
|
<li>After infrastructure recovery</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Quick Actions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<form asp-area="Admin" asp-controller="Bots" asp-action="CheckRemoteStatus" asp-route-id="@Model.Id" method="post" class="d-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-outline-info w-100">
|
||||||
|
<i class="fas fa-sync"></i> Check Remote Status
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<a href="/Admin/Bots/Details/@Model.Id" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Details
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
146
LittleShop/DTOs/BotDiscoveryDto.cs
Normal file
146
LittleShop/DTOs/BotDiscoveryDto.cs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace LittleShop.DTOs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Input for discovering a remote TeleBot instance
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteBotDiscoveryDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[StringLength(255)]
|
||||||
|
public string IpAddress { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Range(1, 65535)]
|
||||||
|
public int Port { get; set; } = 5010;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response from TeleBot's discovery probe endpoint
|
||||||
|
/// </summary>
|
||||||
|
public class DiscoveryProbeResponse
|
||||||
|
{
|
||||||
|
public string InstanceId { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Version { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public bool HasToken { get; set; }
|
||||||
|
public bool IsConfigured { get; set; }
|
||||||
|
public bool IsInitialized { get; set; }
|
||||||
|
public string? TelegramUsername { get; set; }
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Input for registering a discovered remote bot
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteBotRegistrationDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[StringLength(255)]
|
||||||
|
public string IpAddress { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Range(1, 65535)]
|
||||||
|
public int Port { get; set; } = 5010;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[StringLength(100)]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[StringLength(500)]
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[StringLength(50)]
|
||||||
|
public string PersonalityName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instance ID from the discovery probe response
|
||||||
|
/// </summary>
|
||||||
|
public string? RemoteInstanceId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Input for configuring a remote bot with Telegram credentials
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteBotConfigureDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public Guid BotId { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string BotToken { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public Dictionary<string, object>? Settings { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of a discovery probe operation
|
||||||
|
/// </summary>
|
||||||
|
public class DiscoveryResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public DiscoveryProbeResponse? ProbeResponse { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of initializing a remote bot
|
||||||
|
/// </summary>
|
||||||
|
public class InitializeResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public string? InstanceId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of configuring a remote bot
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigureResult
|
||||||
|
{
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// View model for the discovery wizard
|
||||||
|
/// </summary>
|
||||||
|
public class DiscoveryWizardViewModel
|
||||||
|
{
|
||||||
|
// Step 1: Discovery
|
||||||
|
public string IpAddress { get; set; } = string.Empty;
|
||||||
|
public int Port { get; set; } = 5010;
|
||||||
|
|
||||||
|
// Step 2: Registration (populated after probe)
|
||||||
|
public DiscoveryProbeResponse? ProbeResponse { get; set; }
|
||||||
|
public string BotName { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string PersonalityName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Step 3: Configuration (populated after registration)
|
||||||
|
public Guid? BotId { get; set; }
|
||||||
|
public string? BotKey { get; set; }
|
||||||
|
public string BotToken { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Step tracking
|
||||||
|
public int CurrentStep { get; set; } = 1;
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
public string? SuccessMessage { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Discovery status constants
|
||||||
|
/// </summary>
|
||||||
|
public static class DiscoveryStatus
|
||||||
|
{
|
||||||
|
public const string Local = "Local";
|
||||||
|
public const string AwaitingDiscovery = "AwaitingDiscovery";
|
||||||
|
public const string Discovered = "Discovered";
|
||||||
|
public const string Initialized = "Initialized";
|
||||||
|
public const string Configured = "Configured";
|
||||||
|
public const string Offline = "Offline";
|
||||||
|
public const string Error = "Error";
|
||||||
|
}
|
||||||
@ -24,6 +24,23 @@ public class BotDto
|
|||||||
public string PersonalityName { get; set; } = string.Empty;
|
public string PersonalityName { get; set; } = string.Empty;
|
||||||
public Dictionary<string, object> Settings { get; set; } = new();
|
public Dictionary<string, object> Settings { get; set; } = new();
|
||||||
|
|
||||||
|
// Remote Discovery Fields
|
||||||
|
public string? RemoteAddress { get; set; }
|
||||||
|
public int? RemotePort { get; set; }
|
||||||
|
public DateTime? LastDiscoveryAt { get; set; }
|
||||||
|
public string DiscoveryStatus { get; set; } = "Local";
|
||||||
|
public string? RemoteInstanceId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if this is a remotely discovered bot
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRemote => !string.IsNullOrEmpty(RemoteAddress);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full remote endpoint URL
|
||||||
|
/// </summary>
|
||||||
|
public string? RemoteEndpoint => IsRemote ? $"{RemoteAddress}:{RemotePort}" : null;
|
||||||
|
|
||||||
// Metrics summary
|
// Metrics summary
|
||||||
public int TotalSessions { get; set; }
|
public int TotalSessions { get; set; }
|
||||||
public int ActiveSessions { get; set; }
|
public int ActiveSessions { get; set; }
|
||||||
|
|||||||
@ -52,6 +52,36 @@ public class Bot
|
|||||||
[StringLength(50)]
|
[StringLength(50)]
|
||||||
public string PersonalityName { get; set; } = string.Empty;
|
public string PersonalityName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Remote Discovery Fields
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IP address or hostname of the remote TeleBot instance
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(255)]
|
||||||
|
public string? RemoteAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Port number for the remote TeleBot instance
|
||||||
|
/// </summary>
|
||||||
|
public int? RemotePort { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp of last successful discovery probe
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastDiscoveryAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Discovery status: Local, Discovered, Initialized, Configured, Offline
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(50)]
|
||||||
|
public string DiscoveryStatus { get; set; } = "Local";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instance ID returned by the remote TeleBot
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(100)]
|
||||||
|
public string? RemoteInstanceId { get; set; }
|
||||||
|
|
||||||
// Navigation properties
|
// Navigation properties
|
||||||
public virtual ICollection<BotMetric> Metrics { get; set; } = new List<BotMetric>();
|
public virtual ICollection<BotMetric> Metrics { get; set; } = new List<BotMetric>();
|
||||||
public virtual ICollection<BotSession> Sessions { get; set; } = new List<BotSession>();
|
public virtual ICollection<BotSession> Sessions { get; set; } = new List<BotSession>();
|
||||||
|
|||||||
@ -226,6 +226,7 @@ builder.Services.AddScoped<IDataSeederService, DataSeederService>();
|
|||||||
builder.Services.AddScoped<IBotService, BotService>();
|
builder.Services.AddScoped<IBotService, BotService>();
|
||||||
builder.Services.AddScoped<IBotMetricsService, BotMetricsService>();
|
builder.Services.AddScoped<IBotMetricsService, BotMetricsService>();
|
||||||
builder.Services.AddScoped<IBotContactService, BotContactService>();
|
builder.Services.AddScoped<IBotContactService, BotContactService>();
|
||||||
|
builder.Services.AddHttpClient<IBotDiscoveryService, BotDiscoveryService>();
|
||||||
builder.Services.AddScoped<IMessageDeliveryService, MessageDeliveryService>();
|
builder.Services.AddScoped<IMessageDeliveryService, MessageDeliveryService>();
|
||||||
builder.Services.AddScoped<ICustomerService, CustomerService>();
|
builder.Services.AddScoped<ICustomerService, CustomerService>();
|
||||||
builder.Services.AddScoped<ICustomerMessageService, CustomerMessageService>();
|
builder.Services.AddScoped<ICustomerMessageService, CustomerMessageService>();
|
||||||
|
|||||||
450
LittleShop/Services/BotDiscoveryService.cs
Normal file
450
LittleShop/Services/BotDiscoveryService.cs
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for discovering and configuring remote TeleBot instances.
|
||||||
|
/// Handles communication with TeleBot's discovery API endpoints.
|
||||||
|
/// </summary>
|
||||||
|
public class BotDiscoveryService : IBotDiscoveryService
|
||||||
|
{
|
||||||
|
private readonly ILogger<BotDiscoveryService> _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<BotDiscoveryService> logger,
|
||||||
|
IConfiguration configuration,
|
||||||
|
HttpClient httpClient,
|
||||||
|
IBotService botService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_botService = botService;
|
||||||
|
|
||||||
|
// Configure default timeout
|
||||||
|
var timeoutSeconds = _configuration.GetValue<int>("BotDiscovery:ConnectionTimeoutSeconds", 10);
|
||||||
|
_httpClient.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DiscoveryResult> 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<DiscoveryProbeResponse>(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<InitializeResult> 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<InitializeResponse>(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<ConfigureResult> PushConfigurationAsync(Guid botId, string botToken, Dictionary<string, object>? 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<ConfigureResponse>(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<bool> 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<DiscoveryProbeResponse?> 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<DiscoveryProbeResponse>(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 bot = await _botService.GetBotByIdAsync(botId);
|
||||||
|
if (bot != null)
|
||||||
|
{
|
||||||
|
// Update via direct database access would be better, but for now use a workaround
|
||||||
|
// This would typically be done through a dedicated method on IBotService
|
||||||
|
_logger.LogInformation("Updating bot {BotId} discovery status to {Status}", botId, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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
|
||||||
|
}
|
||||||
@ -323,6 +323,31 @@ public class BotService : IBotService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateRemoteInfoAsync(Guid botId, string remoteAddress, int remotePort, string? instanceId, string discoveryStatus)
|
||||||
|
{
|
||||||
|
var bot = await _context.Bots.FindAsync(botId);
|
||||||
|
if (bot == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bot.RemoteAddress = remoteAddress;
|
||||||
|
bot.RemotePort = remotePort;
|
||||||
|
bot.RemoteInstanceId = instanceId;
|
||||||
|
bot.DiscoveryStatus = discoveryStatus;
|
||||||
|
bot.LastDiscoveryAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Updated remote info for bot {BotId}: {Address}:{Port} (Status: {Status})",
|
||||||
|
botId, remoteAddress, remotePort, discoveryStatus);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetBotKeyAsync(Guid botId)
|
||||||
|
{
|
||||||
|
var bot = await _context.Bots.FindAsync(botId);
|
||||||
|
return bot?.BotKey;
|
||||||
|
}
|
||||||
|
|
||||||
private BotDto MapToDto(Bot bot)
|
private BotDto MapToDto(Bot bot)
|
||||||
{
|
{
|
||||||
var settings = new Dictionary<string, object>();
|
var settings = new Dictionary<string, object>();
|
||||||
@ -355,6 +380,13 @@ public class BotService : IBotService
|
|||||||
PlatformId = bot.PlatformId,
|
PlatformId = bot.PlatformId,
|
||||||
PersonalityName = bot.PersonalityName,
|
PersonalityName = bot.PersonalityName,
|
||||||
Settings = settings,
|
Settings = settings,
|
||||||
|
// Remote Discovery Fields
|
||||||
|
RemoteAddress = bot.RemoteAddress,
|
||||||
|
RemotePort = bot.RemotePort,
|
||||||
|
LastDiscoveryAt = bot.LastDiscoveryAt,
|
||||||
|
DiscoveryStatus = bot.DiscoveryStatus,
|
||||||
|
RemoteInstanceId = bot.RemoteInstanceId,
|
||||||
|
// Metrics
|
||||||
TotalSessions = bot.Sessions.Count,
|
TotalSessions = bot.Sessions.Count,
|
||||||
ActiveSessions = activeSessions,
|
ActiveSessions = activeSessions,
|
||||||
TotalRevenue = totalRevenue,
|
TotalRevenue = totalRevenue,
|
||||||
|
|||||||
34
LittleShop/Services/IBotDiscoveryService.cs
Normal file
34
LittleShop/Services/IBotDiscoveryService.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using LittleShop.DTOs;
|
||||||
|
|
||||||
|
namespace LittleShop.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for discovering and configuring remote TeleBot instances
|
||||||
|
/// </summary>
|
||||||
|
public interface IBotDiscoveryService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Probe a remote TeleBot instance to discover its status
|
||||||
|
/// </summary>
|
||||||
|
Task<DiscoveryResult> ProbeRemoteBotAsync(string ipAddress, int port);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize a remote TeleBot instance with a BotKey
|
||||||
|
/// </summary>
|
||||||
|
Task<InitializeResult> InitializeRemoteBotAsync(Guid botId, string ipAddress, int port);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Push configuration (bot token and settings) to a remote TeleBot instance
|
||||||
|
/// </summary>
|
||||||
|
Task<ConfigureResult> PushConfigurationAsync(Guid botId, string botToken, Dictionary<string, object>? settings = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test basic connectivity to a remote address
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> TestConnectivityAsync(string ipAddress, int port);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the status of a remote TeleBot instance
|
||||||
|
/// </summary>
|
||||||
|
Task<DiscoveryProbeResponse?> GetRemoteStatusAsync(string ipAddress, int port, string botKey);
|
||||||
|
}
|
||||||
@ -23,4 +23,6 @@ public interface IBotService
|
|||||||
Task<bool> ValidateBotKeyAsync(string botKey);
|
Task<bool> ValidateBotKeyAsync(string botKey);
|
||||||
Task<string> GenerateBotKeyAsync();
|
Task<string> GenerateBotKeyAsync();
|
||||||
Task<bool> UpdatePlatformInfoAsync(Guid botId, UpdatePlatformInfoDto dto);
|
Task<bool> UpdatePlatformInfoAsync(Guid botId, UpdatePlatformInfoDto dto);
|
||||||
|
Task<bool> UpdateRemoteInfoAsync(Guid botId, string remoteAddress, int remotePort, string? instanceId, string discoveryStatus);
|
||||||
|
Task<string?> GetBotKeyAsync(Guid botId);
|
||||||
}
|
}
|
||||||
@ -47,6 +47,14 @@
|
|||||||
"172.16.0.0/12"
|
"172.16.0.0/12"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"BotDiscovery": {
|
||||||
|
"SharedSecret": "CHANGE_THIS_SHARED_SECRET_32_CHARS",
|
||||||
|
"ConnectionTimeoutSeconds": 10,
|
||||||
|
"ProbeRetryAttempts": 3,
|
||||||
|
"WebhookSecret": "",
|
||||||
|
"LittleShopApiUrl": "",
|
||||||
|
"Comment": "SharedSecret must match TeleBot Discovery:Secret. LittleShopApiUrl is the public URL for TeleBot to call back."
|
||||||
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
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.AddSingleton<BotManagerService>();
|
||||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<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
|
// Message Delivery Service - Single instance
|
||||||
builder.Services.AddSingleton<MessageDeliveryService>();
|
builder.Services.AddSingleton<MessageDeliveryService>();
|
||||||
builder.Services.AddSingleton<IMessageDeliveryService>(sp => sp.GetRequiredService<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("Privacy Mode: {PrivacyMode}", builder.Configuration["Privacy:Mode"]);
|
||||||
Log.Information("Ephemeral by Default: {Ephemeral}", builder.Configuration["Privacy:EphemeralByDefault"]);
|
Log.Information("Ephemeral by Default: {Ephemeral}", builder.Configuration["Privacy:EphemeralByDefault"]);
|
||||||
Log.Information("Tor Enabled: {Tor}", builder.Configuration["Privacy:EnableTor"]);
|
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");
|
Log.Information("Webhook endpoints available at /api/webhook");
|
||||||
|
|
||||||
await app.RunAsync();
|
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 ITelegramBotClient? _botClient;
|
||||||
private CancellationTokenSource? _cancellationTokenSource;
|
private CancellationTokenSource? _cancellationTokenSource;
|
||||||
private string? _currentBotToken;
|
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(
|
public TelegramBotService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
@ -120,6 +126,8 @@ namespace TeleBot
|
|||||||
cancellationToken: _cancellationTokenSource.Token
|
cancellationToken: _cancellationTokenSource.Token
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_isRunning = true;
|
||||||
|
|
||||||
var me = await _botClient.GetMeAsync(cancellationToken);
|
var me = await _botClient.GetMeAsync(cancellationToken);
|
||||||
_logger.LogInformation("Bot started: @{Username} ({Id})", me.Username, me.Id);
|
_logger.LogInformation("Bot started: @{Username} ({Id})", me.Username, me.Id);
|
||||||
|
|
||||||
@ -132,6 +140,7 @@ namespace TeleBot
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
_isRunning = false;
|
||||||
_cancellationTokenSource?.Cancel();
|
_cancellationTokenSource?.Cancel();
|
||||||
_logger.LogInformation("Bot stopped");
|
_logger.LogInformation("Bot stopped");
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@ -273,6 +282,8 @@ namespace TeleBot
|
|||||||
cancellationToken: _cancellationTokenSource.Token
|
cancellationToken: _cancellationTokenSource.Token
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_isRunning = true;
|
||||||
|
|
||||||
var me = await _botClient.GetMeAsync();
|
var me = await _botClient.GetMeAsync();
|
||||||
_logger.LogInformation("Bot restarted with new token: @{Username} ({Id})", me.Username, me.Id);
|
_logger.LogInformation("Bot restarted with new token: @{Username} ({Id})", me.Username, me.Id);
|
||||||
|
|
||||||
|
|||||||
@ -2,18 +2,29 @@
|
|||||||
"BotInfo": {
|
"BotInfo": {
|
||||||
"Name": "LittleShop TeleBot",
|
"Name": "LittleShop TeleBot",
|
||||||
"Description": "Privacy-focused e-commerce Telegram bot",
|
"Description": "Privacy-focused e-commerce Telegram bot",
|
||||||
"Version": "1.0.0"
|
"Version": "1.0.0",
|
||||||
|
"InstanceId": ""
|
||||||
},
|
},
|
||||||
"BotManager": {
|
"BotManager": {
|
||||||
"ApiKey": "",
|
"ApiKey": "",
|
||||||
"Comment": "This will be populated after first registration with admin panel"
|
"Comment": "Populated by LittleShop during discovery initialization"
|
||||||
},
|
},
|
||||||
"Telegram": {
|
"Telegram": {
|
||||||
"BotToken": "8496279616:AAE7kV_riICbWxn6-MPFqcrWx7K8b4_NKq0",
|
"BotToken": "",
|
||||||
"AdminChatId": "123456789",
|
"AdminChatId": "",
|
||||||
"WebhookUrl": "",
|
"WebhookUrl": "",
|
||||||
"UseWebhook": false,
|
"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": {
|
"Webhook": {
|
||||||
"Secret": "",
|
"Secret": "",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user