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:
SilverLabs DevTeam
2025-09-19 12:14:39 +01:00
parent 36b393dd2e
commit 73e8773ea3
195 changed files with 77086 additions and 198 deletions

View 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();
}
}

View File

@@ -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">

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

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