351 lines
11 KiB
C#
351 lines
11 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 new bot: {BotName}", dto.Name);
|
|
|
|
var botKey = await GenerateBotKeyAsync();
|
|
|
|
var bot = new Bot
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = dto.Name,
|
|
Description = dto.Description,
|
|
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("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)
|
|
.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;
|
|
}
|
|
|
|
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,
|
|
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)];
|
|
}
|
|
} |