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:
parent
36b393dd2e
commit
73e8773ea3
29
.env.hostinger.template
Normal file
29
.env.hostinger.template
Normal 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
|
||||||
113
BTCPAY_EXTERNAL_NODES_STATUS.md
Normal file
113
BTCPAY_EXTERNAL_NODES_STATUS.md
Normal 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
220
HOSTINGER-DEPLOYMENT.md
Normal 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.
|
||||||
@ -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; }
|
||||||
}
|
}
|
||||||
@ -22,4 +23,17 @@ public class ProductPhoto
|
|||||||
public string Url { get; set; } = string.Empty;
|
public string Url { get; set; } = string.Empty;
|
||||||
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; }
|
||||||
}
|
}
|
||||||
165
LittleShop/Areas/Admin/Controllers/BotRecoveryController.cs
Normal file
165
LittleShop/Areas/Admin/Controllers/BotRecoveryController.cs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using LittleShop.Services;
|
||||||
|
|
||||||
|
namespace LittleShop.Areas.Admin.Controllers;
|
||||||
|
|
||||||
|
[Area("Admin")]
|
||||||
|
[Authorize(Policy = "AdminOnly")]
|
||||||
|
public class BotRecoveryController : Controller
|
||||||
|
{
|
||||||
|
private readonly IBotContactService _contactService;
|
||||||
|
private readonly IBotService _botService;
|
||||||
|
private readonly ILogger<BotRecoveryController> _logger;
|
||||||
|
|
||||||
|
public BotRecoveryController(
|
||||||
|
IBotContactService contactService,
|
||||||
|
IBotService botService,
|
||||||
|
ILogger<BotRecoveryController> logger)
|
||||||
|
{
|
||||||
|
_contactService = contactService;
|
||||||
|
_botService = botService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: Admin/BotRecovery
|
||||||
|
public async Task<IActionResult> Index()
|
||||||
|
{
|
||||||
|
var allBots = await _botService.GetAllBotsAsync();
|
||||||
|
var botsWithStatus = allBots.Select(bot => new
|
||||||
|
{
|
||||||
|
Bot = bot,
|
||||||
|
Contacts = _contactService.GetBotContactsAsync(bot.Id).Result,
|
||||||
|
IsHealthy = bot.Status == Enums.BotStatus.Active &&
|
||||||
|
bot.LastSeenAt > DateTime.UtcNow.AddMinutes(-5)
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
ViewData["BotsWithStatus"] = botsWithStatus;
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: Admin/BotRecovery/PrepareRecovery/{botId}
|
||||||
|
public async Task<IActionResult> PrepareRecovery(Guid botId)
|
||||||
|
{
|
||||||
|
var recoveryData = await _contactService.PrepareContactRecoveryAsync(botId);
|
||||||
|
return View(recoveryData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Admin/BotRecovery/ExecuteRecovery
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> ExecuteRecovery(Guid fromBotId, Guid toBotId, bool notifyUsers = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Migrate contacts
|
||||||
|
var success = await _contactService.MigrateContactsAsync(fromBotId, toBotId);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Failed to migrate contacts. Check logs for details.";
|
||||||
|
return RedirectToAction(nameof(PrepareRecovery), new { botId = fromBotId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the new bot info for notifications
|
||||||
|
if (notifyUsers)
|
||||||
|
{
|
||||||
|
var toBot = await _botService.GetBotByIdAsync(toBotId);
|
||||||
|
var orphanedContacts = await _contactService.GetOrphanedContactsAsync(fromBotId);
|
||||||
|
|
||||||
|
foreach (var contact in orphanedContacts)
|
||||||
|
{
|
||||||
|
await _contactService.NotifyContactOfBotChangeAsync(
|
||||||
|
contact.TelegramUserId,
|
||||||
|
toBot.PlatformUsername);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update bot statuses
|
||||||
|
await _botService.UpdateBotStatusAsync(fromBotId, Enums.BotStatus.Retired);
|
||||||
|
|
||||||
|
TempData["Success"] = $"Successfully migrated contacts from bot {fromBotId} to {toBotId}";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to execute bot recovery");
|
||||||
|
TempData["Error"] = "An error occurred during recovery. Please try again.";
|
||||||
|
return RedirectToAction(nameof(PrepareRecovery), new { botId = fromBotId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: Admin/BotRecovery/Backup/{botId}
|
||||||
|
public async Task<IActionResult> Backup(Guid botId)
|
||||||
|
{
|
||||||
|
var backup = await _contactService.CreateContactBackupAsync(botId);
|
||||||
|
|
||||||
|
// Return as downloadable JSON file
|
||||||
|
var json = System.Text.Json.JsonSerializer.Serialize(backup, new System.Text.Json.JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
|
||||||
|
return File(bytes, "application/json", $"bot-contacts-{botId}-{DateTime.UtcNow:yyyyMMdd}.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Admin/BotRecovery/RestoreBackup
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RestoreBackup(Guid toBotId, IFormFile backupFile)
|
||||||
|
{
|
||||||
|
if (backupFile == null || backupFile.Length == 0)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Please select a backup file to restore";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = backupFile.OpenReadStream();
|
||||||
|
using var reader = new System.IO.StreamReader(stream);
|
||||||
|
var json = await reader.ReadToEndAsync();
|
||||||
|
|
||||||
|
var backup = System.Text.Json.JsonSerializer.Deserialize<ContactBackupDto>(json);
|
||||||
|
|
||||||
|
if (backup == null)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Invalid backup file format";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = await _contactService.RestoreContactsFromBackupAsync(toBotId, backup);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
TempData["Success"] = $"Successfully restored {backup.ContactCount} contacts to bot";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Failed to restore contacts from backup";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to restore backup");
|
||||||
|
TempData["Error"] = "An error occurred while restoring the backup";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: Admin/BotRecovery/ContactHistory/{telegramUserId}
|
||||||
|
public async Task<IActionResult> ContactHistory(long telegramUserId)
|
||||||
|
{
|
||||||
|
// Show all bot interactions for a specific user
|
||||||
|
ViewData["UserId"] = telegramUserId;
|
||||||
|
// Implementation would query all BotContacts for this user across all bots
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<form asp-action="Edit" method="post">
|
<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">
|
||||||
|
|||||||
80
LittleShop/Models/BotContact.cs
Normal file
80
LittleShop/Models/BotContact.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace LittleShop.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks all contacts for each bot, enabling contact recovery and migration
|
||||||
|
/// </summary>
|
||||||
|
public class BotContact
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
// Bot Association
|
||||||
|
[Required]
|
||||||
|
public Guid BotId { get; set; }
|
||||||
|
public virtual Bot Bot { get; set; } = null!;
|
||||||
|
|
||||||
|
// Telegram User Information
|
||||||
|
[Required]
|
||||||
|
public long TelegramUserId { get; set; }
|
||||||
|
|
||||||
|
[StringLength(100)]
|
||||||
|
public string TelegramUsername { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[StringLength(200)]
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[StringLength(50)]
|
||||||
|
public string FirstName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[StringLength(50)]
|
||||||
|
public string LastName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Contact Metadata
|
||||||
|
public DateTime FirstContactDate { get; set; }
|
||||||
|
public DateTime LastContactDate { get; set; }
|
||||||
|
public int TotalInteractions { get; set; }
|
||||||
|
public string LastKnownLanguage { get; set; } = "en";
|
||||||
|
|
||||||
|
// Relationship Status
|
||||||
|
public ContactStatus Status { get; set; } = ContactStatus.Active;
|
||||||
|
public string? StatusReason { get; set; }
|
||||||
|
|
||||||
|
// Customer Link (if they've made purchases)
|
||||||
|
public Guid? CustomerId { get; set; }
|
||||||
|
public virtual Customer? Customer { get; set; }
|
||||||
|
|
||||||
|
// Recovery Information
|
||||||
|
public bool IsRecovered { get; set; } = false;
|
||||||
|
public Guid? RecoveredFromBotId { get; set; }
|
||||||
|
public DateTime? RecoveredAt { get; set; }
|
||||||
|
|
||||||
|
// Backup Metadata
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
// Additional Contact Info (encrypted/hashed)
|
||||||
|
[StringLength(500)]
|
||||||
|
public string? EncryptedContactData { get; set; } // For storing additional contact methods
|
||||||
|
|
||||||
|
// Preferences and Notes
|
||||||
|
[StringLength(500)]
|
||||||
|
public string? Preferences { get; set; } // JSON string of user preferences
|
||||||
|
|
||||||
|
[StringLength(1000)]
|
||||||
|
public string? Notes { get; set; } // Admin notes about this contact
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ContactStatus
|
||||||
|
{
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
Blocked,
|
||||||
|
Migrated,
|
||||||
|
Lost,
|
||||||
|
Recovered
|
||||||
|
}
|
||||||
386
LittleShop/Services/BotContactService.cs
Normal file
386
LittleShop/Services/BotContactService.cs
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using LittleShop.Data;
|
||||||
|
using LittleShop.Models;
|
||||||
|
using LittleShop.DTOs;
|
||||||
|
|
||||||
|
namespace LittleShop.Services;
|
||||||
|
|
||||||
|
public interface IMessageDeliveryService
|
||||||
|
{
|
||||||
|
// Placeholder interface for compilation
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IBotContactService
|
||||||
|
{
|
||||||
|
Task<BotContact> RecordContactAsync(Guid botId, TelegramUserDto user);
|
||||||
|
Task<IEnumerable<BotContact>> GetBotContactsAsync(Guid botId, bool activeOnly = true);
|
||||||
|
Task<IEnumerable<BotContact>> GetOrphanedContactsAsync(Guid failedBotId);
|
||||||
|
Task<bool> MigrateContactsAsync(Guid fromBotId, Guid toBotId);
|
||||||
|
Task<BotContactRecoveryDto> PrepareContactRecoveryAsync(Guid failedBotId);
|
||||||
|
Task<bool> NotifyContactOfBotChangeAsync(long telegramUserId, string newBotUsername);
|
||||||
|
Task<ContactBackupDto> CreateContactBackupAsync(Guid botId);
|
||||||
|
Task<bool> RestoreContactsFromBackupAsync(Guid toBotId, ContactBackupDto backup);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BotContactService : IBotContactService
|
||||||
|
{
|
||||||
|
private readonly LittleShopContext _context;
|
||||||
|
private readonly ILogger<BotContactService> _logger;
|
||||||
|
private readonly IMessageDeliveryService _messageService;
|
||||||
|
|
||||||
|
public BotContactService(
|
||||||
|
LittleShopContext context,
|
||||||
|
ILogger<BotContactService> logger,
|
||||||
|
IMessageDeliveryService messageService)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
_messageService = messageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BotContact> RecordContactAsync(Guid botId, TelegramUserDto user)
|
||||||
|
{
|
||||||
|
var existingContact = await _context.BotContacts
|
||||||
|
.FirstOrDefaultAsync(c => c.BotId == botId && c.TelegramUserId == user.Id);
|
||||||
|
|
||||||
|
if (existingContact != null)
|
||||||
|
{
|
||||||
|
// Update existing contact
|
||||||
|
existingContact.TelegramUsername = user.Username ?? string.Empty;
|
||||||
|
existingContact.DisplayName = $"{user.FirstName} {user.LastName}".Trim();
|
||||||
|
existingContact.FirstName = user.FirstName ?? string.Empty;
|
||||||
|
existingContact.LastName = user.LastName ?? string.Empty;
|
||||||
|
existingContact.LastContactDate = DateTime.UtcNow;
|
||||||
|
existingContact.TotalInteractions++;
|
||||||
|
existingContact.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
_context.BotContacts.Update(existingContact);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Create new contact record
|
||||||
|
existingContact = new BotContact
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
BotId = botId,
|
||||||
|
TelegramUserId = user.Id,
|
||||||
|
TelegramUsername = user.Username ?? string.Empty,
|
||||||
|
DisplayName = $"{user.FirstName} {user.LastName}".Trim(),
|
||||||
|
FirstName = user.FirstName ?? string.Empty,
|
||||||
|
LastName = user.LastName ?? string.Empty,
|
||||||
|
FirstContactDate = DateTime.UtcNow,
|
||||||
|
LastContactDate = DateTime.UtcNow,
|
||||||
|
TotalInteractions = 1,
|
||||||
|
LastKnownLanguage = user.LanguageCode ?? "en",
|
||||||
|
Status = ContactStatus.Active,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
IsActive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.BotContacts.AddAsync(existingContact);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
_logger.LogInformation("Recorded contact {UserId} for bot {BotId}", user.Id, botId);
|
||||||
|
|
||||||
|
return existingContact;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<BotContact>> GetBotContactsAsync(Guid botId, bool activeOnly = true)
|
||||||
|
{
|
||||||
|
var query = _context.BotContacts.Where(c => c.BotId == botId);
|
||||||
|
|
||||||
|
if (activeOnly)
|
||||||
|
query = query.Where(c => c.IsActive && c.Status == ContactStatus.Active);
|
||||||
|
|
||||||
|
return await query
|
||||||
|
.OrderByDescending(c => c.LastContactDate)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<BotContact>> GetOrphanedContactsAsync(Guid failedBotId)
|
||||||
|
{
|
||||||
|
// Get contacts from failed bot that haven't been recovered yet
|
||||||
|
return await _context.BotContacts
|
||||||
|
.Where(c => c.BotId == failedBotId &&
|
||||||
|
c.Status == ContactStatus.Active &&
|
||||||
|
!c.IsRecovered)
|
||||||
|
.OrderByDescending(c => c.TotalInteractions) // Prioritize most active users
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> MigrateContactsAsync(Guid fromBotId, Guid toBotId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var contactsToMigrate = await GetOrphanedContactsAsync(fromBotId);
|
||||||
|
var toBot = await _context.Bots.FindAsync(toBotId);
|
||||||
|
|
||||||
|
if (toBot == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("Target bot {BotId} not found", toBotId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var contact in contactsToMigrate)
|
||||||
|
{
|
||||||
|
// Create new contact record for new bot
|
||||||
|
var newContact = new BotContact
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
BotId = toBotId,
|
||||||
|
TelegramUserId = contact.TelegramUserId,
|
||||||
|
TelegramUsername = contact.TelegramUsername,
|
||||||
|
DisplayName = contact.DisplayName,
|
||||||
|
FirstName = contact.FirstName,
|
||||||
|
LastName = contact.LastName,
|
||||||
|
FirstContactDate = DateTime.UtcNow,
|
||||||
|
LastContactDate = DateTime.UtcNow,
|
||||||
|
TotalInteractions = 0, // Reset for new bot
|
||||||
|
LastKnownLanguage = contact.LastKnownLanguage,
|
||||||
|
Status = ContactStatus.Recovered,
|
||||||
|
CustomerId = contact.CustomerId,
|
||||||
|
IsRecovered = true,
|
||||||
|
RecoveredFromBotId = fromBotId,
|
||||||
|
RecoveredAt = DateTime.UtcNow,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
IsActive = true,
|
||||||
|
Preferences = contact.Preferences,
|
||||||
|
Notes = $"Migrated from bot {fromBotId} on {DateTime.UtcNow:yyyy-MM-dd}"
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.BotContacts.AddAsync(newContact);
|
||||||
|
|
||||||
|
// Mark original contact as migrated
|
||||||
|
contact.Status = ContactStatus.Migrated;
|
||||||
|
contact.IsRecovered = true;
|
||||||
|
contact.UpdatedAt = DateTime.UtcNow;
|
||||||
|
_context.BotContacts.Update(contact);
|
||||||
|
|
||||||
|
_logger.LogInformation("Migrated contact {UserId} from bot {FromBot} to {ToBot}",
|
||||||
|
contact.TelegramUserId, fromBotId, toBotId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to migrate contacts from {FromBot} to {ToBot}", fromBotId, toBotId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BotContactRecoveryDto> PrepareContactRecoveryAsync(Guid failedBotId)
|
||||||
|
{
|
||||||
|
var orphanedContacts = await GetOrphanedContactsAsync(failedBotId);
|
||||||
|
var failedBot = await _context.Bots.FindAsync(failedBotId);
|
||||||
|
var availableBots = await _context.Bots
|
||||||
|
.Where(b => b.Id != failedBotId && b.Status == BotStatus.Active)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return new BotContactRecoveryDto
|
||||||
|
{
|
||||||
|
FailedBotId = failedBotId,
|
||||||
|
FailedBotName = failedBot?.Name ?? "Unknown",
|
||||||
|
OrphanedContactCount = orphanedContacts.Count(),
|
||||||
|
HighValueContacts = orphanedContacts
|
||||||
|
.Where(c => c.TotalInteractions > 10 || c.CustomerId != null)
|
||||||
|
.Count(),
|
||||||
|
AvailableRecoveryBots = availableBots.Select(b => new BotInfoDto
|
||||||
|
{
|
||||||
|
Id = b.Id,
|
||||||
|
Name = b.Name,
|
||||||
|
Status = b.Status.ToString(),
|
||||||
|
CurrentContactCount = _context.BotContacts.Count(c => c.BotId == b.Id && c.IsActive)
|
||||||
|
}).ToList(),
|
||||||
|
ContactDetails = orphanedContacts.Select(c => new ContactSummaryDto
|
||||||
|
{
|
||||||
|
TelegramUserId = c.TelegramUserId,
|
||||||
|
Username = c.TelegramUsername,
|
||||||
|
DisplayName = c.DisplayName,
|
||||||
|
TotalInteractions = c.TotalInteractions,
|
||||||
|
LastContactDate = c.LastContactDate,
|
||||||
|
IsCustomer = c.CustomerId != null
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> NotifyContactOfBotChangeAsync(long telegramUserId, string newBotUsername)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// This would integrate with the message delivery service
|
||||||
|
var message = $"Hello! Your previous bot is temporarily unavailable. " +
|
||||||
|
$"Please continue your conversation with our new bot: @{newBotUsername} " +
|
||||||
|
$"All your order history and preferences have been preserved.";
|
||||||
|
|
||||||
|
// Queue message for delivery when user contacts new bot
|
||||||
|
await _messageService.QueueRecoveryMessageAsync(telegramUserId, message);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to notify user {UserId} of bot change", telegramUserId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ContactBackupDto> CreateContactBackupAsync(Guid botId)
|
||||||
|
{
|
||||||
|
var contacts = await GetBotContactsAsync(botId, activeOnly: false);
|
||||||
|
var bot = await _context.Bots.FindAsync(botId);
|
||||||
|
|
||||||
|
return new ContactBackupDto
|
||||||
|
{
|
||||||
|
BackupId = Guid.NewGuid(),
|
||||||
|
BotId = botId,
|
||||||
|
BotName = bot?.Name ?? "Unknown",
|
||||||
|
BackupDate = DateTime.UtcNow,
|
||||||
|
ContactCount = contacts.Count(),
|
||||||
|
Contacts = contacts.Select(c => new ContactExportDto
|
||||||
|
{
|
||||||
|
TelegramUserId = c.TelegramUserId,
|
||||||
|
TelegramUsername = c.TelegramUsername,
|
||||||
|
DisplayName = c.DisplayName,
|
||||||
|
FirstName = c.FirstName,
|
||||||
|
LastName = c.LastName,
|
||||||
|
FirstContactDate = c.FirstContactDate,
|
||||||
|
LastContactDate = c.LastContactDate,
|
||||||
|
TotalInteractions = c.TotalInteractions,
|
||||||
|
LastKnownLanguage = c.LastKnownLanguage,
|
||||||
|
Status = c.Status.ToString(),
|
||||||
|
CustomerId = c.CustomerId,
|
||||||
|
Preferences = c.Preferences,
|
||||||
|
Notes = c.Notes
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RestoreContactsFromBackupAsync(Guid toBotId, ContactBackupDto backup)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var contactData in backup.Contacts)
|
||||||
|
{
|
||||||
|
// Check if contact already exists for this bot
|
||||||
|
var existingContact = await _context.BotContacts
|
||||||
|
.FirstOrDefaultAsync(c => c.BotId == toBotId &&
|
||||||
|
c.TelegramUserId == contactData.TelegramUserId);
|
||||||
|
|
||||||
|
if (existingContact == null)
|
||||||
|
{
|
||||||
|
var newContact = new BotContact
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
BotId = toBotId,
|
||||||
|
TelegramUserId = contactData.TelegramUserId,
|
||||||
|
TelegramUsername = contactData.TelegramUsername,
|
||||||
|
DisplayName = contactData.DisplayName,
|
||||||
|
FirstName = contactData.FirstName,
|
||||||
|
LastName = contactData.LastName,
|
||||||
|
FirstContactDate = contactData.FirstContactDate,
|
||||||
|
LastContactDate = contactData.LastContactDate,
|
||||||
|
TotalInteractions = contactData.TotalInteractions,
|
||||||
|
LastKnownLanguage = contactData.LastKnownLanguage,
|
||||||
|
Status = Enum.Parse<ContactStatus>(contactData.Status),
|
||||||
|
CustomerId = contactData.CustomerId,
|
||||||
|
IsRecovered = true,
|
||||||
|
RecoveredFromBotId = backup.BotId,
|
||||||
|
RecoveredAt = DateTime.UtcNow,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
IsActive = true,
|
||||||
|
Preferences = contactData.Preferences,
|
||||||
|
Notes = $"Restored from backup {backup.BackupId} on {DateTime.UtcNow:yyyy-MM-dd}"
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.BotContacts.AddAsync(newContact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
_logger.LogInformation("Restored {Count} contacts from backup to bot {BotId}",
|
||||||
|
backup.Contacts.Count, toBotId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to restore contacts from backup");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTOs for contact management
|
||||||
|
public class TelegramUserDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string? Username { get; set; }
|
||||||
|
public string? FirstName { get; set; }
|
||||||
|
public string? LastName { get; set; }
|
||||||
|
public string? LanguageCode { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BotContactRecoveryDto
|
||||||
|
{
|
||||||
|
public Guid FailedBotId { get; set; }
|
||||||
|
public string FailedBotName { get; set; } = string.Empty;
|
||||||
|
public int OrphanedContactCount { get; set; }
|
||||||
|
public int HighValueContacts { get; set; }
|
||||||
|
public List<BotInfoDto> AvailableRecoveryBots { get; set; } = new();
|
||||||
|
public List<ContactSummaryDto> ContactDetails { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BotInfoDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public int CurrentContactCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ContactSummaryDto
|
||||||
|
{
|
||||||
|
public long TelegramUserId { get; set; }
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
public int TotalInteractions { get; set; }
|
||||||
|
public DateTime LastContactDate { get; set; }
|
||||||
|
public bool IsCustomer { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ContactBackupDto
|
||||||
|
{
|
||||||
|
public Guid BackupId { get; set; }
|
||||||
|
public Guid BotId { get; set; }
|
||||||
|
public string BotName { get; set; } = string.Empty;
|
||||||
|
public DateTime BackupDate { get; set; }
|
||||||
|
public int ContactCount { get; set; }
|
||||||
|
public List<ContactExportDto> Contacts { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ContactExportDto
|
||||||
|
{
|
||||||
|
public long TelegramUserId { get; set; }
|
||||||
|
public string TelegramUsername { get; set; } = string.Empty;
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
public string FirstName { get; set; } = string.Empty;
|
||||||
|
public string LastName { get; set; } = string.Empty;
|
||||||
|
public DateTime FirstContactDate { get; set; }
|
||||||
|
public DateTime LastContactDate { get; set; }
|
||||||
|
public int TotalInteractions { get; set; }
|
||||||
|
public string LastKnownLanguage { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public Guid? CustomerId { get; set; }
|
||||||
|
public string? Preferences { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
}
|
||||||
131
TeleBot/.env.multi.example
Normal file
131
TeleBot/.env.multi.example
Normal 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
|
||||||
504
TeleBot/MULTI-HOST-DEPLOYMENT.md
Normal file
504
TeleBot/MULTI-HOST-DEPLOYMENT.md
Normal 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
|
||||||
7
TeleBot/TeleBot/BotConfig.cs
Normal file
7
TeleBot/TeleBot/BotConfig.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace TeleBot
|
||||||
|
{
|
||||||
|
public static class BotConfig
|
||||||
|
{
|
||||||
|
public static string BrandName { get; set; } = "Little Shop";
|
||||||
|
}
|
||||||
|
}
|
||||||
101
TeleBot/TeleBot/Controllers/WebhookController.cs
Normal file
101
TeleBot/TeleBot/Controllers/WebhookController.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
{
|
{
|
||||||
@ -97,7 +98,15 @@ namespace TeleBot.Handlers
|
|||||||
case "add":
|
case "add":
|
||||||
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,27 +345,49 @@ 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)
|
||||||
{
|
{
|
||||||
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
|
||||||
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)
|
||||||
@ -370,6 +401,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)
|
||||||
{
|
{
|
||||||
@ -406,18 +580,18 @@ namespace TeleBot.Handlers
|
|||||||
await bot.AnswerCallbackQueryAsync("", "Your cart is empty", showAlert: true);
|
await bot.AnswerCallbackQueryAsync("", "Your cart is empty", showAlert: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize order flow
|
// Initialize order flow
|
||||||
session.OrderFlow = new OrderFlowData
|
session.OrderFlow = new OrderFlowData
|
||||||
{
|
{
|
||||||
UsePGPEncryption = session.Privacy.RequirePGP
|
UsePGPEncryption = session.Privacy.RequirePGP
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -11,10 +11,11 @@ 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)
|
||||||
{
|
{
|
||||||
existingItem.Quantity += quantity;
|
existingItem.Quantity += quantity;
|
||||||
@ -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
|
||||||
@ -32,7 +34,7 @@ namespace TeleBot.Models
|
|||||||
newItem.UpdateTotalPrice(); // Ensure total is calculated after all properties are set
|
newItem.UpdateTotalPrice(); // Ensure total is calculated after all properties are set
|
||||||
Items.Add(newItem);
|
Items.Add(newItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdatedAt = DateTime.UtcNow;
|
UpdatedAt = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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; }
|
||||||
|
|||||||
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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,10 +88,11 @@ 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);
|
||||||
|
|
||||||
_logger.LogInformation("Bot manager service stopped");
|
_logger.LogInformation("Bot manager service stopped");
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,9 +26,11 @@ 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,
|
||||||
ILogger<TelegramBotService> logger,
|
ILogger<TelegramBotService> logger,
|
||||||
@ -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,16 +48,27 @@ 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();
|
||||||
@ -128,9 +144,82 @@ namespace TeleBot
|
|||||||
ApiRequestException apiException => $"Telegram API Error: [{apiException.ErrorCode}] {apiException.Message}",
|
ApiRequestException apiException => $"Telegram API Error: [{apiException.ErrorCode}] {apiException.Message}",
|
||||||
_ => exception.ToString()
|
_ => exception.ToString()
|
||||||
};
|
};
|
||||||
|
|
||||||
_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -328,13 +328,50 @@ 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[]
|
||||||
|
|||||||
@ -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" +
|
||||||
@ -83,43 +83,93 @@ namespace TeleBot.UI
|
|||||||
public static string FormatSingleProduct(Product product)
|
public static string FormatSingleProduct(Product product)
|
||||||
{
|
{
|
||||||
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))
|
||||||
{
|
{
|
||||||
// Truncate description for bubble format
|
// Truncate description for bubble format
|
||||||
var desc = product.Description.Length > 100
|
var desc = product.Description.Length > 100
|
||||||
? product.Description.Substring(0, 100) + "..."
|
? product.Description.Substring(0, 100) + "..."
|
||||||
: product.Description;
|
: product.Description;
|
||||||
sb.AppendLine($"\n_{desc}_");
|
sb.AppendLine($"\n_{desc}_");
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string FormatProductDetail(Product product)
|
public static string FormatProductDetail(Product product)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
sb.AppendLine($"🛍️ *{product.Name}*\n");
|
sb.AppendLine($"🛍️ *{product.Name}*\n");
|
||||||
sb.AppendLine($"💰 *Price:* ${product.Price:F2}");
|
sb.AppendLine($"💰 *Price:* ${product.Price:F2}");
|
||||||
sb.AppendLine($"⚖️ *Weight:* {product.Weight} {product.WeightUnit}");
|
sb.AppendLine($"⚖️ *Weight:* {product.Weight} {product.WeightUnit}");
|
||||||
sb.AppendLine($"📁 *Category:* {product.CategoryName ?? "Uncategorized"}");
|
sb.AppendLine($"📁 *Category:* {product.CategoryName ?? "Uncategorized"}");
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(product.Description))
|
if (!string.IsNullOrEmpty(product.Description))
|
||||||
{
|
{
|
||||||
sb.AppendLine($"\n📝 *Description:*\n{product.Description}");
|
sb.AppendLine($"\n📝 *Description:*\n{product.Description}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (product.Photos.Any())
|
if (product.Photos.Any())
|
||||||
{
|
{
|
||||||
sb.AppendLine($"\n🖼️ _{product.Photos.Count} photo(s) available_");
|
sb.AppendLine($"\n🖼️ _{product.Photos.Count} photo(s) available_");
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.AppendLine("\nSelect quantity and add to cart:");
|
sb.AppendLine("\nSelect quantity and add to cart:");
|
||||||
|
|
||||||
|
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();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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
253
TeleBot/deploy-bot.sh
Executable 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"
|
||||||
284
TeleBot/docker-compose.multi.yml
Normal file
284
TeleBot/docker-compose.multi.yml
Normal 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
|
||||||
295
TeleBot/portainer-template.json
Normal file
295
TeleBot/portainer-template.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
123
docker-compose.hostinger.yml
Normal file
123
docker-compose.hostinger.yml
Normal 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
BIN
hostinger-deployment.tar.gz
Normal file
Binary file not shown.
BIN
littleshop-btcpay-fix.tar.gz
Normal file
BIN
littleshop-btcpay-fix.tar.gz
Normal file
Binary file not shown.
BIN
littleshop-complete.tar.gz
Normal file
BIN
littleshop-complete.tar.gz
Normal file
Binary file not shown.
BIN
littleshop-src.tar.gz
Normal file
BIN
littleshop-src.tar.gz
Normal file
Binary file not shown.
BIN
publish/AutoMapper.dll
Executable file
BIN
publish/AutoMapper.dll
Executable file
Binary file not shown.
BIN
publish/BTCPayServer.Client.dll
Executable file
BIN
publish/BTCPayServer.Client.dll
Executable file
Binary file not shown.
BIN
publish/BTCPayServer.Lightning.Common.dll
Executable file
BIN
publish/BTCPayServer.Lightning.Common.dll
Executable file
Binary file not shown.
BIN
publish/BouncyCastle.Crypto.dll
Executable file
BIN
publish/BouncyCastle.Crypto.dll
Executable file
Binary file not shown.
BIN
publish/FluentValidation.AspNetCore.dll
Executable file
BIN
publish/FluentValidation.AspNetCore.dll
Executable file
Binary file not shown.
BIN
publish/FluentValidation.DependencyInjectionExtensions.dll
Executable file
BIN
publish/FluentValidation.DependencyInjectionExtensions.dll
Executable file
Binary file not shown.
BIN
publish/FluentValidation.dll
Executable file
BIN
publish/FluentValidation.dll
Executable file
Binary file not shown.
BIN
publish/LittleShop
Executable file
BIN
publish/LittleShop
Executable file
Binary file not shown.
1531
publish/LittleShop.deps.json
Normal file
1531
publish/LittleShop.deps.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
publish/LittleShop.dll
Normal file
BIN
publish/LittleShop.dll
Normal file
Binary file not shown.
BIN
publish/LittleShop.pdb
Normal file
BIN
publish/LittleShop.pdb
Normal file
Binary file not shown.
21
publish/LittleShop.runtimeconfig.json
Normal file
21
publish/LittleShop.runtimeconfig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
publish/LittleShop.staticwebassets.endpoints.json
Normal file
1
publish/LittleShop.staticwebassets.endpoints.json
Normal file
File diff suppressed because one or more lines are too long
BIN
publish/Microsoft.AspNetCore.Authentication.JwtBearer.dll
Executable file
BIN
publish/Microsoft.AspNetCore.Authentication.JwtBearer.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.AspNetCore.Identity.EntityFrameworkCore.dll
Executable file
BIN
publish/Microsoft.AspNetCore.Identity.EntityFrameworkCore.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.Data.Sqlite.dll
Executable file
BIN
publish/Microsoft.Data.Sqlite.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.EntityFrameworkCore.Abstractions.dll
Executable file
BIN
publish/Microsoft.EntityFrameworkCore.Abstractions.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.EntityFrameworkCore.InMemory.dll
Executable file
BIN
publish/Microsoft.EntityFrameworkCore.InMemory.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.EntityFrameworkCore.Relational.dll
Executable file
BIN
publish/Microsoft.EntityFrameworkCore.Relational.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.EntityFrameworkCore.Sqlite.dll
Executable file
BIN
publish/Microsoft.EntityFrameworkCore.Sqlite.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.EntityFrameworkCore.dll
Executable file
BIN
publish/Microsoft.EntityFrameworkCore.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.Extensions.Caching.Abstractions.dll
Executable file
BIN
publish/Microsoft.Extensions.Caching.Abstractions.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.Extensions.Caching.Memory.dll
Executable file
BIN
publish/Microsoft.Extensions.Caching.Memory.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.Extensions.DependencyInjection.Abstractions.dll
Executable file
BIN
publish/Microsoft.Extensions.DependencyInjection.Abstractions.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.Extensions.DependencyInjection.dll
Executable file
BIN
publish/Microsoft.Extensions.DependencyInjection.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.Extensions.DependencyModel.dll
Executable file
BIN
publish/Microsoft.Extensions.DependencyModel.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.Extensions.Logging.Abstractions.dll
Executable file
BIN
publish/Microsoft.Extensions.Logging.Abstractions.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.Extensions.Logging.dll
Executable file
BIN
publish/Microsoft.Extensions.Logging.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.Extensions.Options.dll
Executable file
BIN
publish/Microsoft.Extensions.Options.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.Extensions.Primitives.dll
Executable file
BIN
publish/Microsoft.Extensions.Primitives.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.IdentityModel.Abstractions.dll
Executable file
BIN
publish/Microsoft.IdentityModel.Abstractions.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.IdentityModel.JsonWebTokens.dll
Executable file
BIN
publish/Microsoft.IdentityModel.JsonWebTokens.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.IdentityModel.Logging.dll
Executable file
BIN
publish/Microsoft.IdentityModel.Logging.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll
Executable file
BIN
publish/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.IdentityModel.Protocols.dll
Executable file
BIN
publish/Microsoft.IdentityModel.Protocols.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.IdentityModel.Tokens.dll
Executable file
BIN
publish/Microsoft.IdentityModel.Tokens.dll
Executable file
Binary file not shown.
BIN
publish/Microsoft.OpenApi.dll
Executable file
BIN
publish/Microsoft.OpenApi.dll
Executable file
Binary file not shown.
BIN
publish/NBitcoin.dll
Executable file
BIN
publish/NBitcoin.dll
Executable file
Binary file not shown.
BIN
publish/Newtonsoft.Json.dll
Executable file
BIN
publish/Newtonsoft.Json.dll
Executable file
Binary file not shown.
BIN
publish/SQLitePCLRaw.batteries_v2.dll
Executable file
BIN
publish/SQLitePCLRaw.batteries_v2.dll
Executable file
Binary file not shown.
BIN
publish/SQLitePCLRaw.core.dll
Executable file
BIN
publish/SQLitePCLRaw.core.dll
Executable file
Binary file not shown.
BIN
publish/SQLitePCLRaw.provider.e_sqlite3.dll
Executable file
BIN
publish/SQLitePCLRaw.provider.e_sqlite3.dll
Executable file
Binary file not shown.
BIN
publish/Serilog.AspNetCore.dll
Executable file
BIN
publish/Serilog.AspNetCore.dll
Executable file
Binary file not shown.
BIN
publish/Serilog.Extensions.Hosting.dll
Executable file
BIN
publish/Serilog.Extensions.Hosting.dll
Executable file
Binary file not shown.
BIN
publish/Serilog.Extensions.Logging.dll
Executable file
BIN
publish/Serilog.Extensions.Logging.dll
Executable file
Binary file not shown.
BIN
publish/Serilog.Formatting.Compact.dll
Executable file
BIN
publish/Serilog.Formatting.Compact.dll
Executable file
Binary file not shown.
BIN
publish/Serilog.Settings.Configuration.dll
Executable file
BIN
publish/Serilog.Settings.Configuration.dll
Executable file
Binary file not shown.
BIN
publish/Serilog.Sinks.Console.dll
Executable file
BIN
publish/Serilog.Sinks.Console.dll
Executable file
Binary file not shown.
BIN
publish/Serilog.Sinks.Debug.dll
Executable file
BIN
publish/Serilog.Sinks.Debug.dll
Executable file
Binary file not shown.
BIN
publish/Serilog.Sinks.File.dll
Executable file
BIN
publish/Serilog.Sinks.File.dll
Executable file
Binary file not shown.
BIN
publish/Serilog.dll
Executable file
BIN
publish/Serilog.dll
Executable file
Binary file not shown.
BIN
publish/Swashbuckle.AspNetCore.Swagger.dll
Executable file
BIN
publish/Swashbuckle.AspNetCore.Swagger.dll
Executable file
Binary file not shown.
BIN
publish/Swashbuckle.AspNetCore.SwaggerGen.dll
Executable file
BIN
publish/Swashbuckle.AspNetCore.SwaggerGen.dll
Executable file
Binary file not shown.
BIN
publish/Swashbuckle.AspNetCore.SwaggerUI.dll
Executable file
BIN
publish/Swashbuckle.AspNetCore.SwaggerUI.dll
Executable file
Binary file not shown.
BIN
publish/System.IdentityModel.Tokens.Jwt.dll
Executable file
BIN
publish/System.IdentityModel.Tokens.Jwt.dll
Executable file
Binary file not shown.
2447
publish/TestAgent_Results/authentication_analysis.json
Normal file
2447
publish/TestAgent_Results/authentication_analysis.json
Normal file
File diff suppressed because it is too large
Load Diff
6861
publish/TestAgent_Results/coverage_analysis.json
Normal file
6861
publish/TestAgent_Results/coverage_analysis.json
Normal file
File diff suppressed because it is too large
Load Diff
2940
publish/TestAgent_Results/endpoint_discovery.json
Normal file
2940
publish/TestAgent_Results/endpoint_discovery.json
Normal file
File diff suppressed because it is too large
Load Diff
1386
publish/TestAgent_Results/error_detection.json
Normal file
1386
publish/TestAgent_Results/error_detection.json
Normal file
File diff suppressed because it is too large
Load Diff
31
publish/TestAgent_Results/executive_summary.json
Normal file
31
publish/TestAgent_Results/executive_summary.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
79
publish/TestAgent_Results/intelligent_analysis.json
Normal file
79
publish/TestAgent_Results/intelligent_analysis.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
1669
publish/TestAgent_Results/project_structure.json
Normal file
1669
publish/TestAgent_Results/project_structure.json
Normal file
File diff suppressed because it is too large
Load Diff
17
publish/TestAgent_Results/visual_testing.json
Normal file
17
publish/TestAgent_Results/visual_testing.json
Normal 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
BIN
publish/WebPush.dll
Executable file
Binary file not shown.
46
publish/appsettings.Hostinger.json
Normal file
46
publish/appsettings.Hostinger.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
publish/appsettings.Production.json
Normal file
52
publish/appsettings.Production.json
Normal 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
38
publish/appsettings.json
Normal 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
Loading…
Reference in New Issue
Block a user