littleshop/LittleShop/Services/BotService.cs
SysAdmin 86f19ba044
All checks were successful
Build and Deploy LittleShop / Deploy to Pre-Production (CT109) (push) Successful in 59s
Build and Deploy LittleShop / Deploy to Production VPS (Manual Only) (push) Has been skipped
feat: Add AlexHost deployment pipeline and bot control functionality
- Add Gitea Actions workflow for manual AlexHost deployment
- Add docker-compose.alexhost.yml for production deployment
- Add deploy-alexhost.sh script with server-side build support
- Add Bot Control feature (Start/Stop/Restart) for remote bot management
- Add discovery control endpoint in TeleBot
- Update TeleBot with StartPollingAsync/StopPolling/RestartPollingAsync
- Fix platform architecture issues by building on target server
- Update docker-compose configurations for all environments

Deployment tested successfully:
- TeleShop: healthy at https://teleshop.silentmary.mywire.org
- TeleBot: healthy with discovery integration
- SilverPay: connectivity verified

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 12:33:46 +00:00

415 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using LittleShop.Data;
using LittleShop.DTOs;
using LittleShop.Enums;
using LittleShop.Models;
namespace LittleShop.Services;
public class BotService : IBotService
{
private readonly LittleShopContext _context;
private readonly ILogger<BotService> _logger;
public BotService(LittleShopContext context, ILogger<BotService> logger)
{
_context = context;
_logger = logger;
}
public async Task<BotRegistrationResponseDto> RegisterBotAsync(BotRegistrationDto dto)
{
_logger.LogInformation("Registering bot: {BotName} (Type: {BotType})", dto.Name, dto.Type);
// Check if a bot with the same name and type already exists
var existingBot = await _context.Bots
.FirstOrDefaultAsync(b => b.Name == dto.Name && b.Type == dto.Type);
if (existingBot != null)
{
_logger.LogInformation("Bot already exists: {BotId}. Updating existing bot instead of creating duplicate.", existingBot.Id);
// Update existing bot
existingBot.Description = dto.Description;
existingBot.Version = dto.Version;
existingBot.Settings = JsonSerializer.Serialize(dto.InitialSettings);
existingBot.PersonalityName = string.IsNullOrEmpty(dto.PersonalityName) ? existingBot.PersonalityName : dto.PersonalityName;
existingBot.Status = BotStatus.Active;
existingBot.IsActive = true;
existingBot.LastConfigSyncAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Existing bot updated: {BotId}", existingBot.Id);
return new BotRegistrationResponseDto
{
BotId = existingBot.Id,
BotKey = existingBot.BotKey,
Name = existingBot.Name,
Settings = dto.InitialSettings
};
}
// Create new bot if none exists
var botKey = await GenerateBotKeyAsync();
var bot = new Bot
{
Id = Guid.NewGuid(),
Name = dto.Name,
Description = dto.Description ?? string.Empty,
Type = dto.Type,
BotKey = botKey,
Status = BotStatus.Active,
Settings = JsonSerializer.Serialize(dto.InitialSettings),
Version = dto.Version,
PersonalityName = string.IsNullOrEmpty(dto.PersonalityName) ? AssignDefaultPersonality(dto.Name) : dto.PersonalityName,
CreatedAt = DateTime.UtcNow,
IsActive = true
};
_context.Bots.Add(bot);
await _context.SaveChangesAsync();
_logger.LogInformation("New bot registered successfully: {BotId}", bot.Id);
return new BotRegistrationResponseDto
{
BotId = bot.Id,
BotKey = botKey,
Name = bot.Name,
Settings = dto.InitialSettings
};
}
public async Task<BotDto?> AuthenticateBotAsync(string botKey)
{
var bot = await _context.Bots
.Include(b => b.Sessions)
.Include(b => b.Metrics)
.FirstOrDefaultAsync(b => b.BotKey == botKey && b.Status == BotStatus.Active);
if (bot == null)
{
_logger.LogWarning("Authentication failed for bot key: {BotKey}", botKey.Substring(0, 8) + "...");
return null;
}
// Update last seen
bot.LastSeenAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
return MapToDto(bot);
}
public async Task<BotDto?> GetBotByIdAsync(Guid id)
{
var bot = await _context.Bots
.Include(b => b.Sessions)
.Include(b => b.Metrics)
.FirstOrDefaultAsync(b => b.Id == id);
return bot != null ? MapToDto(bot) : null;
}
public async Task<BotDto?> GetBotByKeyAsync(string botKey)
{
var bot = await _context.Bots
.Include(b => b.Sessions)
.Include(b => b.Metrics)
.FirstOrDefaultAsync(b => b.BotKey == botKey);
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
{
var bots = await _context.Bots
.Include(b => b.Sessions)
.Include(b => b.Metrics)
.Where(b => b.Status != BotStatus.Deleted) // Filter out deleted bots
.OrderByDescending(b => b.CreatedAt)
.ToListAsync();
return bots.Select(MapToDto);
}
catch (Microsoft.Data.Sqlite.SqliteException ex) when (ex.Message.Contains("no such table"))
{
// Tables don't exist yet - return empty list
return new List<BotDto>();
}
}
public async Task<IEnumerable<BotDto>> GetActiveBots()
{
try
{
var bots = await _context.Bots
.Include(b => b.Sessions)
.Include(b => b.Metrics)
.Where(b => b.Status == BotStatus.Active && b.IsActive)
.OrderByDescending(b => b.LastSeenAt)
.ToListAsync();
return bots.Select(MapToDto);
}
catch (Microsoft.Data.Sqlite.SqliteException ex) when (ex.Message.Contains("no such table"))
{
// Tables don't exist yet - return empty list
return new List<BotDto>();
}
}
public async Task<bool> UpdateBotSettingsAsync(Guid botId, UpdateBotSettingsDto dto)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return false;
bot.Settings = JsonSerializer.Serialize(dto.Settings);
bot.LastConfigSyncAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Updated settings for bot {BotId}", botId);
return true;
}
public async Task<bool> UpdateBotStatusAsync(Guid botId, BotStatus status)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return false;
var oldStatus = bot.Status;
bot.Status = status;
bot.IsActive = status == BotStatus.Active;
await _context.SaveChangesAsync();
_logger.LogInformation("Bot {BotId} status changed from {OldStatus} to {NewStatus}",
botId, oldStatus, status);
return true;
}
public async Task<bool> RecordHeartbeatAsync(Guid botId, BotHeartbeatDto dto)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return false;
bot.LastSeenAt = DateTime.UtcNow;
bot.Version = dto.Version;
bot.IpAddress = dto.IpAddress;
// Record uptime metric
var uptimeMetric = new BotMetric
{
Id = Guid.NewGuid(),
BotId = botId,
MetricType = MetricType.Uptime,
Value = 1,
Metadata = JsonSerializer.Serialize(dto.Status),
RecordedAt = DateTime.UtcNow,
Category = "System",
Description = "Heartbeat"
};
_context.BotMetrics.Add(uptimeMetric);
await _context.SaveChangesAsync();
return true;
}
public async Task<bool> DeleteBotAsync(Guid botId)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return false;
bot.Status = BotStatus.Deleted;
bot.IsActive = false;
await _context.SaveChangesAsync();
_logger.LogInformation("Bot {BotId} marked as deleted", botId);
return true;
}
public async Task<Dictionary<string, object>> GetBotSettingsAsync(Guid botId)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return new Dictionary<string, object>();
try
{
return JsonSerializer.Deserialize<Dictionary<string, object>>(bot.Settings)
?? new Dictionary<string, object>();
}
catch
{
return new Dictionary<string, object>();
}
}
public async Task<bool> ValidateBotKeyAsync(string botKey)
{
return await _context.Bots.AnyAsync(b => b.BotKey == botKey && b.Status == BotStatus.Active);
}
public Task<string> GenerateBotKeyAsync()
{
const string prefix = "bot_";
const int keyLength = 32;
using var rng = RandomNumberGenerator.Create();
var bytes = new byte[keyLength];
rng.GetBytes(bytes);
var key = prefix + Convert.ToBase64String(bytes)
.Replace("+", "")
.Replace("/", "")
.Replace("=", "")
.Substring(0, keyLength);
return Task.FromResult(key);
}
public async Task<bool> UpdatePlatformInfoAsync(Guid botId, UpdatePlatformInfoDto dto)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return false;
bot.PlatformUsername = dto.PlatformUsername;
bot.PlatformDisplayName = dto.PlatformDisplayName;
bot.PlatformId = dto.PlatformId;
await _context.SaveChangesAsync();
_logger.LogInformation("Updated platform info for bot {BotId}: @{Username} ({DisplayName})",
botId, dto.PlatformUsername, dto.PlatformDisplayName);
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)
{
var settings = new Dictionary<string, object>();
try
{
settings = JsonSerializer.Deserialize<Dictionary<string, object>>(bot.Settings)
?? new Dictionary<string, object>();
}
catch { }
var activeSessions = bot.Sessions.Count(s => !s.EndedAt.HasValue);
var totalRevenue = bot.Sessions.Sum(s => s.TotalSpent);
var totalOrders = bot.Sessions.Sum(s => s.OrderCount);
return new BotDto
{
Id = bot.Id,
Name = bot.Name,
Description = bot.Description,
Type = bot.Type,
Status = bot.Status,
CreatedAt = bot.CreatedAt,
LastSeenAt = bot.LastSeenAt,
LastConfigSyncAt = bot.LastConfigSyncAt,
IsActive = bot.IsActive,
Version = bot.Version,
IpAddress = bot.IpAddress,
PlatformUsername = bot.PlatformUsername,
PlatformDisplayName = bot.PlatformDisplayName,
PlatformId = bot.PlatformId,
PersonalityName = bot.PersonalityName,
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,
ActiveSessions = activeSessions,
TotalRevenue = totalRevenue,
TotalOrders = totalOrders
};
}
private string AssignDefaultPersonality(string botName)
{
var personalities = new[] { "Alan", "Dave", "Sarah", "Mike", "Emma", "Tom" };
// Smart assignment based on name
var lowerName = botName.ToLower();
if (lowerName.Contains("alan")) return "Alan";
if (lowerName.Contains("dave")) return "Dave";
if (lowerName.Contains("sarah")) return "Sarah";
if (lowerName.Contains("mike")) return "Mike";
if (lowerName.Contains("emma")) return "Emma";
if (lowerName.Contains("tom")) return "Tom";
// Random assignment if no match
var random = new Random();
return personalities[random.Next(personalities.Length)];
}
}