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

29
.env.hostinger.template Normal file
View File

@ -0,0 +1,29 @@
# LittleShop + TeleBot Hostinger Deployment Configuration
# Copy this to .env and fill in your values
# BTCPay Server Configuration (Hostinger deployment)
BTCPAY_API_KEY=994589c8b514531f867dd24c83a02b6381a5f4a2
BTCPAY_STORE_ID=AoxXjM9NJT6P9C1MErkaawXaSchz8sFPYdQ9FyhmQz33
BTCPAY_WEBHOOK_SECRET=generate-a-secure-webhook-secret-here
# Telegram Bot Configuration
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
TELEGRAM_ADMIN_CHAT_ID=your-telegram-admin-chat-id
# LittleShop Admin Credentials
LITTLESHOP_USERNAME=admin
LITTLESHOP_PASSWORD=admin
# Brand Name
BRAND_NAME=Little Shop
# Security Keys
JWT_SECRET_KEY=YourSuperSecretKeyThatIsAtLeast32CharactersLong!
DATABASE_ENCRYPTION_KEY=CHANGE_THIS_KEY_IN_PRODUCTION_32CHAR
# Redis Configuration
REDIS_ENABLED=false
REDIS_PASSWORD=RedisPassword123
# Hangfire Configuration
HANGFIRE_ENABLED=false

View File

@ -0,0 +1,113 @@
# BTCPay External Nodes Configuration Status
## ✅ Configuration Completed
### Infrastructure Setup
1. **Tor Container**: ✅ Running and connected (port 9050)
2. **Bitcoin Node**: ✅ Running but only 60% synced (continuing in background)
3. **Monero Remote Wallet**: ✅ Running with Tor onion node connection
4. **BTCPay Server**: ✅ Running and accessible
### Current Configuration
#### Tor Network
- **Status**: Fully operational
- **SOCKS Port**: 9050
- **Container**: tor (running)
- **Bootstrap**: 100% complete
#### Bitcoin Setup
- **Local Node**: Running (60% synced, July 2022)
- **Container**: btcpayserver_bitcoind
- **Pruned Mode**: 10GB max storage
- **Will sync in background**: ~2-3 weeks to complete
#### Monero Setup
- **Remote Node Used**: `zbjkbsxc5munw3qusl7j2hpcmikhqocdf4pqhnhtpzw5nt5jrmofptid.onion:18089`
- **Connection**: Via Tor SOCKS proxy
- **Container**: monero_wallet_remote
- **RPC Port**: 18082
- **Credentials**: btcpay:btcpay
## ⚠️ Manual Steps Required in BTCPay Admin
To complete the setup and start accepting payments, you need to configure wallets through the BTCPay web interface:
### For Bitcoin (Immediate Setup - No Sync Required)
1. **Login to BTCPay**: https://thebankofdebbie.giize.com
2. **Navigate to**: Store Settings → Wallets → Bitcoin
3. **Choose Setup Method**:
- Click "Connect an existing wallet"
- Select "Enter extended public key"
4. **Enter Your xpub/zpub**:
- For testing: `xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB`
- For production: Generate from your hardware wallet or Bitcoin Core
5. **Configure Derivation**:
- Account Key Path: `m/84'/0'/0'` (for native segwit)
- Or use Legacy: `m/44'/0'/0'`
6. **Save Configuration**
### For Monero (Requires View Key)
1. **Navigate to**: Store Settings → Wallets → Monero
2. **Configure Wallet**:
- Primary Address: Your Monero address
- View Key: Your private view key (for incoming payment detection)
3. **Save Configuration**
## Alternative: Use External Services (Fastest)
If you need immediate operation without any node syncing:
### Option 1: BTCPay Server Demo (Immediate)
1. Use BTCPay's demo server: https://testnet.demo.btcpayserver.org
2. Create account and store
3. Update LittleShop configuration with new credentials
### Option 2: Third-Party BTCPay Host
- Voltage: https://voltage.cloud
- LunaNode: https://www.lunanode.com
- BTCPayJungle: https://btcpayjungle.com
### Option 3: Electrum Personal Server
1. Install Electrum Personal Server locally
2. Connect it to public Electrum servers via Tor
3. Configure BTCPay to use your EPS instance
## Current Payment Status
- **Bitcoin**: ❌ Requires wallet configuration (xpub/zpub)
- **Monero**: ❌ Requires wallet configuration (address + view key)
- **API Connection**: ✅ Working
- **Invoice Creation**: Will work after wallet setup
## Monitoring Commands
```bash
# Check Bitcoin sync progress
ssh -i /silverlabs/src/LittleShop/Hostinger/vps_hardening_key -p 2255 sysadmin@thebankofdebbie.giize.com \
"docker logs btcpayserver_bitcoind --tail 5 | grep progress"
# Check Tor status
ssh -i /silverlabs/src/LittleShop/Hostinger/vps_hardening_key -p 2255 sysadmin@thebankofdebbie.giize.com \
"docker logs tor --tail 5"
# Check Monero wallet
ssh -i /silverlabs/src/LittleShop/Hostinger/vps_hardening_key -p 2255 sysadmin@thebankofdebbie.giize.com \
"docker logs monero_wallet_remote --tail 5"
# Test BTCPay connection
curl https://thebankofdebbie.giize.com/api/v1/stores \
-H "Authorization: token db920209c0101efdbd1c6b6d1c99a48e3ba9d0de"
```
## Summary
The infrastructure is ready with external node connectivity via Tor. However, BTCPay requires wallet configuration through its web interface to start accepting payments. You can either:
1. **Configure watch-only wallets** (xpub for Bitcoin, view key for Monero) - Immediate
2. **Wait for local Bitcoin sync** to complete (~2-3 weeks) - Most private
3. **Use external BTCPay service** temporarily - Fastest
The system is designed for privacy with all external connections routed through Tor.

220
HOSTINGER-DEPLOYMENT.md Normal file
View File

@ -0,0 +1,220 @@
# Hostinger Deployment Guide for LittleShop + TeleBot
## Overview
This guide explains how to deploy LittleShop and TeleBot to Hostinger VPS with BTCPay Server integration.
## Current BTCPay Server Setup
- **URL**: https://thebankofdebbie.giize.com
- **Host**: srv1002428.hstgr.cloud
- **Cryptocurrencies**: BTC, DOGE, XMR, ETH, ZEC
- **API Key**: 994589c8b514531f867dd24c83a02b6381a5f4a2
- **Store ID**: AoxXjM9NJT6P9C1MErkaawXaSchz8sFPYdQ9FyhmQz33
## Deployment Files Created
### 1. `appsettings.Hostinger.json`
- Production configuration for LittleShop
- Points to Hostinger BTCPay Server
- Uses local SQLite database
### 2. `docker-compose.hostinger.yml`
- Combined deployment for LittleShop + TeleBot
- Network configuration for container communication
- Volume persistence for data
### 3. `.env.hostinger.template`
- Environment variables template
- Copy to `.env` and configure
### 4. `deploy-to-hostinger.sh`
- Automated deployment script
- Builds and starts all services
- Health checks included
## Step-by-Step Deployment
### 1. Connect to Hostinger VPS
```bash
ssh root@srv1002428.hstgr.cloud -p 2255
```
### 2. Clone/Update Repository
```bash
cd /root
git clone https://git.silverlabs.uk/SilverLABS/LittleShop.git
# OR if already exists
cd LittleShop
git pull
```
### 3. Configure Environment
```bash
cp .env.hostinger.template .env
nano .env
```
Required configurations:
- `TELEGRAM_BOT_TOKEN` - Your Telegram bot token
- `TELEGRAM_ADMIN_CHAT_ID` - Your Telegram chat ID for admin messages
- `BTCPAY_WEBHOOK_SECRET` - Generate a secure random string (32+ chars)
### 4. Run Deployment Script
```bash
./deploy-to-hostinger.sh
```
## BTCPay Webhook Configuration
After deployment, configure the webhook in BTCPay Server:
1. Log into BTCPay: https://thebankofdebbie.giize.com
2. Go to Store Settings → Webhooks
3. Add new webhook:
- **URL**: `http://srv1002428.hstgr.cloud:8080/api/orders/payments/webhook`
- **Secret**: Use the same value as `BTCPAY_WEBHOOK_SECRET` in `.env`
- **Events**: Select all payment-related events
## Troubleshooting
### Problem: TeleBot can't create orders
**Solution**: Check BTCPay connection
```bash
# Check LittleShop logs
docker logs littleshop | grep -i btcpay
# Test BTCPay connectivity from container
docker exec littleshop curl https://thebankofdebbie.giize.com/api/v1/health
```
### Problem: Payment creation fails
**Possible causes**:
1. Wrong BTCPay API key or Store ID
2. Currency not configured in BTCPay
3. Network connectivity issues
**Debug steps**:
```bash
# Check detailed error logs
docker logs littleshop | grep -i error
# Verify environment variables
docker exec littleshop env | grep BTCPAY
# Test API directly
curl -X GET https://thebankofdebbie.giize.com/api/v1/stores \
-H "Authorization: token 994589c8b514531f867dd24c83a02b6381a5f4a2"
```
### Problem: TeleBot not responding
**Solution**: Check bot registration
```bash
# Check TeleBot logs
docker logs littleshop-telebot
# Verify bot token
docker exec littleshop-telebot env | grep TELEGRAM
# Restart bot
docker restart littleshop-telebot
```
### Problem: Orders stuck in "PendingPayment"
**Solution**: Check webhook configuration
```bash
# Monitor webhook delivery
docker logs littleshop | grep webhook
# Test webhook manually
curl -X POST http://localhost:8080/api/orders/payments/webhook \
-H "Content-Type: application/json" \
-H "BTCPay-Sig: sha256=test" \
-d '{"test": true}'
```
## Service URLs
### External Access
- **BTCPay Server**: https://thebankofdebbie.giize.com
- **LittleShop API**: http://srv1002428.hstgr.cloud:8080
- **Telegram Bot**: Search for your bot on Telegram
### Internal Container Network
- **LittleShop**: http://littleshop:8080
- **Redis**: redis:6379
## Monitoring
### View Real-time Logs
```bash
# All services
docker-compose -f docker-compose.hostinger.yml logs -f
# Specific service
docker logs -f littleshop
docker logs -f littleshop-telebot
```
### Check Service Status
```bash
docker-compose -f docker-compose.hostinger.yml ps
```
### Resource Usage
```bash
docker stats
```
## Backup
### Database Backup
```bash
# Backup LittleShop database
docker exec littleshop sqlite3 /app/data/littleshop.db ".backup /app/data/backup.db"
docker cp littleshop:/app/data/backup.db ./littleshop-backup-$(date +%Y%m%d).db
# Backup TeleBot database
docker exec littleshop-telebot sqlite3 /app/data/telebot.db ".backup /app/data/backup.db"
docker cp littleshop-telebot:/app/data/backup.db ./telebot-backup-$(date +%Y%m%d).db
```
### Full Volume Backup
```bash
# Stop services
docker-compose -f docker-compose.hostinger.yml down
# Backup volumes
docker run --rm -v littleshop_data:/data -v $(pwd):/backup alpine tar czf /backup/littleshop-data-$(date +%Y%m%d).tar.gz -C /data .
# Restart services
docker-compose -f docker-compose.hostinger.yml up -d
```
## Update Deployment
To update to latest version:
```bash
# Pull latest code
git pull
# Rebuild and restart
./deploy-to-hostinger.sh
```
## Security Considerations
1. **Change default passwords** in production
2. **Use strong webhook secret** (32+ random characters)
3. **Enable firewall** for port 8080 if exposing externally
4. **Regular backups** of databases
5. **Monitor logs** for suspicious activity
## Support Currencies
Currently configured in BTCPay:
- BTC (Bitcoin)
- DOGE (Dogecoin)
- XMR (Monero)
- ETH (Ethereum)
- ZEC (Zcash)
To add more currencies, configure them in BTCPay Server first.

View File

@ -12,6 +12,7 @@ public class Product
public string? CategoryName { get; set; } public string? CategoryName { get; set; }
public bool IsActive { get; set; } public bool IsActive { get; set; }
public List<ProductPhoto> Photos { get; set; } = new(); public List<ProductPhoto> Photos { get; set; } = new();
public List<ProductVariation> Variations { get; set; } = new();
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; } public DateTime UpdatedAt { get; set; }
} }
@ -23,3 +24,16 @@ public class ProductPhoto
public string? AltText { get; set; } public string? AltText { get; set; }
public int SortOrder { get; set; } public int SortOrder { get; set; }
} }
public class ProductVariation
{
public Guid Id { get; set; }
public Guid ProductId { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal PricePerUnit { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; }
}

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"> <form asp-action="Edit" method="post">
@Html.AntiForgeryToken()
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">
<div class="card mb-3"> <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; }
}

131
TeleBot/.env.multi.example Normal file
View File

@ -0,0 +1,131 @@
# LittleShop Multi-Bot Environment Configuration Template
# Copy this file to .env.multi and configure your values
# ========================================
# SHARED CONFIGURATION (All Bots)
# ========================================
# LittleShop API Configuration
LITTLESHOP_API_URL=http://localhost:8080
# For remote API: https://api.yourdomain.com
# For Docker host: http://host.docker.internal:8080
# For same network: http://littleshop-api:8080
LITTLESHOP_USERNAME=admin
LITTLESHOP_PASSWORD=admin
# Redis Configuration (Optional - for shared caching)
REDIS_ENABLED=false
REDIS_PASSWORD=your_secure_redis_password_here
# ========================================
# SUPPORT BOT CONFIGURATION
# ========================================
# Telegram Bot Token from @BotFather
SUPPORT_BOT_TOKEN=
# Admin Chat ID for notifications (get from @userinfobot)
SUPPORT_ADMIN_CHAT_ID=
# Bot API Key from LittleShop Admin Panel
SUPPORT_BOT_API_KEY=
# 32-character encryption key for database
SUPPORT_DB_ENCRYPTION_KEY=change_this_to_32_char_secure_key
# ========================================
# SALES & MARKETING BOT CONFIGURATION
# ========================================
# Telegram Bot Token from @BotFather
SALES_BOT_TOKEN=
# Admin Chat ID for notifications
SALES_ADMIN_CHAT_ID=
# Bot API Key from LittleShop Admin Panel
SALES_BOT_API_KEY=
# 32-character encryption key for database
SALES_DB_ENCRYPTION_KEY=change_this_to_32_char_secure_key
# ========================================
# VIP/PREMIUM BOT CONFIGURATION
# ========================================
# Telegram Bot Token from @BotFather
VIP_BOT_TOKEN=
# Admin Chat ID for notifications
VIP_ADMIN_CHAT_ID=
# Bot API Key from LittleShop Admin Panel
VIP_BOT_API_KEY=
# 32-character encryption key for database
VIP_DB_ENCRYPTION_KEY=change_this_to_32_char_secure_key
# Enhanced privacy for VIP customers
VIP_ENABLE_TOR=false
# ========================================
# EU REGION BOT CONFIGURATION (Optional)
# ========================================
# Telegram Bot Token from @BotFather
EU_BOT_TOKEN=
# Admin Chat ID for notifications
EU_ADMIN_CHAT_ID=
# Bot API Key from LittleShop Admin Panel
EU_BOT_API_KEY=
# 32-character encryption key for database
EU_DB_ENCRYPTION_KEY=change_this_to_32_char_secure_key
# Optional: Different API endpoint for EU region
EU_API_URL=${LITTLESHOP_API_URL}
# ========================================
# DEPLOYMENT NOTES
# ========================================
# To generate secure encryption keys:
# openssl rand -hex 16 # Generates 32-character hex string
# or
# cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1
# To get your Telegram Chat ID:
# 1. Message @userinfobot on Telegram
# 2. It will reply with your user info including chat ID
# To create bot tokens:
# 1. Message @BotFather on Telegram
# 2. Send /newbot and follow instructions
# 3. Copy the token provided
# To get Bot API Keys:
# 1. Login to LittleShop Admin Panel
# 2. Go to /Admin/Bots/Wizard
# 3. Create bot and copy the API key
# ========================================
# DOCKER DEPLOYMENT COMMANDS
# ========================================
# Deploy all bots:
# docker-compose -f docker-compose.multi.yml --env-file .env.multi up -d
# Deploy specific bot:
# docker-compose -f docker-compose.multi.yml --env-file .env.multi up -d bot-support
# View logs:
# docker-compose -f docker-compose.multi.yml logs -f bot-support
# Stop all bots:
# docker-compose -f docker-compose.multi.yml down
# Remove all data (CAUTION):
# docker-compose -f docker-compose.multi.yml down -v

View File

@ -0,0 +1,504 @@
# 🚀 Multi-Host Docker Bot Deployment Guide
Deploy multiple LittleShop TeleBots across different Docker hosts for scalability, redundancy, and geographic distribution.
## 📋 Table of Contents
- [Overview](#overview)
- [Architecture](#architecture)
- [Quick Start](#quick-start)
- [Deployment Methods](#deployment-methods)
- [Configuration](#configuration)
- [Management](#management)
- [Security](#security)
- [Troubleshooting](#troubleshooting)
## Overview
The LittleShop TeleBot system supports deploying multiple bot instances across different Docker hosts. Each bot:
- Runs independently in its own container
- Connects to the central LittleShop API
- Has its own Telegram bot token and personality
- Can serve different customer segments or regions
## Architecture
```
┌─────────────────────────────────────────────────┐
│ Central LittleShop API │
│ (http://api.littleshop.com) │
└─────────────┬─────────┬─────────┬───────────────┘
│ │ │
┌────▼───┐ ┌──▼───┐ ┌──▼───┐
│Docker │ │Docker│ │Docker│
│Host A │ │Host B│ │Host C│
└────────┘ └──────┘ └──────┘
Support Sales VIP Bots
Bot Bot
```
## Quick Start
### 1. Build Docker Image
```bash
# Build the image locally
cd /silverlabs/src/LittleShop
docker build -f TeleBot/TeleBot/Dockerfile -t littleshop/telebot:latest .
# Push to registry (Docker Hub, GitLab, or private)
docker push littleshop/telebot:latest
```
### 2. Create Bot in Admin Panel
1. Navigate to `http://localhost:8080/Admin/Bots/Wizard`
2. Follow the wizard to create a bot with @BotFather
3. Save the bot token and API key
### 3. Deploy Bot
```bash
# Using the deployment script
./TeleBot/deploy-bot.sh \
-n my-bot \
-t "YOUR_BOT_TOKEN" \
-k "YOUR_API_KEY" \
-a "https://api.littleshop.com"
# Or using Docker directly
docker run -d \
--name littleshop-bot-sales \
-e Telegram__BotToken=YOUR_TOKEN \
-e LittleShop__ApiUrl=https://api.shop.com \
littleshop/telebot:latest
```
## Deployment Methods
### Method 1: Deployment Script
The `deploy-bot.sh` script provides an easy way to deploy bots:
```bash
# Deploy to local Docker
./deploy-bot.sh -n support-bot -t "TOKEN" -k "API_KEY"
# Deploy to remote host
./deploy-bot.sh -n sales-bot -t "TOKEN" -h ssh://user@server.com
# Deploy with all options
./deploy-bot.sh \
-n vip-bot \
-t "BOT_TOKEN" \
-k "API_KEY" \
-a https://api.shop.com \
-c "ADMIN_CHAT_ID" \
-e "32_CHAR_ENCRYPTION_KEY" \
-p "Sarah" \
-m strict \
--pull --rm
```
### Method 2: Docker Compose (Multiple Bots)
Deploy multiple bots on the same host:
```bash
# Copy and configure environment file
cp .env.multi.example .env.multi
# Edit .env.multi with your values
# Deploy all bots
docker-compose -f docker-compose.multi.yml --env-file .env.multi up -d
# Deploy specific bot
docker-compose -f docker-compose.multi.yml --env-file .env.multi up -d bot-support
# View logs
docker-compose -f docker-compose.multi.yml logs -f
```
### Method 3: Portainer Stack
1. **Add Template to Portainer**:
- Go to **App Templates** → **Custom Templates**
- Add the content from `portainer-template.json`
2. **Deploy from Template**:
- Go to **App Templates**
- Select "LittleShop TeleBot"
- Fill in environment variables
- Deploy
3. **Or Deploy Stack Directly**:
```bash
curl -X POST \
http://portainer:9000/api/stacks \
-H "X-API-Key: YOUR_PORTAINER_KEY" \
-F "Name=littleshop-bot" \
-F "StackFileContent=@docker-compose.yml" \
-F "Env=@.env"
```
### Method 4: Docker Swarm
Deploy across a Docker Swarm cluster:
```bash
# Initialize swarm (if not already)
docker swarm init
# Create secrets
echo "YOUR_TOKEN" | docker secret create bot_token -
echo "YOUR_API_KEY" | docker secret create bot_api_key -
# Deploy service
docker service create \
--name littleshop-bot \
--secret bot_token \
--secret bot_api_key \
--replicas 3 \
--env Telegram__BotToken_FILE=/run/secrets/bot_token \
--env BotManager__ApiKey_FILE=/run/secrets/bot_api_key \
littleshop/telebot:latest
```
## Configuration
### Environment Variables
| Variable | Description | Required |
|----------|-------------|----------|
| `Telegram__BotToken` | Bot token from @BotFather | ✅ |
| `LittleShop__ApiUrl` | LittleShop API endpoint | ✅ |
| `BotManager__ApiKey` | API key from admin panel | ⭕ |
| `Telegram__AdminChatId` | Admin notifications chat | ⭕ |
| `Database__EncryptionKey` | 32-char encryption key | ⭕ |
| `Privacy__Mode` | strict/moderate/relaxed | ⭕ |
### Bot Personalities
Configure different personalities for different bots:
```yaml
# Support Bot - Helpful and professional
Bot__PersonalityName: "Alan"
Bot__Tone: "professional"
# Sales Bot - Enthusiastic and engaging
Bot__PersonalityName: "Sarah"
Bot__Tone: "friendly"
# VIP Bot - Discrete and sophisticated
Bot__PersonalityName: "Emma"
Bot__Tone: "formal"
```
### Network Configuration
#### Same Network as API
```yaml
LittleShop__ApiUrl: http://littleshop-api:8080
```
#### Docker Host Network
```yaml
LittleShop__ApiUrl: http://host.docker.internal:8080
```
#### External API
```yaml
LittleShop__ApiUrl: https://api.littleshop.com
```
## Management
### Monitor Bots
```bash
# List all bot containers
docker ps --filter "label=com.littleshop.bot=true"
# Check bot health
docker inspect littleshop-bot-support --format='{{.State.Health.Status}}'
# View logs
docker logs -f littleshop-bot-support --tail 100
# Get metrics
docker stats littleshop-bot-support
```
### Update Bots
```bash
# Pull latest image
docker pull littleshop/telebot:latest
# Recreate container with new image
docker-compose -f docker-compose.multi.yml pull
docker-compose -f docker-compose.multi.yml up -d
# Or using deployment script
./deploy-bot.sh -n my-bot -t "TOKEN" --pull --rm
```
### Backup & Restore
```bash
# Backup bot data
docker run --rm \
-v littleshop-bot-support-data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/bot-backup.tar.gz /data
# Restore bot data
docker run --rm \
-v littleshop-bot-support-data:/data \
-v $(pwd):/backup \
alpine tar xzf /backup/bot-backup.tar.gz -C /
```
## Deployment Scenarios
### Scenario 1: Geographic Distribution
Deploy bots closer to users for better latency:
```bash
# EU Bot on European server
ssh eu-server "docker run -d --name bot-eu \
-e Telegram__BotToken=$EU_TOKEN \
-e LittleShop__ApiUrl=https://api.shop.com \
-e TZ=Europe/London \
littleshop/telebot:latest"
# US Bot on US server
ssh us-server "docker run -d --name bot-us \
-e Telegram__BotToken=$US_TOKEN \
-e LittleShop__ApiUrl=https://api.shop.com \
-e TZ=America/New_York \
littleshop/telebot:latest"
# Asia Bot on Singapore server
ssh asia-server "docker run -d --name bot-asia \
-e Telegram__BotToken=$ASIA_TOKEN \
-e LittleShop__ApiUrl=https://api.shop.com \
-e TZ=Asia/Singapore \
littleshop/telebot:latest"
```
### Scenario 2: Customer Segmentation
Different bots for different customer types:
```yaml
# docker-compose.segments.yml
services:
bot-public:
image: littleshop/telebot:latest
environment:
- Privacy__Mode=moderate
- Features__EnableAnalytics=true
bot-vip:
image: littleshop/telebot:latest
environment:
- Privacy__Mode=strict
- Privacy__EnableTor=true
- Features__EnableOrderMixing=true
bot-wholesale:
image: littleshop/telebot:latest
environment:
- Features__BulkOrdering=true
- Features__B2BPricing=true
```
### Scenario 3: A/B Testing
Deploy multiple versions for testing:
```bash
# Version A - Standard features
docker run -d --name bot-version-a \
-e EXPERIMENT_VERSION=A \
littleshop/telebot:latest
# Version B - New features
docker run -d --name bot-version-b \
-e EXPERIMENT_VERSION=B \
-e Features__NewCheckout=true \
littleshop/telebot:v2-beta
```
## Security
### Best Practices
1. **Use Docker Secrets** for sensitive data:
```bash
echo "TOKEN" | docker secret create bot_token -
docker service create --secret bot_token ...
```
2. **Network Isolation**:
```yaml
networks:
frontend:
driver: overlay
encrypted: true
backend:
driver: overlay
internal: true
```
3. **Resource Limits**:
```yaml
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
```
4. **Health Checks**:
```yaml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
```
### TLS/SSL Configuration
For production deployments with TLS:
```bash
docker run -d \
-v /path/to/certs:/certs:ro \
-e ASPNETCORE_Kestrel__Certificates__Default__Path=/certs/cert.pfx \
-e ASPNETCORE_Kestrel__Certificates__Default__Password=CERT_PASSWORD \
-e ASPNETCORE_URLS="https://+:443" \
littleshop/telebot:latest
```
## Troubleshooting
### Common Issues
#### Bot Won't Start
```bash
# Check logs
docker logs littleshop-bot --tail 50
# Common causes:
# - Invalid bot token
# - Can't reach API
# - Port already in use
```
#### Can't Connect to API
```bash
# Test connectivity from container
docker exec littleshop-bot curl http://api-url/health
# Check DNS resolution
docker exec littleshop-bot nslookup api.shop.com
```
#### High Memory Usage
```bash
# Check memory stats
docker stats littleshop-bot
# Limit memory
docker update --memory 512m littleshop-bot
```
### Debug Mode
Enable debug logging:
```bash
docker run -d \
-e Logging__LogLevel__Default=Debug \
-e Logging__LogLevel__TeleBot=Trace \
littleshop/telebot:latest
```
### Performance Monitoring
```bash
# CPU and Memory usage
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
# Network I/O
docker exec littleshop-bot netstat -i
# Disk usage
docker system df -v
```
## Advanced Topics
### Custom Docker Networks
Create isolated networks for different bot groups:
```bash
# Create networks
docker network create bots-support --driver overlay
docker network create bots-sales --driver overlay
docker network create bots-vip --driver overlay --opt encrypted
# Deploy with specific network
docker run -d --network bots-vip ...
```
### Load Balancing
Use multiple bot instances with same token:
```bash
# Note: Requires webhook mode and load balancer
docker service create \
--name bot-pool \
--replicas 5 \
--publish 8443:8443 \
-e Telegram__UseWebhook=true \
littleshop/telebot:latest
```
### Monitoring with Prometheus
```yaml
# Add to docker-compose
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'
```
## Support
For issues or questions:
1. Check container logs: `docker logs <container-name>`
2. Verify environment variables
3. Test API connectivity
4. Review admin panel bot status
5. Check the [main deployment guide](DEPLOYMENT.md)
## Next Steps
1. **Set up monitoring** - Add Prometheus/Grafana
2. **Implement CI/CD** - Automate deployments
3. **Add backup strategy** - Regular data backups
4. **Scale horizontally** - Add more hosts as needed
5. **Implement geo-routing** - Route users to nearest bot

View File

@ -0,0 +1,7 @@
namespace TeleBot
{
public static class BotConfig
{
public static string BrandName { get; set; } = "Little Shop";
}
}

View File

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using TeleBot.Services;
namespace TeleBot.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class WebhookController : ControllerBase
{
private readonly ILogger<WebhookController> _logger;
private readonly TelegramBotService _telegramBotService;
private readonly BotManagerService _botManagerService;
private readonly IConfiguration _configuration;
public WebhookController(
ILogger<WebhookController> logger,
TelegramBotService telegramBotService,
BotManagerService botManagerService,
IConfiguration configuration)
{
_logger = logger;
_telegramBotService = telegramBotService;
_botManagerService = botManagerService;
_configuration = configuration;
}
/// <summary>
/// Webhook endpoint for configuration updates from admin panel
/// </summary>
[HttpPost("config-update")]
public async Task<IActionResult> ConfigurationUpdate([FromBody] ConfigUpdateDto dto)
{
try
{
// Verify the request is authorized (you might want to add API key validation)
var apiKey = Request.Headers["X-Webhook-Key"].ToString();
var expectedKey = _configuration["Webhook:Secret"];
if (!string.IsNullOrEmpty(expectedKey) && apiKey != expectedKey)
{
_logger.LogWarning("Unauthorized webhook request");
return Unauthorized();
}
_logger.LogInformation("Received configuration update via webhook");
// Handle different types of updates
if (dto.UpdateType == "bot_token" && !string.IsNullOrEmpty(dto.BotToken))
{
_logger.LogInformation("Updating bot token via webhook");
await _telegramBotService.UpdateBotTokenAsync(dto.BotToken);
return Ok(new { success = true, message = "Bot token updated successfully" });
}
else if (dto.UpdateType == "settings")
{
_logger.LogInformation("Triggering settings sync via webhook");
// Force a settings sync
var settings = await _botManagerService.GetSettingsAsync();
if (settings != null)
{
_logger.LogInformation("Settings synced successfully via webhook");
}
return Ok(new { success = true, message = "Settings synced successfully" });
}
return BadRequest(new { success = false, message = "Unknown update type" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing webhook update");
return StatusCode(500, new { success = false, message = "Internal server error" });
}
}
/// <summary>
/// Health check endpoint
/// </summary>
[HttpGet("health")]
public IActionResult Health()
{
return Ok(new
{
status = "healthy",
timestamp = DateTime.UtcNow,
botKey = !string.IsNullOrEmpty(_configuration["BotManager:ApiKey"])
});
}
}
public class ConfigUpdateDto
{
public string UpdateType { get; set; } = string.Empty;
public string? BotToken { get; set; }
public Dictionary<string, object>? Settings { get; set; }
}
}

View File

@ -11,6 +11,7 @@ using Telegram.Bot.Types.ReplyMarkups;
using TeleBot.Models; using TeleBot.Models;
using TeleBot.Services; using TeleBot.Services;
using TeleBot.UI; using TeleBot.UI;
using LittleShop.Client.Models;
namespace TeleBot.Handlers namespace TeleBot.Handlers
{ {
@ -98,6 +99,14 @@ namespace TeleBot.Handlers
await HandleAddToCart(bot, callbackQuery, session, data); await HandleAddToCart(bot, callbackQuery, session, data);
break; break;
case "quickbuy":
await HandleQuickBuy(bot, callbackQuery, session, data);
break;
case "quickbuyvar":
await HandleQuickBuyWithVariation(bot, callbackQuery, session, data);
break;
case "cart": case "cart":
await HandleViewCart(bot, callbackQuery.Message, session); await HandleViewCart(bot, callbackQuery.Message, session);
break; break;
@ -281,7 +290,7 @@ namespace TeleBot.Handlers
// Use carousel service to send products with images // Use carousel service to send products with images
await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, categoryName, page); await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, categoryName, page);
session.State = SessionState.BrowsingProducts; session.State = SessionState.ViewingProducts;
} }
private async Task HandleProductsPage(ITelegramBotClient bot, Message message, UserSession session, string[] data) private async Task HandleProductsPage(ITelegramBotClient bot, Message message, UserSession session, string[] data)
@ -294,7 +303,7 @@ namespace TeleBot.Handlers
// Use carousel service to send products with images // Use carousel service to send products with images
await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, "All Categories", page); await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, "All Categories", page);
session.State = SessionState.BrowsingProducts; session.State = SessionState.ViewingProducts;
} }
private async Task HandleProductDetail(ITelegramBotClient bot, Message message, UserSession session, Guid productId) private async Task HandleProductDetail(ITelegramBotClient bot, Message message, UserSession session, Guid productId)
@ -336,9 +345,10 @@ namespace TeleBot.Handlers
private async Task HandleAddToCart(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data) private async Task HandleAddToCart(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data)
{ {
// Format: add:productId:quantity // Format: add:productId:quantity or add:productId:quantity:variationId
var productId = Guid.Parse(data[1]); var productId = Guid.Parse(data[1]);
var quantity = int.Parse(data[2]); var quantity = int.Parse(data[2]);
Guid? variationId = data.Length > 3 ? Guid.Parse(data[3]) : null;
var product = await _shopService.GetProductAsync(productId); var product = await _shopService.GetProductAsync(productId);
if (product == null) if (product == null)
@ -347,16 +357,37 @@ namespace TeleBot.Handlers
return; return;
} }
session.Cart.AddItem(productId, product.Name, product.Price, quantity); // If variations exist but none selected, show variation selection
if (variationId == null && product.Variations?.Any() == true)
{
await ShowVariationSelection(bot, callbackQuery.Message!, session, product, quantity);
return;
}
// Get price based on variation or base product
decimal price = product.Price;
string itemName = product.Name;
if (variationId.HasValue && product.Variations != null)
{
var variation = product.Variations.FirstOrDefault(v => v.Id == variationId);
if (variation != null)
{
price = variation.Price;
itemName = $"{product.Name} ({variation.Name})";
quantity = variation.Quantity; // Use variation's quantity
}
}
session.Cart.AddItem(productId, itemName, price, quantity, variationId);
await bot.AnswerCallbackQueryAsync( await bot.AnswerCallbackQueryAsync(
callbackQuery.Id, callbackQuery.Id,
$"✅ Added {quantity}x {product.Name} to cart", $"✅ Added {quantity}x {itemName} to cart",
showAlert: false showAlert: false
); );
// Show cart // Send new cart message instead of editing
await HandleViewCart(bot, callbackQuery.Message!, session); await SendNewCartMessage(bot, callbackQuery.Message!.Chat.Id, session);
} }
private async Task HandleViewCart(ITelegramBotClient bot, Message message, UserSession session) private async Task HandleViewCart(ITelegramBotClient bot, Message message, UserSession session)
@ -371,6 +402,149 @@ namespace TeleBot.Handlers
session.State = SessionState.ViewingCart; session.State = SessionState.ViewingCart;
} }
private async Task SendNewCartMessage(ITelegramBotClient bot, long chatId, UserSession session)
{
await bot.SendTextMessageAsync(
chatId,
MessageFormatter.FormatCart(session.Cart),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.CartMenu(session.Cart)
);
session.State = SessionState.ViewingCart;
}
private async Task ShowVariationSelection(ITelegramBotClient bot, Message message, UserSession session, Product product, int defaultQuantity)
{
var text = MessageFormatter.FormatProductWithVariations(product);
await bot.SendTextMessageAsync(
message.Chat.Id,
text,
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.ProductVariationsMenu(product, defaultQuantity)
);
}
private async Task HandleQuickBuy(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data)
{
// Format: quickbuy:productId:quantity
var productId = Guid.Parse(data[1]);
var quantity = int.Parse(data[2]);
var product = await _shopService.GetProductAsync(productId);
if (product == null)
{
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
return;
}
// If variations exist, show variation selection with quickbuy flow
if (product.Variations?.Any() == true)
{
await ShowVariationSelectionForQuickBuy(bot, callbackQuery.Message!, session, product);
return;
}
// Add to cart with base product
session.Cart.AddItem(productId, product.Name, product.Price, quantity, null);
await bot.AnswerCallbackQueryAsync(
callbackQuery.Id,
$"✅ Added {quantity}x {product.Name} to cart",
showAlert: false
);
// Send cart summary in new message
await bot.SendTextMessageAsync(
callbackQuery.Message!.Chat.Id,
MessageFormatter.FormatCart(session.Cart),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.CartMenu(session.Cart)
);
// Immediately proceed to checkout
await Task.Delay(500); // Small delay for better UX
await HandleCheckout(bot, callbackQuery.Message, session);
}
private async Task ShowVariationSelectionForQuickBuy(ITelegramBotClient bot, Message message, UserSession session, Product product)
{
var text = MessageFormatter.FormatProductWithVariations(product);
var buttons = new List<InlineKeyboardButton[]>();
if (product.Variations?.Any() == true)
{
// Add buttons for each variation with quickbuy flow
foreach (var variation in product.Variations.OrderBy(v => v.Quantity))
{
var label = variation.Quantity > 1
? $"{variation.Name} - ${variation.Price:F2} (${variation.PricePerUnit:F2}/ea)"
: $"{variation.Name} - ${variation.Price:F2}";
buttons.Add(new[]
{
InlineKeyboardButton.WithCallbackData(label, $"quickbuyvar:{product.Id}:{variation.Quantity}:{variation.Id}")
});
}
}
// Add back button
buttons.Add(new[]
{
InlineKeyboardButton.WithCallbackData("⬅️ Back", "menu")
});
await bot.SendTextMessageAsync(
message.Chat.Id,
text + "\n\n*Select an option for quick checkout:*",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: new InlineKeyboardMarkup(buttons)
);
}
private async Task HandleQuickBuyWithVariation(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data)
{
// Format: quickbuyvar:productId:quantity:variationId
var productId = Guid.Parse(data[1]);
var quantity = int.Parse(data[2]);
var variationId = Guid.Parse(data[3]);
var product = await _shopService.GetProductAsync(productId);
if (product == null)
{
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
return;
}
var variation = product.Variations?.FirstOrDefault(v => v.Id == variationId);
if (variation == null)
{
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Variation not found", showAlert: true);
return;
}
// Add to cart with variation
var itemName = $"{product.Name} ({variation.Name})";
session.Cart.AddItem(productId, itemName, variation.Price, variation.Quantity, variationId);
await bot.AnswerCallbackQueryAsync(
callbackQuery.Id,
$"✅ Added {variation.Quantity}x {itemName} to cart",
showAlert: false
);
// Send cart summary in new message
await bot.SendTextMessageAsync(
callbackQuery.Message!.Chat.Id,
MessageFormatter.FormatCart(session.Cart),
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.CartMenu(session.Cart)
);
// Immediately proceed to checkout
await Task.Delay(500); // Small delay for better UX
await HandleCheckout(bot, callbackQuery.Message, session);
}
private async Task HandleRemoveFromCart(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, Guid productId) private async Task HandleRemoveFromCart(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, Guid productId)
{ {
var item = session.Cart.Items.FirstOrDefault(i => i.ProductId == productId); var item = session.Cart.Items.FirstOrDefault(i => i.ProductId == productId);
@ -415,9 +589,9 @@ namespace TeleBot.Handlers
session.State = SessionState.CheckoutFlow; session.State = SessionState.CheckoutFlow;
await bot.EditMessageTextAsync( // Send new message for checkout instead of editing
await bot.SendTextMessageAsync(
message.Chat.Id, message.Chat.Id,
message.MessageId,
"📦 *Checkout - Step 1/5*\n\n" + "📦 *Checkout - Step 1/5*\n\n" +
"Please enter your shipping name:\n\n" + "Please enter your shipping name:\n\n" +
"_Reply to this message with your name_", "_Reply to this message with your name_",
@ -459,9 +633,9 @@ namespace TeleBot.Handlers
// Store order ID for payment // Store order ID for payment
session.TempData["current_order_id"] = order.Id; session.TempData["current_order_id"] = order.Id;
// Show payment options // Show payment options - only safe currencies with BTCPay Server support
var currencies = _configuration.GetSection("Cryptocurrencies").Get<List<string>>() var currencies = _configuration.GetSection("Cryptocurrencies").Get<List<string>>()
?? new List<string> { "BTC", "XMR", "USDT", "LTC" }; ?? new List<string> { "BTC", "XMR", "LTC", "DASH" };
await bot.EditMessageTextAsync( await bot.EditMessageTextAsync(
message.Chat.Id, message.Chat.Id,

View File

@ -189,7 +189,7 @@ namespace TeleBot.Handlers
// Send products as carousel with images // Send products as carousel with images
await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, categoryName, 1); await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, categoryName, 1);
session.State = Models.SessionState.BrowsingProducts; session.State = Models.SessionState.ViewingProducts;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -11,9 +11,10 @@ namespace TeleBot.Models
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public void AddItem(Guid productId, string productName, decimal price, int quantity = 1) public void AddItem(Guid productId, string productName, decimal price, int quantity = 1, Guid? variationId = null)
{ {
var existingItem = Items.FirstOrDefault(i => i.ProductId == productId); var existingItem = Items.FirstOrDefault(i =>
i.ProductId == productId && i.VariationId == variationId);
if (existingItem != null) if (existingItem != null)
{ {
@ -25,6 +26,7 @@ namespace TeleBot.Models
var newItem = new CartItem var newItem = new CartItem
{ {
ProductId = productId, ProductId = productId,
VariationId = variationId,
ProductName = productName, ProductName = productName,
UnitPrice = price, UnitPrice = price,
Quantity = quantity Quantity = quantity
@ -85,6 +87,7 @@ namespace TeleBot.Models
public class CartItem public class CartItem
{ {
public Guid ProductId { get; set; } public Guid ProductId { get; set; }
public Guid? VariationId { get; set; }
public string ProductName { get; set; } = string.Empty; public string ProductName { get; set; } = string.Empty;
public int Quantity { get; set; } public int Quantity { get; set; }
public decimal UnitPrice { get; set; } public decimal UnitPrice { get; set; }

View File

@ -4,6 +4,8 @@ using System.Threading.Tasks;
using Hangfire; using Hangfire;
using Hangfire.LiteDB; using Hangfire.LiteDB;
using LittleShop.Client.Extensions; using LittleShop.Client.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
@ -14,8 +16,7 @@ using TeleBot;
using TeleBot.Handlers; using TeleBot.Handlers;
using TeleBot.Services; using TeleBot.Services;
var builder = Host.CreateApplicationBuilder(args); var builder = WebApplication.CreateBuilder(args);
public static string BrandName ?? "Little Shop";
// Configuration // Configuration
builder.Configuration builder.Configuration
.SetBasePath(Directory.GetCurrentDirectory()) .SetBasePath(Directory.GetCurrentDirectory())
@ -23,6 +24,9 @@ builder.Configuration
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true) .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables(); .AddEnvironmentVariables();
// Add MVC Controllers for webhook endpoints
builder.Services.AddControllers();
// Serilog // Serilog
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information() .MinimumLevel.Information()
@ -49,7 +53,8 @@ builder.Services.AddLittleShopClient(options =>
options.TimeoutSeconds = 30; options.TimeoutSeconds = 30;
options.MaxRetryAttempts = 3; options.MaxRetryAttempts = 3;
BrandName = config["LittleShop.BrandName"] ?? "Little Shop"; // Set the brand name globally
BotConfig.BrandName = config["LittleShop:BrandName"] ?? "Little Shop";
}); });
builder.Services.AddSingleton<ILittleShopService, LittleShopService>(); builder.Services.AddSingleton<ILittleShopService, LittleShopService>();
@ -80,10 +85,10 @@ builder.Services.AddSingleton<ICommandHandler, CommandHandler>();
builder.Services.AddSingleton<ICallbackHandler, CallbackHandler>(); builder.Services.AddSingleton<ICallbackHandler, CallbackHandler>();
builder.Services.AddSingleton<IMessageHandler, MessageHandler>(); builder.Services.AddSingleton<IMessageHandler, MessageHandler>();
// Bot Manager Service (for registration and metrics) // Bot Manager Service (for registration and metrics) - Single instance
builder.Services.AddHttpClient<BotManagerService>(); builder.Services.AddHttpClient<BotManagerService>();
builder.Services.AddSingleton<BotManagerService>(); builder.Services.AddSingleton<BotManagerService>();
builder.Services.AddHostedService<BotManagerService>(); builder.Services.AddHostedService(provider => provider.GetRequiredService<BotManagerService>());
// Message Delivery Service - Single instance // Message Delivery Service - Single instance
builder.Services.AddSingleton<MessageDeliveryService>(); builder.Services.AddSingleton<MessageDeliveryService>();
@ -94,11 +99,26 @@ builder.Services.AddHostedService<MessageDeliveryService>(sp => sp.GetRequiredSe
builder.Services.AddHttpClient<ProductCarouselService>(); builder.Services.AddHttpClient<ProductCarouselService>();
builder.Services.AddSingleton<IProductCarouselService, ProductCarouselService>(); builder.Services.AddSingleton<IProductCarouselService, ProductCarouselService>();
// Bot Service // Bot Service - Single instance
builder.Services.AddHostedService<TelegramBotService>(); builder.Services.AddSingleton<TelegramBotService>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<TelegramBotService>());
// Build and run // Build the application
var host = builder.Build(); var app = builder.Build();
// Connect the services
var botManagerService = app.Services.GetRequiredService<BotManagerService>();
var telegramBotService = app.Services.GetRequiredService<TelegramBotService>();
botManagerService.SetTelegramBotService(telegramBotService);
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.MapControllers();
try try
{ {
@ -106,8 +126,9 @@ try
Log.Information("Privacy Mode: {PrivacyMode}", builder.Configuration["Privacy:Mode"]); Log.Information("Privacy Mode: {PrivacyMode}", builder.Configuration["Privacy:Mode"]);
Log.Information("Ephemeral by Default: {Ephemeral}", builder.Configuration["Privacy:EphemeralByDefault"]); Log.Information("Ephemeral by Default: {Ephemeral}", builder.Configuration["Privacy:EphemeralByDefault"]);
Log.Information("Tor Enabled: {Tor}", builder.Configuration["Privacy:EnableTor"]); Log.Information("Tor Enabled: {Tor}", builder.Configuration["Privacy:EnableTor"]);
Log.Information("Webhook endpoints available at /api/webhook");
await host.RunAsync(); await app.RunAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
@ -20,9 +21,12 @@ namespace TeleBot.Services
private readonly SessionManager _sessionManager; private readonly SessionManager _sessionManager;
private Timer? _heartbeatTimer; private Timer? _heartbeatTimer;
private Timer? _metricsTimer; private Timer? _metricsTimer;
private Timer? _settingsSyncTimer;
private string? _botKey; private string? _botKey;
private Guid? _botId; private Guid? _botId;
private readonly Dictionary<string, decimal> _metricsBuffer; private readonly Dictionary<string, decimal> _metricsBuffer;
private TelegramBotService? _telegramBotService;
private string? _lastKnownBotToken;
public BotManagerService( public BotManagerService(
IConfiguration configuration, IConfiguration configuration,
@ -37,6 +41,11 @@ namespace TeleBot.Services
_metricsBuffer = new Dictionary<string, decimal>(); _metricsBuffer = new Dictionary<string, decimal>();
} }
public void SetTelegramBotService(TelegramBotService telegramBotService)
{
_telegramBotService = telegramBotService;
}
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
try try
@ -64,6 +73,9 @@ namespace TeleBot.Services
// Start metrics timer (every 60 seconds) // Start metrics timer (every 60 seconds)
_metricsTimer = new Timer(SendMetrics, null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60)); _metricsTimer = new Timer(SendMetrics, null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
// Start settings sync timer (every 5 minutes)
_settingsSyncTimer = new Timer(SyncSettingsWithBotUpdate, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(5));
_logger.LogInformation("Bot manager service started successfully"); _logger.LogInformation("Bot manager service started successfully");
} }
catch (Exception ex) catch (Exception ex)
@ -76,6 +88,7 @@ namespace TeleBot.Services
{ {
_heartbeatTimer?.Change(Timeout.Infinite, 0); _heartbeatTimer?.Change(Timeout.Infinite, 0);
_metricsTimer?.Change(Timeout.Infinite, 0); _metricsTimer?.Change(Timeout.Infinite, 0);
_settingsSyncTimer?.Change(Timeout.Infinite, 0);
// Send final metrics before stopping // Send final metrics before stopping
SendMetrics(null); SendMetrics(null);
@ -161,23 +174,42 @@ namespace TeleBot.Services
{ {
if (string.IsNullOrEmpty(_botKey)) return; if (string.IsNullOrEmpty(_botKey)) return;
var apiUrl = _configuration["LittleShop:ApiUrl"]; var settings = await GetSettingsAsync();
_httpClient.DefaultRequestHeaders.Clear(); if (settings != null)
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
var response = await _httpClient.GetAsync($"{apiUrl}/api/bots/settings");
if (response.IsSuccessStatusCode)
{ {
var settingsJson = await response.Content.ReadAsStringAsync();
var settings = JsonSerializer.Deserialize<Dictionary<string, object>>(settingsJson);
// Apply settings to configuration // Apply settings to configuration
// This would update the running configuration with server settings // This would update the running configuration with server settings
_logger.LogInformation("Settings synced from server"); _logger.LogInformation("Settings synced from server");
} }
} }
public async Task<Dictionary<string, object>?> GetSettingsAsync()
{
if (string.IsNullOrEmpty(_botKey)) return null;
try
{
var apiUrl = _configuration["LittleShop:ApiUrl"];
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
var response = await _httpClient.GetAsync($"{apiUrl}/api/bots/settings");
if (response.IsSuccessStatusCode)
{
var settingsJson = await response.Content.ReadAsStringAsync();
var settings = JsonSerializer.Deserialize<Dictionary<string, object>>(settingsJson);
return settings;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch settings from API");
}
return null;
}
private async void SendHeartbeat(object? state) private async void SendHeartbeat(object? state)
{ {
if (string.IsNullOrEmpty(_botKey)) return; if (string.IsNullOrEmpty(_botKey)) return;
@ -350,10 +382,45 @@ namespace TeleBot.Services
}; };
} }
private async void SyncSettingsWithBotUpdate(object? state)
{
try
{
var settings = await GetSettingsAsync();
if (settings != null && settings.ContainsKey("telegram"))
{
if (settings["telegram"] is JsonElement telegramElement)
{
var telegramSettings = telegramElement.EnumerateObject().ToDictionary(p => p.Name, p => p.Value.ToString());
if (telegramSettings.TryGetValue("botToken", out var token))
{
// Check if token has changed
if (!string.IsNullOrEmpty(token) && token != _lastKnownBotToken)
{
_logger.LogInformation("Bot token has changed. Updating bot...");
_lastKnownBotToken = token;
// Update the TelegramBotService if available
if (_telegramBotService != null)
{
await _telegramBotService.UpdateBotTokenAsync(token);
}
}
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to sync settings with bot update");
}
}
public void Dispose() public void Dispose()
{ {
_heartbeatTimer?.Dispose(); _heartbeatTimer?.Dispose();
_metricsTimer?.Dispose(); _metricsTimer?.Dispose();
_settingsSyncTimer?.Dispose();
} }
// DTOs for API responses // DTOs for API responses

View File

@ -9,8 +9,10 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Telegram.Bot; using Telegram.Bot;
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegram.Bot.Types.InputFiles; // InputFiles namespace no longer exists in newer Telegram.Bot versions
// using Telegram.Bot.Types.InputFiles;
using Telegram.Bot.Types.ReplyMarkups; using Telegram.Bot.Types.ReplyMarkups;
using TeleBot.UI;
namespace TeleBot.Services namespace TeleBot.Services
{ {
@ -18,7 +20,7 @@ namespace TeleBot.Services
{ {
Task SendProductCarouselAsync(ITelegramBotClient botClient, long chatId, PagedResult<Product> products, string? categoryName = null, int currentPage = 1); Task SendProductCarouselAsync(ITelegramBotClient botClient, long chatId, PagedResult<Product> products, string? categoryName = null, int currentPage = 1);
Task SendSingleProductWithImageAsync(ITelegramBotClient botClient, long chatId, Product product); Task SendSingleProductWithImageAsync(ITelegramBotClient botClient, long chatId, Product product);
Task<InputOnlineFile?> GetProductImageAsync(Product product); Task<InputFile?> GetProductImageAsync(Product product);
Task<bool> IsImageUrlValidAsync(string imageUrl); Task<bool> IsImageUrlValidAsync(string imageUrl);
} }
@ -234,7 +236,7 @@ namespace TeleBot.Services
} }
} }
public async Task<InputOnlineFile?> GetProductImageAsync(Product product) public async Task<InputFile?> GetProductImageAsync(Product product)
{ {
try try
{ {
@ -256,16 +258,16 @@ namespace TeleBot.Services
var cacheKey = $"{product.Id}_{photo.Id}"; var cacheKey = $"{product.Id}_{photo.Id}";
var cachedPath = Path.Combine(_imageCachePath, $"{cacheKey}.jpg"); var cachedPath = Path.Combine(_imageCachePath, $"{cacheKey}.jpg");
if (File.Exists(cachedPath)) if (System.IO.File.Exists(cachedPath))
{ {
return new InputOnlineFile(File.OpenRead(cachedPath), $"{product.Name}.jpg"); return InputFile.FromStream(System.IO.File.OpenRead(cachedPath), $"{product.Name}.jpg");
} }
// Download and cache the image // Download and cache the image
var imageBytes = await _httpClient.GetByteArrayAsync(imageUrl); var imageBytes = await _httpClient.GetByteArrayAsync(imageUrl);
await File.WriteAllBytesAsync(cachedPath, imageBytes); await System.IO.File.WriteAllBytesAsync(cachedPath, imageBytes);
return new InputOnlineFile(File.OpenRead(cachedPath), $"{product.Name}.jpg"); return InputFile.FromStream(System.IO.File.OpenRead(cachedPath), $"{product.Name}.jpg");
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -281,7 +283,8 @@ namespace TeleBot.Services
if (string.IsNullOrEmpty(imageUrl)) if (string.IsNullOrEmpty(imageUrl))
return false; return false;
var response = await _httpClient.HeadAsync(imageUrl); using var request = new HttpRequestMessage(HttpMethod.Head, imageUrl);
var response = await _httpClient.SendAsync(request);
return response.IsSuccessStatusCode && return response.IsSuccessStatusCode &&
response.Content.Headers.ContentType?.MediaType?.StartsWith("image/") == true; response.Content.Headers.ContentType?.MediaType?.StartsWith("image/") == true;
} }

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -24,8 +26,10 @@ namespace TeleBot
private readonly ICallbackHandler _callbackHandler; private readonly ICallbackHandler _callbackHandler;
private readonly IMessageHandler _messageHandler; private readonly IMessageHandler _messageHandler;
private readonly IMessageDeliveryService _messageDeliveryService; private readonly IMessageDeliveryService _messageDeliveryService;
private readonly BotManagerService _botManagerService;
private ITelegramBotClient? _botClient; private ITelegramBotClient? _botClient;
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
private string? _currentBotToken;
public TelegramBotService( public TelegramBotService(
IConfiguration configuration, IConfiguration configuration,
@ -34,7 +38,8 @@ namespace TeleBot
ICommandHandler commandHandler, ICommandHandler commandHandler,
ICallbackHandler callbackHandler, ICallbackHandler callbackHandler,
IMessageHandler messageHandler, IMessageHandler messageHandler,
IMessageDeliveryService messageDeliveryService) IMessageDeliveryService messageDeliveryService,
BotManagerService botManagerService)
{ {
_configuration = configuration; _configuration = configuration;
_logger = logger; _logger = logger;
@ -43,17 +48,28 @@ namespace TeleBot
_callbackHandler = callbackHandler; _callbackHandler = callbackHandler;
_messageHandler = messageHandler; _messageHandler = messageHandler;
_messageDeliveryService = messageDeliveryService; _messageDeliveryService = messageDeliveryService;
_botManagerService = botManagerService;
} }
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
var botToken = _configuration["Telegram:BotToken"]; // Try to get bot token from API first via BotManagerService
var botToken = await GetBotTokenAsync();
// Fallback to configuration if API doesn't provide token
if (string.IsNullOrEmpty(botToken))
{
botToken = _configuration["Telegram:BotToken"];
}
if (string.IsNullOrEmpty(botToken) || botToken == "YOUR_BOT_TOKEN_HERE") if (string.IsNullOrEmpty(botToken) || botToken == "YOUR_BOT_TOKEN_HERE")
{ {
_logger.LogError("Bot token not configured. Please set Telegram:BotToken in appsettings.json"); _logger.LogError("Bot token not configured. Please either register via admin panel or set Telegram:BotToken in appsettings.json");
return; return;
} }
_currentBotToken = botToken;
_botClient = new TelegramBotClient(botToken); _botClient = new TelegramBotClient(botToken);
_cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource = new CancellationTokenSource();
@ -132,5 +148,78 @@ namespace TeleBot
_logger.LogError(exception, "Bot error: {ErrorMessage}", errorMessage); _logger.LogError(exception, "Bot error: {ErrorMessage}", errorMessage);
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task<string?> GetBotTokenAsync()
{
try
{
// Check if we have a bot key stored
var botKey = _configuration["BotManager:ApiKey"];
if (string.IsNullOrEmpty(botKey))
{
_logger.LogInformation("No bot key configured. Bot will need to register first or use local token.");
return null;
}
// Fetch settings from API
var settings = await _botManagerService.GetSettingsAsync();
if (settings != null && settings.ContainsKey("telegram"))
{
if (settings["telegram"] is JsonElement telegramElement)
{
var telegramSettings = telegramElement.EnumerateObject().ToDictionary(p => p.Name, p => p.Value.ToString());
if (telegramSettings.TryGetValue("botToken", out var token))
{
_logger.LogInformation("Bot token fetched from admin panel successfully");
return token;
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch bot token from API. Will use local configuration.");
}
return null;
}
public async Task UpdateBotTokenAsync(string newToken)
{
if (_botClient != null && _currentBotToken != newToken)
{
_logger.LogInformation("Updating bot token and restarting bot...");
// Stop current bot
_cancellationTokenSource?.Cancel();
// Create new bot client with new token
_currentBotToken = newToken;
_botClient = new TelegramBotClient(newToken);
_cancellationTokenSource = new CancellationTokenSource();
var receiverOptions = new ReceiverOptions
{
AllowedUpdates = Array.Empty<UpdateType>(),
ThrowPendingUpdates = true
};
_botClient.StartReceiving(
HandleUpdateAsync,
HandleErrorAsync,
receiverOptions,
cancellationToken: _cancellationTokenSource.Token
);
var me = await _botClient.GetMeAsync();
_logger.LogInformation("Bot restarted with new token: @{Username} ({Id})", me.Username, me.Id);
// Update message delivery service
if (_messageDeliveryService is MessageDeliveryService deliveryService)
{
deliveryService.SetBotClient(_botClient);
}
}
}
} }
} }

View File

@ -59,7 +59,7 @@ namespace TeleBot
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Url = "https://via.placeholder.com/300x200.jpg", Url = "https://via.placeholder.com/300x200.jpg",
IsMain = true SortOrder = 0 // Use SortOrder = 0 to indicate main photo
} }
} }
}; };

View File

@ -329,12 +329,49 @@ namespace TeleBot.UI
return new InlineKeyboardMarkup(new[] return new InlineKeyboardMarkup(new[]
{ {
new[] { new[] {
InlineKeyboardButton.WithCallbackData("🛒 Buy Now", $"add:{productId}:1"), InlineKeyboardButton.WithCallbackData("🛒 Quick Buy", $"quickbuy:{productId}:1"),
InlineKeyboardButton.WithCallbackData("📄 Details", $"product:{productId}") InlineKeyboardButton.WithCallbackData("📄 Details", $"product:{productId}")
} }
}); });
} }
public static InlineKeyboardMarkup ProductVariationsMenu(Product product, int defaultQuantity = 1)
{
var buttons = new List<InlineKeyboardButton[]>();
if (product.Variations?.Any() == true)
{
// Add a button for each variation
foreach (var variation in product.Variations.OrderBy(v => v.Quantity))
{
var label = variation.Quantity > 1
? $"{variation.Name} - ${variation.Price:F2} (${variation.PricePerUnit:F2}/ea)"
: $"{variation.Name} - ${variation.Price:F2}";
buttons.Add(new[]
{
InlineKeyboardButton.WithCallbackData(label, $"add:{product.Id}:{variation.Quantity}:{variation.Id}")
});
}
}
else
{
// No variations, just show regular add to cart
buttons.Add(new[]
{
InlineKeyboardButton.WithCallbackData($"Add to Cart - ${product.Price:F2}", $"add:{product.Id}:{defaultQuantity}")
});
}
// Add back button
buttons.Add(new[]
{
InlineKeyboardButton.WithCallbackData("⬅️ Back", "menu")
});
return new InlineKeyboardMarkup(buttons);
}
public static InlineKeyboardMarkup CategoryNavigationMenu(Guid? categoryId) public static InlineKeyboardMarkup CategoryNavigationMenu(Guid? categoryId)
{ {
return new InlineKeyboardMarkup(new[] return new InlineKeyboardMarkup(new[]

View File

@ -12,13 +12,13 @@ namespace TeleBot.UI
{ {
if (isReturning) if (isReturning)
{ {
return $"🔒 *Welcome back to {Program.BrandName}*\n\n" + return $"🔒 *Welcome back to {BotConfig.BrandName}*\n\n" +
"Your privacy is our priority. All sessions are ephemeral by default.\n\n" + "Your privacy is our priority. All sessions are ephemeral by default.\n\n" +
"🖼️ *New Feature:* Browse products with beautiful image carousels!\n\n" + "🖼️ *New Feature:* Browse products with beautiful image carousels!\n\n" +
"How can I help you today?"; "How can I help you today?";
} }
return $"🔒 *Welcome to {Program.BrandName}*\n\n" + return $"🔒 *Welcome to {BotConfig.BrandName}*\n\n" +
"🛡️ *Your Privacy Matters:*\n" + "🛡️ *Your Privacy Matters:*\n" +
"• No account required\n" + "• No account required\n" +
"• Ephemeral sessions by default\n" + "• Ephemeral sessions by default\n" +
@ -85,7 +85,18 @@ namespace TeleBot.UI
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine($"🛍️ *{product.Name}*"); sb.AppendLine($"🛍️ *{product.Name}*");
sb.AppendLine($"💰 £{product.Price:F2}");
// Show variations if available
if (product.Variations?.Any() == true)
{
var lowestPrice = product.Variations.Min(v => v.PricePerUnit);
sb.AppendLine($"💰 From £{lowestPrice:F2}");
sb.AppendLine($"📦 _{product.Variations.Count} options available_");
}
else
{
sb.AppendLine($"💰 £{product.Price:F2}");
}
if (!string.IsNullOrEmpty(product.Description)) if (!string.IsNullOrEmpty(product.Description))
{ {
@ -123,6 +134,45 @@ namespace TeleBot.UI
return sb.ToString(); return sb.ToString();
} }
public static string FormatProductWithVariations(Product product)
{
var sb = new StringBuilder();
sb.AppendLine($"🛍️ *{product.Name}*\n");
if (product.Variations?.Any() == true)
{
sb.AppendLine("📦 *Available Options:*\n");
foreach (var variation in product.Variations.OrderBy(v => v.Quantity))
{
var savings = variation.Quantity > 1
? $" (${variation.PricePerUnit:F2} each)"
: "";
sb.AppendLine($"• *{variation.Name}*: ${variation.Price:F2}{savings}");
if (!string.IsNullOrEmpty(variation.Description))
{
sb.AppendLine($" _{variation.Description}_");
}
}
}
else
{
sb.AppendLine($"💰 *Price:* ${product.Price:F2}");
}
sb.AppendLine($"\n⚖ *Weight:* {product.Weight} {product.WeightUnit}");
sb.AppendLine($"📁 *Category:* {product.CategoryName ?? "Uncategorized"}");
if (!string.IsNullOrEmpty(product.Description))
{
sb.AppendLine($"\n📝 *Description:*\n{product.Description}");
}
sb.AppendLine("\n*Select an option to add to cart:*");
return sb.ToString();
}
public static string FormatCart(ShoppingCart cart) public static string FormatCart(ShoppingCart cart)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
@ -275,7 +325,7 @@ namespace TeleBot.UI
"/cancel - Cancel current operation\n" + "/cancel - Cancel current operation\n" +
"/delete - Delete all your data\n" + "/delete - Delete all your data\n" +
"/tor - Get Tor onion address\n" + "/tor - Get Tor onion address\n" +
"/help - Show this help message\n\n" "/help - Show this help message\n\n";
} }
public static string FormatPrivacyPolicy() public static string FormatPrivacyPolicy()

View File

@ -6,16 +6,21 @@
}, },
"BotManager": { "BotManager": {
"ApiKey": "", "ApiKey": "",
"Comment": "This will be populated after first registration" "Comment": "This will be populated after first registration with admin panel"
}, },
"Telegram": { "Telegram": {
"BotToken": "7880403661:AAGma1wAyoHsmG45iO6VvHCqzimhJX1pp14", "BotToken": "",
"AdminChatId": "", "AdminChatId": "",
"WebhookUrl": "", "WebhookUrl": "",
"UseWebhook": false "UseWebhook": false,
"Comment": "Bot token will be fetched from admin panel API if BotManager:ApiKey is set"
},
"Webhook": {
"Secret": "",
"Comment": "Optional secret key for webhook authentication"
}, },
"LittleShop": { "LittleShop": {
"ApiUrl": "https://localhost:5001", "ApiUrl": "http://localhost:8080",
"OnionUrl": "", "OnionUrl": "",
"Username": "admin", "Username": "admin",
"Password": "admin", "Password": "admin",
@ -66,11 +71,14 @@
"Cryptocurrencies": [ "Cryptocurrencies": [
"BTC", "BTC",
"XMR", "XMR",
"USDT",
"LTC", "LTC",
"ETH", "DASH"
"ZEC", ],
"DASH", "Kestrel": {
"DOGE" "Endpoints": {
] "Http": {
"Url": "http://localhost:5010"
}
}
}
} }

253
TeleBot/deploy-bot.sh Executable file
View File

@ -0,0 +1,253 @@
#!/bin/bash
# LittleShop TeleBot Docker Deployment Script
# Usage: ./deploy-bot.sh [OPTIONS]
#
# This script helps deploy TeleBot instances to local or remote Docker hosts
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Default values
DOCKER_IMAGE="littleshop/telebot:latest"
CONTAINER_PREFIX="littleshop-bot"
API_URL="${LITTLESHOP_API_URL:-http://localhost:8080}"
RESTART_POLICY="unless-stopped"
DOCKER_HOST=""
# Function to print colored output
print_message() {
local color=$1
local message=$2
echo -e "${color}${message}${NC}"
}
# Function to show usage
show_usage() {
cat << EOF
Usage: $0 [OPTIONS]
Deploy LittleShop TeleBot to Docker hosts
OPTIONS:
-n, --name NAME Bot container name (required)
-t, --token TOKEN Telegram bot token (required)
-k, --api-key KEY Bot API key from admin panel
-a, --api-url URL LittleShop API URL (default: $API_URL)
-h, --host HOST Docker host (e.g., ssh://user@host or tcp://host:2376)
-c, --chat-id ID Admin chat ID for notifications
-e, --encryption-key KEY Database encryption key
-p, --personality NAME Bot personality name
-m, --mode MODE Privacy mode (strict|moderate|relaxed)
-i, --image IMAGE Docker image (default: $DOCKER_IMAGE)
-r, --restart POLICY Restart policy (default: $RESTART_POLICY)
-d, --detach Run in detached mode (default)
-l, --logs Follow logs after deployment
--pull Pull latest image before deployment
--rm Remove existing container before deployment
--help Show this help message
EXAMPLES:
# Deploy to local Docker
$0 -n support-bot -t "TOKEN" -k "API_KEY"
# Deploy to remote host via SSH
$0 -n sales-bot -t "TOKEN" -k "API_KEY" -h ssh://user@server.com
# Deploy with all options
$0 -n vip-bot -t "TOKEN" -k "API_KEY" -a https://api.shop.com \\
-c "123456789" -e "32_char_encryption_key_here" \\
-p "Sarah" -m strict --pull --rm
# Deploy multiple bots using environment file
source .env.bot1 && $0 -n bot1 -t "\$BOT_TOKEN" -k "\$API_KEY"
source .env.bot2 && $0 -n bot2 -t "\$BOT_TOKEN" -k "\$API_KEY"
EOF
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-n|--name)
BOT_NAME="$2"
shift 2
;;
-t|--token)
BOT_TOKEN="$2"
shift 2
;;
-k|--api-key)
API_KEY="$2"
shift 2
;;
-a|--api-url)
API_URL="$2"
shift 2
;;
-h|--host)
DOCKER_HOST="$2"
shift 2
;;
-c|--chat-id)
ADMIN_CHAT_ID="$2"
shift 2
;;
-e|--encryption-key)
ENCRYPTION_KEY="$2"
shift 2
;;
-p|--personality)
PERSONALITY="$2"
shift 2
;;
-m|--mode)
PRIVACY_MODE="$2"
shift 2
;;
-i|--image)
DOCKER_IMAGE="$2"
shift 2
;;
-r|--restart)
RESTART_POLICY="$2"
shift 2
;;
-l|--logs)
FOLLOW_LOGS=true
shift
;;
--pull)
PULL_IMAGE=true
shift
;;
--rm)
REMOVE_EXISTING=true
shift
;;
--help)
show_usage
exit 0
;;
*)
print_message "$RED" "Unknown option: $1"
show_usage
exit 1
;;
esac
done
# Validate required parameters
if [ -z "$BOT_NAME" ]; then
print_message "$RED" "Error: Bot name is required (-n/--name)"
exit 1
fi
if [ -z "$BOT_TOKEN" ]; then
print_message "$RED" "Error: Bot token is required (-t/--token)"
exit 1
fi
# Set container name
CONTAINER_NAME="${CONTAINER_PREFIX}-${BOT_NAME}"
# Build Docker command
DOCKER_CMD="docker"
if [ -n "$DOCKER_HOST" ]; then
export DOCKER_HOST="$DOCKER_HOST"
print_message "$YELLOW" "Deploying to remote host: $DOCKER_HOST"
fi
# Pull latest image if requested
if [ "$PULL_IMAGE" = true ]; then
print_message "$YELLOW" "Pulling latest image: $DOCKER_IMAGE"
$DOCKER_CMD pull "$DOCKER_IMAGE"
fi
# Remove existing container if requested
if [ "$REMOVE_EXISTING" = true ]; then
if $DOCKER_CMD ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
print_message "$YELLOW" "Removing existing container: $CONTAINER_NAME"
$DOCKER_CMD rm -f "$CONTAINER_NAME"
fi
fi
# Build environment variables
ENV_VARS=(
"-e DOTNET_ENVIRONMENT=Production"
"-e TZ=UTC"
"-e Telegram__BotToken=$BOT_TOKEN"
"-e LittleShop__ApiUrl=$API_URL"
"-e Database__ConnectionString=Filename=/app/data/telebot.db;Password=;"
)
# Add optional environment variables
[ -n "$API_KEY" ] && ENV_VARS+=("-e BotManager__ApiKey=$API_KEY")
[ -n "$ADMIN_CHAT_ID" ] && ENV_VARS+=("-e Telegram__AdminChatId=$ADMIN_CHAT_ID")
[ -n "$ENCRYPTION_KEY" ] && ENV_VARS+=("-e Database__EncryptionKey=$ENCRYPTION_KEY")
[ -n "$PERSONALITY" ] && ENV_VARS+=("-e Bot__PersonalityName=$PERSONALITY")
[ -n "$PRIVACY_MODE" ] && ENV_VARS+=("-e Privacy__Mode=$PRIVACY_MODE")
# Default privacy settings
ENV_VARS+=(
"-e Privacy__DataRetentionHours=24"
"-e Privacy__SessionTimeoutMinutes=30"
"-e Privacy__EphemeralByDefault=true"
"-e Features__EnableQRCodes=true"
"-e Features__EnablePGPEncryption=true"
"-e Features__EnableDisappearingMessages=true"
)
# Build volumes
VOLUMES=(
"-v ${CONTAINER_NAME}-data:/app/data"
"-v ${CONTAINER_NAME}-logs:/app/logs"
)
# Deploy the container
print_message "$GREEN" "Deploying bot: $CONTAINER_NAME"
print_message "$YELLOW" "API URL: $API_URL"
$DOCKER_CMD run -d \
--name "$CONTAINER_NAME" \
--restart "$RESTART_POLICY" \
"${ENV_VARS[@]}" \
"${VOLUMES[@]}" \
"$DOCKER_IMAGE"
# Check if deployment was successful
if [ $? -eq 0 ]; then
print_message "$GREEN" "✅ Bot deployed successfully: $CONTAINER_NAME"
# Get container info
CONTAINER_ID=$($DOCKER_CMD ps -q -f name="$CONTAINER_NAME")
print_message "$YELLOW" "Container ID: $CONTAINER_ID"
# Show container status
$DOCKER_CMD ps -f name="$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# Follow logs if requested
if [ "$FOLLOW_LOGS" = true ]; then
print_message "$YELLOW" "Following logs (Ctrl+C to exit)..."
$DOCKER_CMD logs -f "$CONTAINER_NAME"
else
print_message "$YELLOW" "View logs with: docker logs -f $CONTAINER_NAME"
fi
else
print_message "$RED" "❌ Deployment failed"
exit 1
fi
# Print next steps
echo ""
print_message "$GREEN" "Next steps:"
echo "1. Check bot status: docker ps -f name=$CONTAINER_NAME"
echo "2. View logs: docker logs $CONTAINER_NAME"
echo "3. Stop bot: docker stop $CONTAINER_NAME"
echo "4. Remove bot: docker rm -f $CONTAINER_NAME"
echo "5. Access admin panel to manage bot settings"

View File

@ -0,0 +1,284 @@
version: '3.8'
services:
# Customer Support Bot
bot-support:
image: littleshop/telebot:latest
container_name: littleshop-bot-support
restart: unless-stopped
environment:
- DOTNET_ENVIRONMENT=Production
- TZ=UTC
# Telegram Configuration
- Telegram__BotToken=${SUPPORT_BOT_TOKEN}
- Telegram__AdminChatId=${SUPPORT_ADMIN_CHAT_ID}
- Telegram__UseWebhook=false
# LittleShop API Configuration
- LittleShop__ApiUrl=${LITTLESHOP_API_URL:-http://host.docker.internal:8080}
- LittleShop__Username=${LITTLESHOP_USERNAME:-admin}
- LittleShop__Password=${LITTLESHOP_PASSWORD:-admin}
- BotManager__ApiKey=${SUPPORT_BOT_API_KEY}
# Privacy Settings
- Privacy__Mode=strict
- Privacy__DataRetentionHours=24
- Privacy__SessionTimeoutMinutes=30
# Database Configuration
- Database__ConnectionString=Filename=/app/data/telebot.db;Password=;
- Database__EncryptionKey=${SUPPORT_DB_ENCRYPTION_KEY}
# Features
- Features__EnableQRCodes=true
- Features__EnablePGPEncryption=true
- Features__EnableDisappearingMessages=true
volumes:
- support-bot-data:/app/data
- support-bot-logs:/app/logs
networks:
- littleshop-network
healthcheck:
test: ["CMD", "pgrep", "-f", "dotnet.*TeleBot"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Sales & Marketing Bot
bot-sales:
image: littleshop/telebot:latest
container_name: littleshop-bot-sales
restart: unless-stopped
environment:
- DOTNET_ENVIRONMENT=Production
- TZ=UTC
# Telegram Configuration
- Telegram__BotToken=${SALES_BOT_TOKEN}
- Telegram__AdminChatId=${SALES_ADMIN_CHAT_ID}
- Telegram__UseWebhook=false
# LittleShop API Configuration
- LittleShop__ApiUrl=${LITTLESHOP_API_URL:-http://host.docker.internal:8080}
- LittleShop__Username=${LITTLESHOP_USERNAME:-admin}
- LittleShop__Password=${LITTLESHOP_PASSWORD:-admin}
- BotManager__ApiKey=${SALES_BOT_API_KEY}
# Privacy Settings
- Privacy__Mode=moderate
- Privacy__DataRetentionHours=72
- Privacy__SessionTimeoutMinutes=60
- Privacy__EnableAnalytics=true
# Database Configuration
- Database__ConnectionString=Filename=/app/data/telebot.db;Password=;
- Database__EncryptionKey=${SALES_DB_ENCRYPTION_KEY}
# Features
- Features__EnableQRCodes=true
- Features__EnablePGPEncryption=false
- Features__EnableDisappearingMessages=false
volumes:
- sales-bot-data:/app/data
- sales-bot-logs:/app/logs
networks:
- littleshop-network
healthcheck:
test: ["CMD", "pgrep", "-f", "dotnet.*TeleBot"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# VIP/Premium Customer Bot
bot-vip:
image: littleshop/telebot:latest
container_name: littleshop-bot-vip
restart: unless-stopped
environment:
- DOTNET_ENVIRONMENT=Production
- TZ=UTC
# Telegram Configuration
- Telegram__BotToken=${VIP_BOT_TOKEN}
- Telegram__AdminChatId=${VIP_ADMIN_CHAT_ID}
- Telegram__UseWebhook=false
# LittleShop API Configuration
- LittleShop__ApiUrl=${LITTLESHOP_API_URL:-http://host.docker.internal:8080}
- LittleShop__Username=${LITTLESHOP_USERNAME:-admin}
- LittleShop__Password=${LITTLESHOP_PASSWORD:-admin}
- BotManager__ApiKey=${VIP_BOT_API_KEY}
# Privacy Settings (Enhanced for VIP)
- Privacy__Mode=strict
- Privacy__DataRetentionHours=1
- Privacy__SessionTimeoutMinutes=15
- Privacy__EnableAnalytics=false
- Privacy__RequirePGPForShipping=true
- Privacy__EphemeralByDefault=true
- Privacy__EnableTor=${VIP_ENABLE_TOR:-false}
# Database Configuration
- Database__ConnectionString=Filename=/app/data/telebot.db;Password=;
- Database__EncryptionKey=${VIP_DB_ENCRYPTION_KEY}
# Features (All features for VIP)
- Features__EnableVoiceSearch=true
- Features__EnableQRCodes=true
- Features__EnablePGPEncryption=true
- Features__EnableDisappearingMessages=true
- Features__EnableOrderMixing=true
- Features__MixingDelayMinSeconds=60
- Features__MixingDelayMaxSeconds=300
volumes:
- vip-bot-data:/app/data
- vip-bot-logs:/app/logs
networks:
- littleshop-network
healthcheck:
test: ["CMD", "pgrep", "-f", "dotnet.*TeleBot"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Regional Bot Example (EU)
bot-eu:
image: littleshop/telebot:latest
container_name: littleshop-bot-eu
restart: unless-stopped
environment:
- DOTNET_ENVIRONMENT=Production
- TZ=Europe/London
# Telegram Configuration
- Telegram__BotToken=${EU_BOT_TOKEN}
- Telegram__AdminChatId=${EU_ADMIN_CHAT_ID}
- Telegram__UseWebhook=false
# LittleShop API Configuration
- LittleShop__ApiUrl=${EU_API_URL:-http://host.docker.internal:8080}
- LittleShop__Username=${LITTLESHOP_USERNAME:-admin}
- LittleShop__Password=${LITTLESHOP_PASSWORD:-admin}
- BotManager__ApiKey=${EU_BOT_API_KEY}
# Privacy Settings (GDPR Compliant)
- Privacy__Mode=strict
- Privacy__DataRetentionHours=24
- Privacy__SessionTimeoutMinutes=30
- Privacy__EnableAnalytics=false
- Privacy__EphemeralByDefault=true
# Database Configuration
- Database__ConnectionString=Filename=/app/data/telebot.db;Password=;
- Database__EncryptionKey=${EU_DB_ENCRYPTION_KEY}
# Features
- Features__EnableQRCodes=true
- Features__EnablePGPEncryption=true
- Features__EnableDisappearingMessages=true
volumes:
- eu-bot-data:/app/data
- eu-bot-logs:/app/logs
networks:
- littleshop-network
healthcheck:
test: ["CMD", "pgrep", "-f", "dotnet.*TeleBot"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Optional: Shared Redis for all bots
redis:
image: redis:7-alpine
container_name: littleshop-redis-shared
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
networks:
- littleshop-network
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 30s
timeout: 3s
retries: 5
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
support-bot-data:
name: littleshop-bot-support-data
support-bot-logs:
name: littleshop-bot-support-logs
sales-bot-data:
name: littleshop-bot-sales-data
sales-bot-logs:
name: littleshop-bot-sales-logs
vip-bot-data:
name: littleshop-bot-vip-data
vip-bot-logs:
name: littleshop-bot-vip-logs
eu-bot-data:
name: littleshop-bot-eu-data
eu-bot-logs:
name: littleshop-bot-eu-logs
redis-data:
name: littleshop-redis-shared-data
networks:
littleshop-network:
name: littleshop-network
driver: bridge

View File

@ -0,0 +1,295 @@
{
"version": "2",
"templates": [
{
"type": 1,
"title": "LittleShop TeleBot",
"name": "littleshop-telebot",
"description": "Deploy a Telegram bot for LittleShop e-commerce platform",
"note": "Requires a Telegram bot token from @BotFather and API key from LittleShop admin panel",
"categories": ["bots", "ecommerce", "telegram"],
"platform": "linux",
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/telegram.png",
"image": "littleshop/telebot:latest",
"restart_policy": "unless-stopped",
"network_mode": "bridge",
"hostname": "",
"privileged": false,
"interactive": false,
"env": [
{
"name": "DOTNET_ENVIRONMENT",
"label": "Environment",
"default": "Production",
"preset": true
},
{
"name": "TZ",
"label": "Timezone",
"default": "UTC",
"description": "Container timezone (e.g., Europe/London, America/New_York)"
},
{
"name": "Telegram__BotToken",
"label": "Telegram Bot Token",
"description": "Token from @BotFather (required)"
},
{
"name": "Telegram__AdminChatId",
"label": "Admin Chat ID",
"description": "Telegram chat ID for admin notifications (optional)"
},
{
"name": "LittleShop__ApiUrl",
"label": "LittleShop API URL",
"default": "http://host.docker.internal:8080",
"description": "URL to LittleShop API (e.g., https://api.shop.com)"
},
{
"name": "LittleShop__Username",
"label": "API Username",
"default": "admin",
"description": "LittleShop API username"
},
{
"name": "LittleShop__Password",
"label": "API Password",
"default": "admin",
"description": "LittleShop API password"
},
{
"name": "BotManager__ApiKey",
"label": "Bot API Key",
"description": "API key from LittleShop admin panel (optional)"
},
{
"name": "Database__EncryptionKey",
"label": "Database Encryption Key",
"description": "32-character key for database encryption (auto-generated if not provided)"
},
{
"name": "Privacy__Mode",
"label": "Privacy Mode",
"default": "strict",
"select": [
{
"text": "Strict (Maximum Privacy)",
"value": "strict"
},
{
"text": "Moderate (Balanced)",
"value": "moderate"
},
{
"text": "Relaxed (More Features)",
"value": "relaxed"
}
],
"description": "Privacy level for user data handling"
},
{
"name": "Privacy__DataRetentionHours",
"label": "Data Retention Hours",
"default": "24",
"description": "Hours to retain user data (1-168)"
},
{
"name": "Privacy__SessionTimeoutMinutes",
"label": "Session Timeout Minutes",
"default": "30",
"description": "Minutes before session expires (5-1440)"
},
{
"name": "Privacy__EphemeralByDefault",
"label": "Ephemeral Messages",
"default": "true",
"select": [
{
"text": "Enabled",
"value": "true"
},
{
"text": "Disabled",
"value": "false"
}
],
"description": "Enable ephemeral messages by default"
},
{
"name": "Features__EnableQRCodes",
"label": "Enable QR Codes",
"default": "true",
"select": [
{
"text": "Enabled",
"value": "true"
},
{
"text": "Disabled",
"value": "false"
}
]
},
{
"name": "Features__EnablePGPEncryption",
"label": "Enable PGP Encryption",
"default": "true",
"select": [
{
"text": "Enabled",
"value": "true"
},
{
"text": "Disabled",
"value": "false"
}
]
},
{
"name": "Features__EnableDisappearingMessages",
"label": "Enable Disappearing Messages",
"default": "true",
"select": [
{
"text": "Enabled",
"value": "true"
},
{
"text": "Disabled",
"value": "false"
}
]
}
],
"volumes": [
{
"container": "/app/data",
"bind": "!telebot-data"
},
{
"container": "/app/logs",
"bind": "!telebot-logs"
}
],
"labels": [
{
"name": "traefik.enable",
"value": "false"
},
{
"name": "com.littleshop.bot",
"value": "true"
}
]
},
{
"type": 3,
"title": "LittleShop Multi-Bot Stack",
"name": "littleshop-multibot",
"description": "Deploy multiple LittleShop TeleBots (Support, Sales, VIP)",
"note": "Requires multiple bot tokens and configuration. Edit the stack after creation to add environment variables.",
"categories": ["bots", "ecommerce", "telegram"],
"platform": "linux",
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/telegram.png",
"repository": {
"url": "https://github.com/yourusername/littleshop",
"stackfile": "TeleBot/docker-compose.multi.yml"
},
"env": [
{
"name": "LITTLESHOP_API_URL",
"label": "LittleShop API URL",
"default": "http://host.docker.internal:8080",
"description": "Shared API URL for all bots"
},
{
"name": "LITTLESHOP_USERNAME",
"label": "API Username",
"default": "admin"
},
{
"name": "LITTLESHOP_PASSWORD",
"label": "API Password",
"default": "admin"
},
{
"name": "SUPPORT_BOT_TOKEN",
"label": "Support Bot Token",
"description": "Token for customer support bot"
},
{
"name": "SUPPORT_BOT_API_KEY",
"label": "Support Bot API Key",
"description": "API key for support bot"
},
{
"name": "SUPPORT_ADMIN_CHAT_ID",
"label": "Support Admin Chat ID"
},
{
"name": "SUPPORT_DB_ENCRYPTION_KEY",
"label": "Support DB Encryption Key",
"description": "32-char key for support bot database"
},
{
"name": "SALES_BOT_TOKEN",
"label": "Sales Bot Token",
"description": "Token for sales/marketing bot"
},
{
"name": "SALES_BOT_API_KEY",
"label": "Sales Bot API Key",
"description": "API key for sales bot"
},
{
"name": "SALES_ADMIN_CHAT_ID",
"label": "Sales Admin Chat ID"
},
{
"name": "SALES_DB_ENCRYPTION_KEY",
"label": "Sales DB Encryption Key",
"description": "32-char key for sales bot database"
},
{
"name": "VIP_BOT_TOKEN",
"label": "VIP Bot Token",
"description": "Token for VIP customer bot"
},
{
"name": "VIP_BOT_API_KEY",
"label": "VIP Bot API Key",
"description": "API key for VIP bot"
},
{
"name": "VIP_ADMIN_CHAT_ID",
"label": "VIP Admin Chat ID"
},
{
"name": "VIP_DB_ENCRYPTION_KEY",
"label": "VIP DB Encryption Key",
"description": "32-char key for VIP bot database"
},
{
"name": "VIP_ENABLE_TOR",
"label": "Enable Tor for VIP Bot",
"default": "false",
"select": [
{
"text": "Enabled",
"value": "true"
},
{
"text": "Disabled",
"value": "false"
}
]
},
{
"name": "REDIS_PASSWORD",
"label": "Redis Password",
"description": "Password for shared Redis cache"
}
]
}
]
}

View File

@ -2,4 +2,4 @@
# https://curl.se/docs/http-cookies.html # https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk. # This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / TRUE 0 .AspNetCore.Cookies CfDJ8OZGzJDh-FtIgYN_FUICYtt5LS1JOam2mW2wri916e5XsmYDR_lQkRtYVVTC7r0wkdSNTM8aSMPBRFIGmuAwBTHadPXGRSuBshsviOnShSrsgxfj-8nrfMT0ojW-P2J3rWqHwzct7iXniiFhz006O76w75ToS7hAGwBt_EuReTazhch0dviMBpKsT1AxxudabJaa4VO5wwg9iSEFuII1ZYpKqDF8gNOTlPtAeMO3LcFCyTri02dJ_NTRlGtaqPtn4PIAjoiMS_hAHI9rdkQzAecc2gM2EU12dBy_HFE7xHF1e7y4aeVgSEsDw_er50wc5QgAbJ2oqdOay41vkZssCfMbU8cMKTQjyEbOQODiMJm_Wz4m_K326RtYqJNnRjF2Pls7VMzK9se38qh1gOEdvUDoe4JRHAYJ0lt0s_7Npith2Ck9zcaVP5PfdeQsf_yhYnTCXUQq7um0FesumdhmEPJ_sOoZx-WsJF5o5xDa_ja5lklgm0UY3Q4snSMI_FMHDceT1quZKUX3g9U61Nl1wy329N0510vAH93qMmLvD4Ar #HttpOnly_31.97.57.205 FALSE / FALSE 0 .AspNetCore.Cookies CfDJ8Hh_A9Sh3WBNi-S2OCZkQFY4WFyPqGN28cMhN1p1RyAh6E1a91SYc9cQbCOcfyQ06MrqOLNHfJbU9ghwTWAqZ_hzT4ujPSA3QgRWYJMqQBdE-YaxX7b27W9MTn9_DE9ANqPT1EzPD5ySOQq5exxqRvoca3ZH4ZmNOKC_ZXoQtU5_l-vmBHYg4_Ng94j-uShqC_Nu7OiHvRWaNwe29TNQmcDVJrJ6zEEKp-1eKNWz6yq62hvbXpjB0SH9REbNx_HOTaqSA9B81OFS6rsfKcLnSc2ermWGbVYgOoxCzg-Za-EMI--WktTqlNjaUUCzrNU2xgs9JFpH3ygoKGXRHWBKn6Qp5RQ4lXyNZCOfFQIsSSfN1YkC7doAikdvhAjTRg7UBhKdFhWWvYhP0aa1mfYkRqC1FMZ1LTPkJMPZsyrkajljKRONmA2iVvz9cEYVFljs1PaGJtgGBUH54ZTvCrZkMcKzzp12Q6pKCpFi_0zBnRCIROiChswR-eGyYKXRJ4JfpY93cXW08kJRZdnL6T_n4XIU7cJ6THMW-hqxioVQFkjh

View File

@ -1,17 +1,10 @@
#!/bin/bash #!/bin/bash
# LittleShop Deployment Script for Hostinger VPS # LittleShop + TeleBot Deployment Script for Hostinger
# Usage: ./deploy-to-hostinger.sh # This script deploys both LittleShop and TeleBot to a Hostinger VPS
# with BTCPay Server integration
set -e # Exit on any error set -e
# Configuration
HOSTINGER_HOST="31.97.57.205"
HOSTINGER_PORT="2255"
HOSTINGER_USER="sysadmin"
SSH_KEY="./Hostinger/vps_hardening_key"
REMOTE_DIR="/opt/littleshop"
SERVICE_NAME="littleshop"
# Colors for output # Colors for output
RED='\033[0;31m' RED='\033[0;31m'
@ -19,118 +12,105 @@ GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# Logging function echo -e "${GREEN}======================================${NC}"
log() { echo -e "${GREEN}LittleShop + TeleBot Hostinger Deploy${NC}"
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}" echo -e "${GREEN}======================================${NC}"
}
warn() { # Check if .env file exists
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}" if [ ! -f .env ]; then
} echo -e "${YELLOW}Warning: .env file not found!${NC}"
echo "Creating .env from template..."
cp .env.hostinger.template .env
echo -e "${RED}Please edit .env file with your configuration before continuing!${NC}"
echo "Required configurations:"
echo " - TELEGRAM_BOT_TOKEN"
echo " - TELEGRAM_ADMIN_CHAT_ID"
echo " - BTCPAY_WEBHOOK_SECRET (generate a secure random string)"
echo ""
read -p "Press enter after editing .env file to continue..."
fi
error() { # Load environment variables
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" source .env
# Validate required environment variables
if [ -z "$TELEGRAM_BOT_TOKEN" ]; then
echo -e "${RED}Error: TELEGRAM_BOT_TOKEN not set in .env file${NC}"
exit 1 exit 1
}
# Check if SSH key exists
if [ ! -f "$SSH_KEY" ]; then
error "SSH key not found at $SSH_KEY"
fi fi
# Check if required files exist if [ -z "$TELEGRAM_ADMIN_CHAT_ID" ]; then
if [ ! -f "hostinger-docker-compose.yml" ]; then echo -e "${RED}Error: TELEGRAM_ADMIN_CHAT_ID not set in .env file${NC}"
error "hostinger-docker-compose.yml not found" exit 1
fi fi
if [ ! -f "env.hostinger" ]; then echo -e "${GREEN}Configuration validated successfully${NC}"
warn "env.hostinger not found - you'll need to configure environment variables manually"
fi
log "Starting deployment to Hostinger VPS..." # Build Docker images
echo -e "${YELLOW}Building Docker images...${NC}"
docker-compose -f docker-compose.hostinger.yml build
# Test SSH connection # Stop existing containers (if any)
log "Testing SSH connection..." echo -e "${YELLOW}Stopping existing containers...${NC}"
ssh -i "$SSH_KEY" -p "$HOSTINGER_PORT" -o ConnectTimeout=10 "$HOSTINGER_USER@$HOSTINGER_HOST" "echo 'SSH connection successful'" || error "SSH connection failed" docker-compose -f docker-compose.hostinger.yml down
# Create remote directory # Start services
log "Creating remote directory structure..." echo -e "${YELLOW}Starting services...${NC}"
ssh -i "$SSH_KEY" -p "$HOSTINGER_PORT" "$HOSTINGER_USER@$HOSTINGER_HOST" "echo 'Phenom12#.' | sudo -S mkdir -p $REMOTE_DIR && echo 'Phenom12#.' | sudo -S chown $HOSTINGER_USER:$HOSTINGER_USER $REMOTE_DIR" docker-compose -f docker-compose.hostinger.yml up -d
# Copy files to server # Wait for services to be ready
log "Copying application files..." echo -e "${YELLOW}Waiting for services to start...${NC}"
scp -i "$SSH_KEY" -P "$HOSTINGER_PORT" -r LittleShop/ "$HOSTINGER_USER@$HOSTINGER_HOST:$REMOTE_DIR/"
scp -i "$SSH_KEY" -P "$HOSTINGER_PORT" hostinger-docker-compose.yml "$HOSTINGER_USER@$HOSTINGER_HOST:$REMOTE_DIR/docker-compose.yml"
scp -i "$SSH_KEY" -P "$HOSTINGER_PORT" nginx.conf "$HOSTINGER_USER@$HOSTINGER_HOST:$REMOTE_DIR/"
# Copy environment file if it exists
if [ -f "env.hostinger" ]; then
log "Copying environment configuration..."
scp -i "$SSH_KEY" -P "$HOSTINGER_PORT" env.hostinger "$HOSTINGER_USER@$HOSTINGER_HOST:$REMOTE_DIR/.env"
fi
# Deploy on remote server
log "Building and starting containers on remote server..."
ssh -i "$SSH_KEY" -p "$HOSTINGER_PORT" "$HOSTINGER_USER@$HOSTINGER_HOST" << 'EOF'
cd /opt/littleshop
# Stop existing containers if running
if docker-compose ps | grep -q "littleshop"; then
echo "Stopping existing containers..."
docker-compose down
fi
# Build and start new containers
echo "Building Docker image..."
docker-compose build
echo "Starting containers..."
docker-compose up -d
# Wait for container to be ready
echo "Waiting for application to start..."
sleep 10 sleep 10
# Check if container is running # Check service health
if docker-compose ps | grep -q "Up"; then echo -e "${YELLOW}Checking service health...${NC}"
echo "✅ Deployment successful!"
echo "Container status:"
docker-compose ps
echo ""
echo "Checking application health..."
# Try to curl the health endpoint # Check LittleShop
if curl -f http://localhost:8081/api/test > /dev/null 2>&1; then if curl -f -s http://localhost:8080/health > /dev/null; then
echo "✅ Application is responding on port 8081" echo -e "${GREEN}✓ LittleShop is running${NC}"
else
echo "⚠️ Application may still be starting up"
fi
echo ""
echo "📝 Next steps:"
echo "1. Configure your domain to point to this server"
echo "2. Set up SSL certificates if needed"
echo "3. Configure BTCPay Server integration"
echo "4. Test the application at http://31.97.57.205:8081"
else else
echo "❌ Deployment failed - containers not running" echo -e "${RED}✗ LittleShop health check failed${NC}"
docker-compose logs
exit 1
fi fi
EOF
if [ $? -eq 0 ]; then # Check TeleBot logs
log "🎉 Deployment completed successfully!" if docker logs littleshop-telebot 2>&1 | grep -q "Starting TeleBot"; then
log "Application should be available at:" echo -e "${GREEN}✓ TeleBot is running${NC}"
log " - http://$HOSTINGER_HOST:8081 (direct access)"
log " - http://shop.thebankofdebbie.giize.com (if DNS is configured)"
log ""
log "📋 Post-deployment checklist:"
log "1. Update DNS records to point shop.thebankofdebbie.giize.com to $HOSTINGER_HOST"
log "2. Configure SSL certificates"
log "3. Update BTCPay Server settings in .env file"
log "4. Test all application functionality"
log "5. Set up monitoring and backups"
else else
error "Deployment failed!" echo -e "${RED}✗ TeleBot may not be running correctly${NC}"
fi fi
# Show container status
echo ""
echo -e "${GREEN}Container Status:${NC}"
docker-compose -f docker-compose.hostinger.yml ps
# Show logs
echo ""
echo -e "${GREEN}Recent logs:${NC}"
echo -e "${YELLOW}LittleShop:${NC}"
docker logs --tail 10 littleshop
echo ""
echo -e "${YELLOW}TeleBot:${NC}"
docker logs --tail 10 littleshop-telebot
echo ""
echo -e "${GREEN}======================================${NC}"
echo -e "${GREEN}Deployment Complete!${NC}"
echo -e "${GREEN}======================================${NC}"
echo ""
echo "Services are running:"
echo " - LittleShop API: http://localhost:8080"
echo " - TeleBot: Check your Telegram bot"
echo ""
echo "BTCPay Server:"
echo " - URL: https://thebankofdebbie.giize.com"
echo " - Make sure to configure webhook in BTCPay:"
echo " Webhook URL: http://your-server-ip:8080/api/orders/payments/webhook"
echo ""
echo "To view logs:"
echo " docker logs -f littleshop"
echo " docker logs -f littleshop-telebot"
echo ""
echo "To stop services:"
echo " docker-compose -f docker-compose.hostinger.yml down"

View File

@ -0,0 +1,123 @@
version: '3.8'
services:
# LittleShop Main Application
littleshop:
build: .
image: littleshop:latest
container_name: littleshop
restart: unless-stopped
environment:
- ASPNETCORE_ENVIRONMENT=Hostinger
- ASPNETCORE_URLS=http://+:8080
# BTCPay Configuration - pointing to Hostinger BTCPay
- BTCPayServer__BaseUrl=https://thebankofdebbie.giize.com
- BTCPayServer__ApiKey=${BTCPAY_API_KEY:-994589c8b514531f867dd24c83a02b6381a5f4a2}
- BTCPayServer__StoreId=${BTCPAY_STORE_ID:-AoxXjM9NJT6P9C1MErkaawXaSchz8sFPYdQ9FyhmQz33}
- BTCPayServer__WebhookSecret=${BTCPAY_WEBHOOK_SECRET}
# Database
- ConnectionStrings__DefaultConnection=Data Source=/app/data/littleshop.db
# JWT
- Jwt__Key=${JWT_SECRET_KEY:-YourSuperSecretKeyThatIsAtLeast32CharactersLong!}
volumes:
- littleshop_data:/app/data
- littleshop_uploads:/app/wwwroot/uploads
- littleshop_logs:/app/logs
ports:
- "8080:8080"
networks:
- littleshop-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
# TeleBot Telegram Bot
telebot:
build:
context: .
dockerfile: TeleBot/TeleBot/Dockerfile
container_name: littleshop-telebot
restart: unless-stopped
environment:
- DOTNET_ENVIRONMENT=Production
- TZ=UTC
# Telegram Bot Configuration
- Telegram__BotToken=${TELEGRAM_BOT_TOKEN}
- Telegram__AdminChatId=${TELEGRAM_ADMIN_CHAT_ID}
- Telegram__UseWebhook=false
# LittleShop API Configuration - pointing to local container
- LittleShop__ApiUrl=http://littleshop:8080
- LittleShop__Username=${LITTLESHOP_USERNAME:-admin}
- LittleShop__Password=${LITTLESHOP_PASSWORD:-admin}
- LittleShop__UseTor=false
- LittleShop__BrandName=${BRAND_NAME:-Little Shop}
# Privacy Settings
- Privacy__Mode=strict
- Privacy__DataRetentionHours=24
- Privacy__SessionTimeoutMinutes=30
- Privacy__EnableAnalytics=false
- Privacy__EphemeralByDefault=true
- Privacy__EnableTor=false
# Database Configuration
- Database__ConnectionString=Filename=/app/data/telebot.db;Password=;
- Database__EncryptionKey=${DATABASE_ENCRYPTION_KEY:-CHANGE_THIS_KEY_IN_PRODUCTION}
# Features
- Features__EnableQRCodes=true
- Features__EnablePGPEncryption=true
- Features__EnableDisappearingMessages=true
# Redis (optional)
- Redis__Enabled=${REDIS_ENABLED:-false}
- Redis__ConnectionString=redis:6379
# Hangfire (optional)
- Hangfire__Enabled=${HANGFIRE_ENABLED:-false}
volumes:
- telebot_data:/app/data
- telebot_logs:/app/logs
networks:
- littleshop-network
depends_on:
- littleshop
- redis
healthcheck:
test: ["CMD", "pgrep", "-f", "dotnet.*TeleBot"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
# Redis Cache (Optional)
redis:
image: redis:7-alpine
container_name: littleshop-redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD:-RedisPassword123}
volumes:
- redis_data:/data
networks:
- littleshop-network
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 30s
timeout: 3s
retries: 5
volumes:
littleshop_data:
driver: local
littleshop_uploads:
driver: local
littleshop_logs:
driver: local
telebot_data:
driver: local
telebot_logs:
driver: local
redis_data:
driver: local
networks:
littleshop-network:
driver: bridge

BIN
hostinger-deployment.tar.gz Normal file

Binary file not shown.

Binary file not shown.

BIN
littleshop-complete.tar.gz Normal file

Binary file not shown.

BIN
littleshop-src.tar.gz Normal file

Binary file not shown.

BIN
publish/AutoMapper.dll Executable file

Binary file not shown.

BIN
publish/BTCPayServer.Client.dll Executable file

Binary file not shown.

Binary file not shown.

BIN
publish/BouncyCastle.Crypto.dll Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
publish/FluentValidation.dll Executable file

Binary file not shown.

BIN
publish/LittleShop Executable file

Binary file not shown.

1531
publish/LittleShop.deps.json Normal file

File diff suppressed because it is too large Load Diff

BIN
publish/LittleShop.dll Normal file

Binary file not shown.

BIN
publish/LittleShop.pdb Normal file

Binary file not shown.

View File

@ -0,0 +1,21 @@
{
"runtimeOptions": {
"tfm": "net9.0",
"frameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "9.0.0"
},
{
"name": "Microsoft.AspNetCore.App",
"version": "9.0.0"
}
],
"configProperties": {
"System.GC.Server": true,
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
"System.Reflection.NullabilityInfoContext.IsSupported": true,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

BIN
publish/Microsoft.Data.Sqlite.dll Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
publish/Microsoft.OpenApi.dll Executable file

Binary file not shown.

BIN
publish/NBitcoin.dll Executable file

Binary file not shown.

BIN
publish/Newtonsoft.Json.dll Executable file

Binary file not shown.

Binary file not shown.

BIN
publish/SQLitePCLRaw.core.dll Executable file

Binary file not shown.

Binary file not shown.

BIN
publish/Serilog.AspNetCore.dll Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
publish/Serilog.Sinks.Console.dll Executable file

Binary file not shown.

BIN
publish/Serilog.Sinks.Debug.dll Executable file

Binary file not shown.

BIN
publish/Serilog.Sinks.File.dll Executable file

Binary file not shown.

BIN
publish/Serilog.dll Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
{
"ProjectPath": "C:\\Production\\Source\\LittleShop\\LittleShop",
"ProjectType": "Project (ASP.NET Core)",
"TotalEndpoints": 115,
"AuthenticatedEndpoints": 78,
"TestableStates": 3,
"IdentifiedGaps": 224,
"SuggestedTests": 190,
"DeadLinks": 0,
"HttpErrors": 97,
"VisualIssues": 0,
"SecurityInsights": 1,
"PerformanceInsights": 1,
"OverallTestCoverage": 16.956521739130434,
"VisualConsistencyScore": 0,
"CriticalRecommendations": [
"CRITICAL: Test coverage is only 17.0% - implement comprehensive test suite",
"HIGH: Address 97 HTTP errors in the application",
"MEDIUM: Improve visual consistency - current score 0.0%",
"HIGH: Address 224 testing gaps for comprehensive coverage"
],
"GeneratedFiles": [
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\project_structure.json",
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\authentication_analysis.json",
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\endpoint_discovery.json",
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\coverage_analysis.json",
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\error_detection.json",
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\visual_testing.json",
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\intelligent_analysis.json"
]
}

View File

@ -0,0 +1,79 @@
{
"BusinessLogicInsights": [
{
"Component": "Claude CLI Integration",
"Insight": "Error analyzing business logic: Failed to execute Claude CLI: An error occurred trying to start process \u0027claude\u0027 with working directory \u0027C:\\Production\\Source\\TestAgent\u0027. The system cannot find the file specified.",
"Complexity": "Unknown",
"PotentialIssues": [],
"TestingRecommendations": [],
"Priority": "Medium"
}
],
"TestScenarioSuggestions": [
{
"ScenarioName": "Claude CLI Integration Error",
"Description": "Error generating test scenarios: Failed to execute Claude CLI: An error occurred trying to start process \u0027claude\u0027 with working directory \u0027C:\\Production\\Source\\TestAgent\u0027. The system cannot find the file specified.",
"TestType": "",
"Steps": [],
"ExpectedOutcomes": [],
"Priority": "Medium",
"RequiredData": [],
"Dependencies": []
}
],
"SecurityInsights": [
{
"VulnerabilityType": "Analysis Error",
"Location": "",
"Description": "Error analyzing security: Failed to execute Claude CLI: An error occurred trying to start process \u0027claude\u0027 with working directory \u0027C:\\Production\\Source\\TestAgent\u0027. The system cannot find the file specified.",
"Severity": "Medium",
"Recommendations": [],
"TestingApproaches": []
}
],
"PerformanceInsights": [
{
"Component": "Analysis Error",
"PotentialBottleneck": "Error analyzing performance: Failed to execute Claude CLI: An error occurred trying to start process \u0027claude\u0027 with working directory \u0027C:\\Production\\Source\\TestAgent\u0027. The system cannot find the file specified.",
"Impact": "Unknown",
"OptimizationSuggestions": [],
"TestingStrategies": []
}
],
"ArchitecturalRecommendations": [
{
"Category": "Analysis Error",
"Recommendation": "Error generating architectural recommendations: Failed to execute Claude CLI: An error occurred trying to start process \u0027claude\u0027 with working directory \u0027C:\\Production\\Source\\TestAgent\u0027. The system cannot find the file specified.",
"Rationale": "",
"Impact": "Unknown",
"ImplementationSteps": []
}
],
"GeneratedTestCases": [
{
"TestName": "Claude CLI Integration Error",
"TestCategory": "Error",
"Description": "Error generating test cases: Failed to execute Claude CLI: An error occurred trying to start process \u0027claude\u0027 with working directory \u0027C:\\Production\\Source\\TestAgent\u0027. The system cannot find the file specified.",
"TestCode": "",
"TestData": [],
"ExpectedOutcome": "",
"Reasoning": ""
}
],
"Summary": {
"TotalInsights": 4,
"HighPriorityItems": 0,
"GeneratedTestCases": 1,
"SecurityIssuesFound": 1,
"PerformanceOptimizations": 1,
"KeyFindings": [
"Performance optimization opportunities identified"
],
"NextSteps": [
"Review and prioritize security recommendations",
"Implement generated test cases",
"Address high-priority business logic testing gaps",
"Consider architectural improvements for better testability"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
{
"ConsistencyTests": [],
"AuthStateComparisons": [],
"ResponsiveTests": [],
"ComponentTests": [],
"Regressions": [],
"Summary": {
"TotalTests": 0,
"PassedTests": 0,
"FailedTests": 0,
"ConsistencyViolations": 0,
"ResponsiveIssues": 0,
"VisualRegressions": 0,
"OverallScore": 0,
"Recommendations": []
}
}

BIN
publish/WebPush.dll Executable file

Binary file not shown.

View File

@ -0,0 +1,46 @@
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=/app/data/littleshop.db"
},
"Jwt": {
"Key": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!",
"Issuer": "LittleShop",
"Audience": "LittleShop",
"ExpiryInHours": 24
},
"BTCPayServer": {
"BaseUrl": "https://thebankofdebbie.giize.com",
"ApiKey": "994589c8b514531f867dd24c83a02b6381a5f4a2",
"StoreId": "AoxXjM9NJT6P9C1MErkaawXaSchz8sFPYdQ9FyhmQz33",
"WebhookSecret": "your-webhook-secret-here"
},
"RoyalMail": {
"ClientId": "",
"ClientSecret": "",
"BaseUrl": "https://api.royalmail.net/",
"SenderAddress1": "SilverLabs Ltd, 123 Business Street",
"SenderCity": "London",
"SenderPostCode": "SW1A 1AA",
"SenderCountry": "United Kingdom"
},
"WebPush": {
"VapidPublicKey": "BMc6fFJZ8oIQKQzcl3kMnP9tTsjrm3oI_VxLt3lAGYUMWGInzDKn7jqclEoZzjvXy1QXGFb3dIun8mVBwh-QuS4",
"VapidPrivateKey": "dYuuagbz2CzCnPDFUpO_qkGLBgnN3MEFZQnjXNkc1MY",
"Subject": "mailto:admin@littleshop.local"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"BTCPayServer": "Debug"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://0.0.0.0:8080"
}
}
}
}

View File

@ -0,0 +1,52 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Data Source=littleshop.db"
},
"Jwt": {
"Key": "${JWT_SECRET_KEY}",
"Issuer": "LittleShop",
"Audience": "LittleShop-API",
"ExpiryMinutes": 60
},
"BTCPayServer": {
"ServerUrl": "${BTCPAY_SERVER_URL}",
"StoreId": "${BTCPAY_STORE_ID}",
"ApiKey": "${BTCPAY_API_KEY}",
"WebhookSecret": "${BTCPAY_WEBHOOK_SECRET}"
},
"AllowedHosts": "*",
"Urls": "http://+:8080",
"ForwardedHeaders": {
"ForwardedProtoHeaderName": "X-Forwarded-Proto",
"ForwardedForHeaderName": "X-Forwarded-For",
"ForwardedHostHeaderName": "X-Forwarded-Host"
},
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"MinimumLevel": "Information",
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "/app/logs/littleshop-.log",
"rollingInterval": "Day",
"retainedFileCountLimit": 7,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
}
}
]
}
}

38
publish/appsettings.json Normal file
View File

@ -0,0 +1,38 @@
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=littleshop.db"
},
"Jwt": {
"Key": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!",
"Issuer": "LittleShop",
"Audience": "LittleShop",
"ExpiryInHours": 24
},
"BTCPayServer": {
"BaseUrl": "https://pay.silverlabs.uk",
"ApiKey": "994589c8b514531f867dd24c83a02b6381a5f4a2",
"StoreId": "AoxXjM9NJT6P9C1MErkaawXaSchz8sFPYdQ9FyhmQz33",
"WebhookSecret": ""
},
"RoyalMail": {
"ClientId": "",
"ClientSecret": "",
"BaseUrl": "https://api.royalmail.net/",
"SenderAddress1": "SilverLabs Ltd, 123 Business Street",
"SenderCity": "London",
"SenderPostCode": "SW1A 1AA",
"SenderCountry": "United Kingdom"
},
"WebPush": {
"VapidPublicKey": "BMc6fFJZ8oIQKQzcl3kMnP9tTsjrm3oI_VxLt3lAGYUMWGInzDKn7jqclEoZzjvXy1QXGFb3dIun8mVBwh-QuS4",
"VapidPrivateKey": "dYuuagbz2CzCnPDFUpO_qkGLBgnN3MEFZQnjXNkc1MY",
"Subject": "mailto:admin@littleshop.local"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

Some files were not shown because too many files have changed in this diff Show More