littleshop/LittleShop/Services/BotContactService.cs
SilverLabs DevTeam 73e8773ea3 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.
2025-09-19 12:14:39 +01:00

386 lines
15 KiB
C#

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; }
}