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:
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
@@ -8,6 +9,7 @@ using Microsoft.Extensions.Logging;
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Enums;
|
||||
using LittleShop.Services;
|
||||
using LittleShop.Models;
|
||||
|
||||
namespace LittleShop.Areas.Admin.Controllers;
|
||||
|
||||
@@ -18,17 +20,20 @@ public class BotsController : Controller
|
||||
private readonly IBotService _botService;
|
||||
private readonly IBotMetricsService _metricsService;
|
||||
private readonly ITelegramBotManagerService _telegramManager;
|
||||
private readonly IBotDiscoveryService _discoveryService;
|
||||
private readonly ILogger<BotsController> _logger;
|
||||
|
||||
public BotsController(
|
||||
IBotService botService,
|
||||
IBotMetricsService metricsService,
|
||||
ITelegramBotManagerService telegramManager,
|
||||
IBotDiscoveryService discoveryService,
|
||||
ILogger<BotsController> logger)
|
||||
{
|
||||
_botService = botService;
|
||||
_metricsService = metricsService;
|
||||
_telegramManager = telegramManager;
|
||||
_discoveryService = discoveryService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -379,4 +384,397 @@ public class BotsController : Controller
|
||||
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>
|
||||
|
||||
@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-header">
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<i class="fas fa-magic"></i> Create Telegram Bot (Wizard)
|
||||
</a>
|
||||
@@ -136,6 +139,12 @@
|
||||
{
|
||||
<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))
|
||||
{
|
||||
<br />
|
||||
@@ -181,6 +190,37 @@
|
||||
<span class="badge bg-dark">@bot.Status</span>
|
||||
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>
|
||||
<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>
|
||||
Reference in New Issue
Block a user