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:
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