feat: Bot management improvements with wallet configuration and duplicate detection

This commit is contained in:
SysAdmin 2025-10-10 12:34:00 +01:00
parent 91000035f5
commit 7008a95df3
9 changed files with 408 additions and 13 deletions

View File

@ -42,10 +42,12 @@
"Bash(/tmp/bypass-hdwallet-unlock.sh:*)", "Bash(/tmp/bypass-hdwallet-unlock.sh:*)",
"Bash(/tmp/fix-db-initialization.sh:*)", "Bash(/tmp/fix-db-initialization.sh:*)",
"SlashCommand(/code-review)", "SlashCommand(/code-review)",
"Read(//mnt/c/Production/Source/SilverLABS/SilverPAY.NET/**)" "Read(//mnt/c/Production/Source/SilverLABS/SilverPAY.NET/**)",
"Bash(sqlite3:*)",
"Bash(git checkout:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []
}, },
"outputStyle": "enterprise-full-stack-developer" "outputStyle": "enterprise-full-stack-developer"
} }

View File

@ -307,6 +307,47 @@ public class BotsController : Controller
return RedirectToAction(nameof(Details), new { id }); return RedirectToAction(nameof(Details), new { id });
} }
// POST: Admin/Bots/UpdateWallets/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UpdateWallets(Guid id, Dictionary<string, string> wallets)
{
try
{
var bot = await _botService.GetBotByIdAsync(id);
if (bot == null)
{
TempData["Error"] = "Bot not found";
return RedirectToAction(nameof(Index));
}
// Get current settings
var settings = bot.Settings ?? new Dictionary<string, object>();
// Filter out empty wallet addresses
var validWallets = wallets
.Where(w => !string.IsNullOrWhiteSpace(w.Value))
.ToDictionary(w => w.Key, w => w.Value.Trim());
// Update or add wallets section
settings["wallets"] = validWallets;
// Save settings
await _botService.UpdateBotSettingsAsync(id, new UpdateBotSettingsDto { Settings = settings });
_logger.LogInformation("Updated {Count} wallet addresses for bot {BotId}", validWallets.Count, id);
TempData["Success"] = $"Updated {validWallets.Count} wallet address(es) successfully";
return RedirectToAction(nameof(Edit), new { id });
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update wallets for bot {BotId}", id);
TempData["Error"] = "Failed to update wallet addresses";
return RedirectToAction(nameof(Edit), new { id });
}
}
// GET: Admin/Bots/RegenerateKey/5 // GET: Admin/Bots/RegenerateKey/5
public IActionResult RegenerateKey(Guid id) public IActionResult RegenerateKey(Guid id)
{ {

View File

@ -49,6 +49,63 @@
</div> </div>
</div> </div>
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">💰 Payment Wallets</h5>
</div>
<div class="card-body">
<p class="text-muted">Configure cryptocurrency wallet addresses for direct payments. These are used as fallback when payment gateway is unavailable.</p>
@{
var wallets = new Dictionary<string, string>();
try
{
var settings = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, System.Text.Json.JsonElement>>(@settingsJson);
if (settings != null && settings.ContainsKey("wallets"))
{
wallets = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(settings["wallets"].GetRawText()) ?? new Dictionary<string, string>();
}
}
catch { }
var supportedCurrencies = new[] {
new { Code = "BTC", Name = "Bitcoin", Placeholder = "bc1q... or 1... or 3..." },
new { Code = "XMR", Name = "Monero", Placeholder = "4... (primary address)" },
new { Code = "LTC", Name = "Litecoin", Placeholder = "ltc1q... or L... or M..." },
new { Code = "DOGE", Name = "Dogecoin", Placeholder = "D..." },
new { Code = "ETH", Name = "Ethereum", Placeholder = "0x..." },
new { Code = "ZEC", Name = "Zcash", Placeholder = "t1... or zs1..." },
new { Code = "DASH", Name = "Dash", Placeholder = "X..." }
};
}
<form asp-action="UpdateWallets" asp-route-id="@Model.Id" method="post" id="walletsForm">
@Html.AntiForgeryToken()
@foreach (var currency in supportedCurrencies)
{
<div class="mb-3">
<label for="wallet_@currency.Code" class="form-label">
<strong>@currency.Code</strong> - @currency.Name
</label>
<input type="text"
name="wallets[@currency.Code]"
id="wallet_@currency.Code"
class="form-control font-monospace"
placeholder="@currency.Placeholder"
value="@(wallets.ContainsKey(currency.Code) ? wallets[currency.Code] : "")" />
</div>
}
<div class="alert alert-info">
<strong>Note:</strong> Only configured wallets will be available as payment options. Leave blank to disable direct payments for that cryptocurrency.
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-wallet2"></i> Save Wallet Addresses
</button>
</form>
</div>
</div>
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="mb-0">Bot Configuration (JSON)</h5> <h5 class="mb-0">Bot Configuration (JSON)</h5>

View File

@ -15,6 +15,86 @@
</a> </a>
</p> </p>
@{
// Detect duplicates by platform username
var duplicateGroups = Model
.Where(b => !string.IsNullOrEmpty(b.PlatformUsername))
.GroupBy(b => b.PlatformUsername)
.Where(g => g.Count() > 1)
.ToList();
if (duplicateGroups.Any())
{
<div class="alert alert-warning">
<h5 class="alert-heading"><i class="bi bi-exclamation-triangle"></i> Duplicate Bots Detected</h5>
<p>Found <strong>@duplicateGroups.Count duplicate bot group(s)</strong> with multiple entries for the same Telegram username.</p>
<p>This usually happens when the bot container restarts without a saved API key. The system now prevents this automatically.</p>
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="collapse" data-bs-target="#duplicateDetails">
Show Details
</button>
<div id="duplicateDetails" class="collapse mt-3">
@foreach (var group in duplicateGroups)
{
<div class="card mb-2">
<div class="card-body">
<h6 class="card-title">@@@group.Key (@group.Count() entries)</h6>
<table class="table table-sm">
<thead>
<tr>
<th>Created</th>
<th>Last Seen</th>
<th>Status</th>
<th>Sessions</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@foreach (var bot in group.OrderByDescending(b => b.LastSeenAt ?? b.CreatedAt))
{
var isNewest = bot == group.OrderByDescending(b => b.LastSeenAt ?? b.CreatedAt).First();
<tr class="@(isNewest ? "table-success" : "")">
<td>@bot.CreatedAt.ToString("yyyy-MM-dd HH:mm")</td>
<td>
@if (bot.LastSeenAt.HasValue)
{
@bot.LastSeenAt.Value.ToString("yyyy-MM-dd HH:mm")
}
else
{
<span class="text-muted">Never</span>
}
</td>
<td><span class="badge bg-@(bot.Status == LittleShop.Enums.BotStatus.Active ? "success" : "secondary")">@bot.Status</span></td>
<td>@bot.TotalSessions</td>
<td>
@if (isNewest)
{
<span class="badge bg-success">Keep (Most Recent)</span>
}
else
{
<form action="/Admin/Bots/Delete/@bot.Id" method="post" style="display:inline;">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-outline-danger"
onclick="return confirm('Delete this duplicate bot entry? Sessions: @bot.TotalSessions')">
<i class="bi bi-trash"></i> Delete
</button>
</form>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
</div>
}
}
@if (TempData["Success"] != null) @if (TempData["Success"] != null)
{ {
<div class="alert alert-success alert-dismissible fade show" role="alert"> <div class="alert alert-success alert-dismissible fade show" role="alert">

View File

@ -54,6 +54,25 @@ public class BotsController : ControllerBase
return Ok(bot); return Ok(bot);
} }
[HttpGet("by-platform/{platformType}/{platformUsername}")]
[AllowAnonymous]
public async Task<ActionResult<BotDto>> GetBotByPlatformUsername(int platformType, string platformUsername)
{
try
{
var bot = await _botService.GetBotByPlatformUsernameAsync(platformType, platformUsername);
if (bot == null)
return NotFound($"Bot not found for platform {platformType} with username {platformUsername}");
return Ok(bot);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to find bot by platform username {Platform}/@{Username}", platformType, platformUsername);
return StatusCode(500, "Internal server error");
}
}
// Bot Settings // Bot Settings
[HttpGet("settings")] [HttpGet("settings")]
public async Task<ActionResult<Dictionary<string, object>>> GetBotSettings() public async Task<ActionResult<Dictionary<string, object>>> GetBotSettings()

View File

@ -98,16 +98,31 @@ public class BotWizardDto
[Required(ErrorMessage = "Bot name is required")] [Required(ErrorMessage = "Bot name is required")]
[StringLength(50)] [StringLength(50)]
public string BotName { get; set; } = string.Empty; public string BotName { get; set; } = string.Empty;
[Required(ErrorMessage = "Bot username is required")] [Required(ErrorMessage = "Bot username is required")]
[StringLength(100)] [StringLength(100)]
public string BotUsername { get; set; } = string.Empty; public string BotUsername { get; set; } = string.Empty;
[StringLength(500)] [StringLength(500)]
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
[StringLength(50)] [StringLength(50)]
public string PersonalityName { get; set; } = string.Empty; public string PersonalityName { get; set; } = string.Empty;
public string BotToken { get; set; } = string.Empty; public string BotToken { get; set; } = string.Empty;
}
public class UpdateBotWalletsDto
{
public Dictionary<string, string> Wallets { get; set; } = new();
}
public class BotWalletEntry
{
[Required]
public string Currency { get; set; } = string.Empty;
[Required]
[StringLength(500)]
public string Address { get; set; } = string.Empty;
} }

View File

@ -99,6 +99,27 @@ public class BotService : IBotService
return bot != null ? MapToDto(bot) : null; return bot != null ? MapToDto(bot) : null;
} }
public async Task<BotDto?> GetBotByPlatformUsernameAsync(int platformType, string platformUsername)
{
try
{
var bot = await _context.Bots
.Include(b => b.Sessions)
.Include(b => b.Metrics)
.FirstOrDefaultAsync(b =>
b.Type == (BotType)platformType &&
b.PlatformUsername == platformUsername &&
b.Status != BotStatus.Deleted);
return bot != null ? MapToDto(bot) : null;
}
catch (Microsoft.Data.Sqlite.SqliteException ex) when (ex.Message.Contains("no such table"))
{
// Tables don't exist yet - return null
return null;
}
}
public async Task<IEnumerable<BotDto>> GetAllBotsAsync() public async Task<IEnumerable<BotDto>> GetAllBotsAsync()
{ {
try try

View File

@ -12,6 +12,7 @@ public interface IBotService
Task<BotDto?> AuthenticateBotAsync(string botKey); Task<BotDto?> AuthenticateBotAsync(string botKey);
Task<BotDto?> GetBotByIdAsync(Guid id); Task<BotDto?> GetBotByIdAsync(Guid id);
Task<BotDto?> GetBotByKeyAsync(string botKey); Task<BotDto?> GetBotByKeyAsync(string botKey);
Task<BotDto?> GetBotByPlatformUsernameAsync(int platformType, string platformUsername);
Task<IEnumerable<BotDto>> GetAllBotsAsync(); Task<IEnumerable<BotDto>> GetAllBotsAsync();
Task<IEnumerable<BotDto>> GetActiveBots(); Task<IEnumerable<BotDto>> GetActiveBots();
Task<bool> UpdateBotSettingsAsync(Guid botId, UpdateBotSettingsDto dto); Task<bool> UpdateBotSettingsAsync(Guid botId, UpdateBotSettingsDto dto);

View File

@ -52,11 +52,37 @@ namespace TeleBot.Services
{ {
// Check if bot key exists in configuration // Check if bot key exists in configuration
_botKey = _configuration["BotManager:ApiKey"]; _botKey = _configuration["BotManager:ApiKey"];
if (string.IsNullOrEmpty(_botKey)) if (string.IsNullOrEmpty(_botKey))
{ {
// Register new bot // Try to find existing bot registration by Telegram username first
await RegisterBotAsync(); var botUsername = await GetTelegramBotUsernameAsync();
if (!string.IsNullOrEmpty(botUsername))
{
var existingBot = await FindExistingBotByPlatformAsync(botUsername);
if (existingBot != null)
{
_logger.LogInformation("Found existing bot registration for @{Username} (ID: {BotId}). Using existing bot.",
botUsername, existingBot.Id);
_botKey = existingBot.BotKey;
_botId = existingBot.Id;
// Update platform info in case it changed
await UpdatePlatformInfoAsync();
}
else
{
_logger.LogInformation("No existing bot found for @{Username}. Registering new bot.", botUsername);
await RegisterBotAsync();
}
}
else
{
_logger.LogWarning("Could not determine bot username. Registering new bot.");
await RegisterBotAsync();
}
} }
else else
{ {
@ -125,18 +151,21 @@ namespace TeleBot.Services
var content = new StringContent(json, Encoding.UTF8, "application/json"); var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{apiUrl}/api/bots/register", content); var response = await _httpClient.PostAsync($"{apiUrl}/api/bots/register", content);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
var responseJson = await response.Content.ReadAsStringAsync(); var responseJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<BotRegistrationResponse>(responseJson); var result = JsonSerializer.Deserialize<BotRegistrationResponse>(responseJson);
_botKey = result?.BotKey; _botKey = result?.BotKey;
_botId = result?.BotId; _botId = result?.BotId;
_logger.LogInformation("Bot registered successfully. Bot ID: {BotId}", _botId); _logger.LogInformation("Bot registered successfully. Bot ID: {BotId}", _botId);
_logger.LogWarning("IMPORTANT: Save this bot key securely: {BotKey}", _botKey); _logger.LogWarning("IMPORTANT: Save this bot key securely: {BotKey}", _botKey);
// Update platform info immediately after registration
await UpdatePlatformInfoAsync();
// Save bot key to configuration or secure storage // Save bot key to configuration or secure storage
// In production, this should be saved securely // In production, this should be saved securely
} }
@ -423,6 +452,118 @@ namespace TeleBot.Services
_settingsSyncTimer?.Dispose(); _settingsSyncTimer?.Dispose();
} }
private async Task<string?> GetTelegramBotUsernameAsync()
{
try
{
var botToken = _configuration["Telegram:BotToken"];
if (string.IsNullOrEmpty(botToken) || botToken == "YOUR_BOT_TOKEN_HERE")
{
_logger.LogWarning("Bot token not configured in appsettings.json");
return null;
}
// Call Telegram API to get bot info
var response = await _httpClient.GetAsync($"https://api.telegram.org/bot{botToken}/getMe");
if (response.IsSuccessStatusCode)
{
var responseJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<TelegramGetMeResponse>(responseJson);
return result?.Result?.Username;
}
else
{
_logger.LogWarning("Failed to get bot info from Telegram: {StatusCode}", response.StatusCode);
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting Telegram bot username");
return null;
}
}
private async Task<BotDto?> FindExistingBotByPlatformAsync(string platformUsername)
{
try
{
var apiUrl = _configuration["LittleShop:ApiUrl"];
const int telegramBotType = 0; // BotType.Telegram enum value
var response = await _httpClient.GetAsync($"{apiUrl}/api/bots/by-platform/{telegramBotType}/{platformUsername}");
if (response.IsSuccessStatusCode)
{
var responseJson = await response.Content.ReadAsStringAsync();
var bot = JsonSerializer.Deserialize<BotDto>(responseJson);
return bot;
}
else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null; // Bot not found - this is expected for first registration
}
else
{
_logger.LogWarning("Failed to check for existing bot: {StatusCode}", response.StatusCode);
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error finding existing bot by platform username");
return null;
}
}
private async Task UpdatePlatformInfoAsync()
{
try
{
var apiUrl = _configuration["LittleShop:ApiUrl"];
var botToken = _configuration["Telegram:BotToken"];
if (string.IsNullOrEmpty(botToken) || string.IsNullOrEmpty(_botKey))
return;
// Get bot info from Telegram
var telegramResponse = await _httpClient.GetAsync($"https://api.telegram.org/bot{botToken}/getMe");
if (!telegramResponse.IsSuccessStatusCode)
return;
var telegramJson = await telegramResponse.Content.ReadAsStringAsync();
var telegramResult = JsonSerializer.Deserialize<TelegramGetMeResponse>(telegramJson);
if (telegramResult?.Result == null)
return;
// Update platform info in LittleShop
var updateData = new
{
PlatformUsername = telegramResult.Result.Username,
PlatformDisplayName = telegramResult.Result.FirstName ?? telegramResult.Result.Username,
PlatformId = telegramResult.Result.Id.ToString()
};
var json = JsonSerializer.Serialize(updateData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
var response = await _httpClient.PutAsync($"{apiUrl}/api/bots/platform-info", content);
if (response.IsSuccessStatusCode)
{
_logger.LogInformation("Updated platform info for @{Username}", telegramResult.Result.Username);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update platform info");
}
}
// DTOs for API responses // DTOs for API responses
private class BotRegistrationResponse private class BotRegistrationResponse
{ {
@ -435,11 +576,29 @@ namespace TeleBot.Services
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string BotKey { get; set; } = string.Empty;
} }
private class SessionDto private class SessionDto
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
} }
private class TelegramGetMeResponse
{
public bool Ok { get; set; }
public TelegramBotInfo? Result { get; set; }
}
private class TelegramBotInfo
{
public long Id { get; set; }
public bool IsBot { get; set; }
public string FirstName { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public bool? CanJoinGroups { get; set; }
public bool? CanReadAllGroupMessages { get; set; }
public bool? SupportsInlineQueries { get; set; }
}
} }
} }