feat: Bot management improvements with wallet configuration and duplicate detection
This commit is contained in:
parent
91000035f5
commit
7008a95df3
@ -42,7 +42,9 @@
|
||||
"Bash(/tmp/bypass-hdwallet-unlock.sh:*)",
|
||||
"Bash(/tmp/fix-db-initialization.sh:*)",
|
||||
"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": [],
|
||||
"ask": []
|
||||
|
||||
@ -307,6 +307,47 @@ public class BotsController : Controller
|
||||
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
|
||||
public IActionResult RegenerateKey(Guid id)
|
||||
{
|
||||
|
||||
@ -49,6 +49,63 @@
|
||||
</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-header">
|
||||
<h5 class="mb-0">Bot Configuration (JSON)</h5>
|
||||
|
||||
@ -15,6 +15,86 @@
|
||||
</a>
|
||||
</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)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
|
||||
@ -54,6 +54,25 @@ public class BotsController : ControllerBase
|
||||
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
|
||||
[HttpGet("settings")]
|
||||
public async Task<ActionResult<Dictionary<string, object>>> GetBotSettings()
|
||||
|
||||
@ -111,3 +111,18 @@ public class BotWizardDto
|
||||
|
||||
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;
|
||||
}
|
||||
@ -99,6 +99,27 @@ public class BotService : IBotService
|
||||
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()
|
||||
{
|
||||
try
|
||||
|
||||
@ -12,6 +12,7 @@ public interface IBotService
|
||||
Task<BotDto?> AuthenticateBotAsync(string botKey);
|
||||
Task<BotDto?> GetBotByIdAsync(Guid id);
|
||||
Task<BotDto?> GetBotByKeyAsync(string botKey);
|
||||
Task<BotDto?> GetBotByPlatformUsernameAsync(int platformType, string platformUsername);
|
||||
Task<IEnumerable<BotDto>> GetAllBotsAsync();
|
||||
Task<IEnumerable<BotDto>> GetActiveBots();
|
||||
Task<bool> UpdateBotSettingsAsync(Guid botId, UpdateBotSettingsDto dto);
|
||||
|
||||
@ -55,8 +55,34 @@ namespace TeleBot.Services
|
||||
|
||||
if (string.IsNullOrEmpty(_botKey))
|
||||
{
|
||||
// Register new bot
|
||||
await RegisterBotAsync();
|
||||
// Try to find existing bot registration by Telegram username first
|
||||
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
|
||||
{
|
||||
@ -137,6 +163,9 @@ namespace TeleBot.Services
|
||||
_logger.LogInformation("Bot registered successfully. Bot ID: {BotId}", _botId);
|
||||
_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
|
||||
// In production, this should be saved securely
|
||||
}
|
||||
@ -423,6 +452,118 @@ namespace TeleBot.Services
|
||||
_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
|
||||
private class BotRegistrationResponse
|
||||
{
|
||||
@ -435,11 +576,29 @@ namespace TeleBot.Services
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string BotKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private class SessionDto
|
||||
{
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user