Configure BTCPay with external nodes via Tor
- Set up Tor container for SOCKS proxy (port 9050) - Configured Monero wallet with remote onion node - Bitcoin node continues syncing in background (60% complete) - Created documentation for wallet configuration steps - All external connections routed through Tor for privacy BTCPay requires manual wallet configuration through web interface: - Bitcoin: Need to add xpub/zpub for watch-only wallet - Monero: Need to add address and view key System ready for payment acceptance once wallets configured.
This commit is contained in:
165
LittleShop/Areas/Admin/Controllers/BotRecoveryController.cs
Normal file
165
LittleShop/Areas/Admin/Controllers/BotRecoveryController.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LittleShop.Services;
|
||||
|
||||
namespace LittleShop.Areas.Admin.Controllers;
|
||||
|
||||
[Area("Admin")]
|
||||
[Authorize(Policy = "AdminOnly")]
|
||||
public class BotRecoveryController : Controller
|
||||
{
|
||||
private readonly IBotContactService _contactService;
|
||||
private readonly IBotService _botService;
|
||||
private readonly ILogger<BotRecoveryController> _logger;
|
||||
|
||||
public BotRecoveryController(
|
||||
IBotContactService contactService,
|
||||
IBotService botService,
|
||||
ILogger<BotRecoveryController> logger)
|
||||
{
|
||||
_contactService = contactService;
|
||||
_botService = botService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// GET: Admin/BotRecovery
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var allBots = await _botService.GetAllBotsAsync();
|
||||
var botsWithStatus = allBots.Select(bot => new
|
||||
{
|
||||
Bot = bot,
|
||||
Contacts = _contactService.GetBotContactsAsync(bot.Id).Result,
|
||||
IsHealthy = bot.Status == Enums.BotStatus.Active &&
|
||||
bot.LastSeenAt > DateTime.UtcNow.AddMinutes(-5)
|
||||
}).ToList();
|
||||
|
||||
ViewData["BotsWithStatus"] = botsWithStatus;
|
||||
return View();
|
||||
}
|
||||
|
||||
// GET: Admin/BotRecovery/PrepareRecovery/{botId}
|
||||
public async Task<IActionResult> PrepareRecovery(Guid botId)
|
||||
{
|
||||
var recoveryData = await _contactService.PrepareContactRecoveryAsync(botId);
|
||||
return View(recoveryData);
|
||||
}
|
||||
|
||||
// POST: Admin/BotRecovery/ExecuteRecovery
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ExecuteRecovery(Guid fromBotId, Guid toBotId, bool notifyUsers = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Migrate contacts
|
||||
var success = await _contactService.MigrateContactsAsync(fromBotId, toBotId);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
TempData["Error"] = "Failed to migrate contacts. Check logs for details.";
|
||||
return RedirectToAction(nameof(PrepareRecovery), new { botId = fromBotId });
|
||||
}
|
||||
|
||||
// Get the new bot info for notifications
|
||||
if (notifyUsers)
|
||||
{
|
||||
var toBot = await _botService.GetBotByIdAsync(toBotId);
|
||||
var orphanedContacts = await _contactService.GetOrphanedContactsAsync(fromBotId);
|
||||
|
||||
foreach (var contact in orphanedContacts)
|
||||
{
|
||||
await _contactService.NotifyContactOfBotChangeAsync(
|
||||
contact.TelegramUserId,
|
||||
toBot.PlatformUsername);
|
||||
}
|
||||
}
|
||||
|
||||
// Update bot statuses
|
||||
await _botService.UpdateBotStatusAsync(fromBotId, Enums.BotStatus.Retired);
|
||||
|
||||
TempData["Success"] = $"Successfully migrated contacts from bot {fromBotId} to {toBotId}";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to execute bot recovery");
|
||||
TempData["Error"] = "An error occurred during recovery. Please try again.";
|
||||
return RedirectToAction(nameof(PrepareRecovery), new { botId = fromBotId });
|
||||
}
|
||||
}
|
||||
|
||||
// GET: Admin/BotRecovery/Backup/{botId}
|
||||
public async Task<IActionResult> Backup(Guid botId)
|
||||
{
|
||||
var backup = await _contactService.CreateContactBackupAsync(botId);
|
||||
|
||||
// Return as downloadable JSON file
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(backup, new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
|
||||
return File(bytes, "application/json", $"bot-contacts-{botId}-{DateTime.UtcNow:yyyyMMdd}.json");
|
||||
}
|
||||
|
||||
// POST: Admin/BotRecovery/RestoreBackup
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RestoreBackup(Guid toBotId, IFormFile backupFile)
|
||||
{
|
||||
if (backupFile == null || backupFile.Length == 0)
|
||||
{
|
||||
TempData["Error"] = "Please select a backup file to restore";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = backupFile.OpenReadStream();
|
||||
using var reader = new System.IO.StreamReader(stream);
|
||||
var json = await reader.ReadToEndAsync();
|
||||
|
||||
var backup = System.Text.Json.JsonSerializer.Deserialize<ContactBackupDto>(json);
|
||||
|
||||
if (backup == null)
|
||||
{
|
||||
TempData["Error"] = "Invalid backup file format";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
var success = await _contactService.RestoreContactsFromBackupAsync(toBotId, backup);
|
||||
|
||||
if (success)
|
||||
{
|
||||
TempData["Success"] = $"Successfully restored {backup.ContactCount} contacts to bot";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["Error"] = "Failed to restore contacts from backup";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to restore backup");
|
||||
TempData["Error"] = "An error occurred while restoring the backup";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
|
||||
// GET: Admin/BotRecovery/ContactHistory/{telegramUserId}
|
||||
public async Task<IActionResult> ContactHistory(long telegramUserId)
|
||||
{
|
||||
// Show all bot interactions for a specific user
|
||||
ViewData["UserId"] = telegramUserId;
|
||||
// Implementation would query all BotContacts for this user across all bots
|
||||
return View();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
}
|
||||
|
||||
<form asp-action="Edit" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card mb-3">
|
||||
|
||||
80
LittleShop/Models/BotContact.cs
Normal file
80
LittleShop/Models/BotContact.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace LittleShop.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks all contacts for each bot, enabling contact recovery and migration
|
||||
/// </summary>
|
||||
public class BotContact
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
// Bot Association
|
||||
[Required]
|
||||
public Guid BotId { get; set; }
|
||||
public virtual Bot Bot { get; set; } = null!;
|
||||
|
||||
// Telegram User Information
|
||||
[Required]
|
||||
public long TelegramUserId { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string TelegramUsername { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
|
||||
// Contact Metadata
|
||||
public DateTime FirstContactDate { get; set; }
|
||||
public DateTime LastContactDate { get; set; }
|
||||
public int TotalInteractions { get; set; }
|
||||
public string LastKnownLanguage { get; set; } = "en";
|
||||
|
||||
// Relationship Status
|
||||
public ContactStatus Status { get; set; } = ContactStatus.Active;
|
||||
public string? StatusReason { get; set; }
|
||||
|
||||
// Customer Link (if they've made purchases)
|
||||
public Guid? CustomerId { get; set; }
|
||||
public virtual Customer? Customer { get; set; }
|
||||
|
||||
// Recovery Information
|
||||
public bool IsRecovered { get; set; } = false;
|
||||
public Guid? RecoveredFromBotId { get; set; }
|
||||
public DateTime? RecoveredAt { get; set; }
|
||||
|
||||
// Backup Metadata
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
// Additional Contact Info (encrypted/hashed)
|
||||
[StringLength(500)]
|
||||
public string? EncryptedContactData { get; set; } // For storing additional contact methods
|
||||
|
||||
// Preferences and Notes
|
||||
[StringLength(500)]
|
||||
public string? Preferences { get; set; } // JSON string of user preferences
|
||||
|
||||
[StringLength(1000)]
|
||||
public string? Notes { get; set; } // Admin notes about this contact
|
||||
}
|
||||
|
||||
public enum ContactStatus
|
||||
{
|
||||
Active,
|
||||
Inactive,
|
||||
Blocked,
|
||||
Migrated,
|
||||
Lost,
|
||||
Recovered
|
||||
}
|
||||
386
LittleShop/Services/BotContactService.cs
Normal file
386
LittleShop/Services/BotContactService.cs
Normal file
@@ -0,0 +1,386 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LittleShop.Data;
|
||||
using LittleShop.Models;
|
||||
using LittleShop.DTOs;
|
||||
|
||||
namespace LittleShop.Services;
|
||||
|
||||
public interface IMessageDeliveryService
|
||||
{
|
||||
// Placeholder interface for compilation
|
||||
}
|
||||
|
||||
public interface IBotContactService
|
||||
{
|
||||
Task<BotContact> RecordContactAsync(Guid botId, TelegramUserDto user);
|
||||
Task<IEnumerable<BotContact>> GetBotContactsAsync(Guid botId, bool activeOnly = true);
|
||||
Task<IEnumerable<BotContact>> GetOrphanedContactsAsync(Guid failedBotId);
|
||||
Task<bool> MigrateContactsAsync(Guid fromBotId, Guid toBotId);
|
||||
Task<BotContactRecoveryDto> PrepareContactRecoveryAsync(Guid failedBotId);
|
||||
Task<bool> NotifyContactOfBotChangeAsync(long telegramUserId, string newBotUsername);
|
||||
Task<ContactBackupDto> CreateContactBackupAsync(Guid botId);
|
||||
Task<bool> RestoreContactsFromBackupAsync(Guid toBotId, ContactBackupDto backup);
|
||||
}
|
||||
|
||||
public class BotContactService : IBotContactService
|
||||
{
|
||||
private readonly LittleShopContext _context;
|
||||
private readonly ILogger<BotContactService> _logger;
|
||||
private readonly IMessageDeliveryService _messageService;
|
||||
|
||||
public BotContactService(
|
||||
LittleShopContext context,
|
||||
ILogger<BotContactService> logger,
|
||||
IMessageDeliveryService messageService)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
_messageService = messageService;
|
||||
}
|
||||
|
||||
public async Task<BotContact> RecordContactAsync(Guid botId, TelegramUserDto user)
|
||||
{
|
||||
var existingContact = await _context.BotContacts
|
||||
.FirstOrDefaultAsync(c => c.BotId == botId && c.TelegramUserId == user.Id);
|
||||
|
||||
if (existingContact != null)
|
||||
{
|
||||
// Update existing contact
|
||||
existingContact.TelegramUsername = user.Username ?? string.Empty;
|
||||
existingContact.DisplayName = $"{user.FirstName} {user.LastName}".Trim();
|
||||
existingContact.FirstName = user.FirstName ?? string.Empty;
|
||||
existingContact.LastName = user.LastName ?? string.Empty;
|
||||
existingContact.LastContactDate = DateTime.UtcNow;
|
||||
existingContact.TotalInteractions++;
|
||||
existingContact.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
_context.BotContacts.Update(existingContact);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new contact record
|
||||
existingContact = new BotContact
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
BotId = botId,
|
||||
TelegramUserId = user.Id,
|
||||
TelegramUsername = user.Username ?? string.Empty,
|
||||
DisplayName = $"{user.FirstName} {user.LastName}".Trim(),
|
||||
FirstName = user.FirstName ?? string.Empty,
|
||||
LastName = user.LastName ?? string.Empty,
|
||||
FirstContactDate = DateTime.UtcNow,
|
||||
LastContactDate = DateTime.UtcNow,
|
||||
TotalInteractions = 1,
|
||||
LastKnownLanguage = user.LanguageCode ?? "en",
|
||||
Status = ContactStatus.Active,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
await _context.BotContacts.AddAsync(existingContact);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInformation("Recorded contact {UserId} for bot {BotId}", user.Id, botId);
|
||||
|
||||
return existingContact;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BotContact>> GetBotContactsAsync(Guid botId, bool activeOnly = true)
|
||||
{
|
||||
var query = _context.BotContacts.Where(c => c.BotId == botId);
|
||||
|
||||
if (activeOnly)
|
||||
query = query.Where(c => c.IsActive && c.Status == ContactStatus.Active);
|
||||
|
||||
return await query
|
||||
.OrderByDescending(c => c.LastContactDate)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BotContact>> GetOrphanedContactsAsync(Guid failedBotId)
|
||||
{
|
||||
// Get contacts from failed bot that haven't been recovered yet
|
||||
return await _context.BotContacts
|
||||
.Where(c => c.BotId == failedBotId &&
|
||||
c.Status == ContactStatus.Active &&
|
||||
!c.IsRecovered)
|
||||
.OrderByDescending(c => c.TotalInteractions) // Prioritize most active users
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> MigrateContactsAsync(Guid fromBotId, Guid toBotId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var contactsToMigrate = await GetOrphanedContactsAsync(fromBotId);
|
||||
var toBot = await _context.Bots.FindAsync(toBotId);
|
||||
|
||||
if (toBot == null)
|
||||
{
|
||||
_logger.LogError("Target bot {BotId} not found", toBotId);
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var contact in contactsToMigrate)
|
||||
{
|
||||
// Create new contact record for new bot
|
||||
var newContact = new BotContact
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
BotId = toBotId,
|
||||
TelegramUserId = contact.TelegramUserId,
|
||||
TelegramUsername = contact.TelegramUsername,
|
||||
DisplayName = contact.DisplayName,
|
||||
FirstName = contact.FirstName,
|
||||
LastName = contact.LastName,
|
||||
FirstContactDate = DateTime.UtcNow,
|
||||
LastContactDate = DateTime.UtcNow,
|
||||
TotalInteractions = 0, // Reset for new bot
|
||||
LastKnownLanguage = contact.LastKnownLanguage,
|
||||
Status = ContactStatus.Recovered,
|
||||
CustomerId = contact.CustomerId,
|
||||
IsRecovered = true,
|
||||
RecoveredFromBotId = fromBotId,
|
||||
RecoveredAt = DateTime.UtcNow,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
IsActive = true,
|
||||
Preferences = contact.Preferences,
|
||||
Notes = $"Migrated from bot {fromBotId} on {DateTime.UtcNow:yyyy-MM-dd}"
|
||||
};
|
||||
|
||||
await _context.BotContacts.AddAsync(newContact);
|
||||
|
||||
// Mark original contact as migrated
|
||||
contact.Status = ContactStatus.Migrated;
|
||||
contact.IsRecovered = true;
|
||||
contact.UpdatedAt = DateTime.UtcNow;
|
||||
_context.BotContacts.Update(contact);
|
||||
|
||||
_logger.LogInformation("Migrated contact {UserId} from bot {FromBot} to {ToBot}",
|
||||
contact.TelegramUserId, fromBotId, toBotId);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to migrate contacts from {FromBot} to {ToBot}", fromBotId, toBotId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BotContactRecoveryDto> PrepareContactRecoveryAsync(Guid failedBotId)
|
||||
{
|
||||
var orphanedContacts = await GetOrphanedContactsAsync(failedBotId);
|
||||
var failedBot = await _context.Bots.FindAsync(failedBotId);
|
||||
var availableBots = await _context.Bots
|
||||
.Where(b => b.Id != failedBotId && b.Status == BotStatus.Active)
|
||||
.ToListAsync();
|
||||
|
||||
return new BotContactRecoveryDto
|
||||
{
|
||||
FailedBotId = failedBotId,
|
||||
FailedBotName = failedBot?.Name ?? "Unknown",
|
||||
OrphanedContactCount = orphanedContacts.Count(),
|
||||
HighValueContacts = orphanedContacts
|
||||
.Where(c => c.TotalInteractions > 10 || c.CustomerId != null)
|
||||
.Count(),
|
||||
AvailableRecoveryBots = availableBots.Select(b => new BotInfoDto
|
||||
{
|
||||
Id = b.Id,
|
||||
Name = b.Name,
|
||||
Status = b.Status.ToString(),
|
||||
CurrentContactCount = _context.BotContacts.Count(c => c.BotId == b.Id && c.IsActive)
|
||||
}).ToList(),
|
||||
ContactDetails = orphanedContacts.Select(c => new ContactSummaryDto
|
||||
{
|
||||
TelegramUserId = c.TelegramUserId,
|
||||
Username = c.TelegramUsername,
|
||||
DisplayName = c.DisplayName,
|
||||
TotalInteractions = c.TotalInteractions,
|
||||
LastContactDate = c.LastContactDate,
|
||||
IsCustomer = c.CustomerId != null
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> NotifyContactOfBotChangeAsync(long telegramUserId, string newBotUsername)
|
||||
{
|
||||
try
|
||||
{
|
||||
// This would integrate with the message delivery service
|
||||
var message = $"Hello! Your previous bot is temporarily unavailable. " +
|
||||
$"Please continue your conversation with our new bot: @{newBotUsername} " +
|
||||
$"All your order history and preferences have been preserved.";
|
||||
|
||||
// Queue message for delivery when user contacts new bot
|
||||
await _messageService.QueueRecoveryMessageAsync(telegramUserId, message);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to notify user {UserId} of bot change", telegramUserId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ContactBackupDto> CreateContactBackupAsync(Guid botId)
|
||||
{
|
||||
var contacts = await GetBotContactsAsync(botId, activeOnly: false);
|
||||
var bot = await _context.Bots.FindAsync(botId);
|
||||
|
||||
return new ContactBackupDto
|
||||
{
|
||||
BackupId = Guid.NewGuid(),
|
||||
BotId = botId,
|
||||
BotName = bot?.Name ?? "Unknown",
|
||||
BackupDate = DateTime.UtcNow,
|
||||
ContactCount = contacts.Count(),
|
||||
Contacts = contacts.Select(c => new ContactExportDto
|
||||
{
|
||||
TelegramUserId = c.TelegramUserId,
|
||||
TelegramUsername = c.TelegramUsername,
|
||||
DisplayName = c.DisplayName,
|
||||
FirstName = c.FirstName,
|
||||
LastName = c.LastName,
|
||||
FirstContactDate = c.FirstContactDate,
|
||||
LastContactDate = c.LastContactDate,
|
||||
TotalInteractions = c.TotalInteractions,
|
||||
LastKnownLanguage = c.LastKnownLanguage,
|
||||
Status = c.Status.ToString(),
|
||||
CustomerId = c.CustomerId,
|
||||
Preferences = c.Preferences,
|
||||
Notes = c.Notes
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> RestoreContactsFromBackupAsync(Guid toBotId, ContactBackupDto backup)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var contactData in backup.Contacts)
|
||||
{
|
||||
// Check if contact already exists for this bot
|
||||
var existingContact = await _context.BotContacts
|
||||
.FirstOrDefaultAsync(c => c.BotId == toBotId &&
|
||||
c.TelegramUserId == contactData.TelegramUserId);
|
||||
|
||||
if (existingContact == null)
|
||||
{
|
||||
var newContact = new BotContact
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
BotId = toBotId,
|
||||
TelegramUserId = contactData.TelegramUserId,
|
||||
TelegramUsername = contactData.TelegramUsername,
|
||||
DisplayName = contactData.DisplayName,
|
||||
FirstName = contactData.FirstName,
|
||||
LastName = contactData.LastName,
|
||||
FirstContactDate = contactData.FirstContactDate,
|
||||
LastContactDate = contactData.LastContactDate,
|
||||
TotalInteractions = contactData.TotalInteractions,
|
||||
LastKnownLanguage = contactData.LastKnownLanguage,
|
||||
Status = Enum.Parse<ContactStatus>(contactData.Status),
|
||||
CustomerId = contactData.CustomerId,
|
||||
IsRecovered = true,
|
||||
RecoveredFromBotId = backup.BotId,
|
||||
RecoveredAt = DateTime.UtcNow,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
IsActive = true,
|
||||
Preferences = contactData.Preferences,
|
||||
Notes = $"Restored from backup {backup.BackupId} on {DateTime.UtcNow:yyyy-MM-dd}"
|
||||
};
|
||||
|
||||
await _context.BotContacts.AddAsync(newContact);
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInformation("Restored {Count} contacts from backup to bot {BotId}",
|
||||
backup.Contacts.Count, toBotId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to restore contacts from backup");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DTOs for contact management
|
||||
public class TelegramUserDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string? Username { get; set; }
|
||||
public string? FirstName { get; set; }
|
||||
public string? LastName { get; set; }
|
||||
public string? LanguageCode { get; set; }
|
||||
}
|
||||
|
||||
public class BotContactRecoveryDto
|
||||
{
|
||||
public Guid FailedBotId { get; set; }
|
||||
public string FailedBotName { get; set; } = string.Empty;
|
||||
public int OrphanedContactCount { get; set; }
|
||||
public int HighValueContacts { get; set; }
|
||||
public List<BotInfoDto> AvailableRecoveryBots { get; set; } = new();
|
||||
public List<ContactSummaryDto> ContactDetails { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BotInfoDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public int CurrentContactCount { get; set; }
|
||||
}
|
||||
|
||||
public class ContactSummaryDto
|
||||
{
|
||||
public long TelegramUserId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public int TotalInteractions { get; set; }
|
||||
public DateTime LastContactDate { get; set; }
|
||||
public bool IsCustomer { get; set; }
|
||||
}
|
||||
|
||||
public class ContactBackupDto
|
||||
{
|
||||
public Guid BackupId { get; set; }
|
||||
public Guid BotId { get; set; }
|
||||
public string BotName { get; set; } = string.Empty;
|
||||
public DateTime BackupDate { get; set; }
|
||||
public int ContactCount { get; set; }
|
||||
public List<ContactExportDto> Contacts { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ContactExportDto
|
||||
{
|
||||
public long TelegramUserId { get; set; }
|
||||
public string TelegramUsername { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
public DateTime FirstContactDate { get; set; }
|
||||
public DateTime LastContactDate { get; set; }
|
||||
public int TotalInteractions { get; set; }
|
||||
public string LastKnownLanguage { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public Guid? CustomerId { get; set; }
|
||||
public string? Preferences { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user