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 bool IsActive { get; set; }
|
||||
public List<ProductPhoto> Photos { get; set; } = new();
|
||||
public List<ProductVariation> Variations { get; set; } = new();
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
@ -23,3 +24,16 @@ public class ProductPhoto
|
||||
public string? AltText { 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">
|
||||
@Html.AntiForgeryToken()
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<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.Services;
|
||||
using TeleBot.UI;
|
||||
using LittleShop.Client.Models;
|
||||
|
||||
namespace TeleBot.Handlers
|
||||
{
|
||||
@ -98,6 +99,14 @@ namespace TeleBot.Handlers
|
||||
await HandleAddToCart(bot, callbackQuery, session, data);
|
||||
break;
|
||||
|
||||
case "quickbuy":
|
||||
await HandleQuickBuy(bot, callbackQuery, session, data);
|
||||
break;
|
||||
|
||||
case "quickbuyvar":
|
||||
await HandleQuickBuyWithVariation(bot, callbackQuery, session, data);
|
||||
break;
|
||||
|
||||
case "cart":
|
||||
await HandleViewCart(bot, callbackQuery.Message, session);
|
||||
break;
|
||||
@ -281,7 +290,7 @@ namespace TeleBot.Handlers
|
||||
|
||||
// Use carousel service to send products with images
|
||||
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)
|
||||
@ -294,7 +303,7 @@ namespace TeleBot.Handlers
|
||||
|
||||
// Use carousel service to send products with images
|
||||
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)
|
||||
@ -336,9 +345,10 @@ namespace TeleBot.Handlers
|
||||
|
||||
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 quantity = int.Parse(data[2]);
|
||||
Guid? variationId = data.Length > 3 ? Guid.Parse(data[3]) : null;
|
||||
|
||||
var product = await _shopService.GetProductAsync(productId);
|
||||
if (product == null)
|
||||
@ -347,16 +357,37 @@ namespace TeleBot.Handlers
|
||||
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(
|
||||
callbackQuery.Id,
|
||||
$"✅ Added {quantity}x {product.Name} to cart",
|
||||
$"✅ Added {quantity}x {itemName} to cart",
|
||||
showAlert: false
|
||||
);
|
||||
|
||||
// Show cart
|
||||
await HandleViewCart(bot, callbackQuery.Message!, session);
|
||||
// Send new cart message instead of editing
|
||||
await SendNewCartMessage(bot, callbackQuery.Message!.Chat.Id, session);
|
||||
}
|
||||
|
||||
private async Task HandleViewCart(ITelegramBotClient bot, Message message, UserSession session)
|
||||
@ -371,6 +402,149 @@ namespace TeleBot.Handlers
|
||||
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)
|
||||
{
|
||||
var item = session.Cart.Items.FirstOrDefault(i => i.ProductId == productId);
|
||||
@ -415,9 +589,9 @@ namespace TeleBot.Handlers
|
||||
|
||||
session.State = SessionState.CheckoutFlow;
|
||||
|
||||
await bot.EditMessageTextAsync(
|
||||
// Send new message for checkout instead of editing
|
||||
await bot.SendTextMessageAsync(
|
||||
message.Chat.Id,
|
||||
message.MessageId,
|
||||
"📦 *Checkout - Step 1/5*\n\n" +
|
||||
"Please enter your shipping name:\n\n" +
|
||||
"_Reply to this message with your name_",
|
||||
@ -459,9 +633,9 @@ namespace TeleBot.Handlers
|
||||
// Store order ID for payment
|
||||
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>>()
|
||||
?? new List<string> { "BTC", "XMR", "USDT", "LTC" };
|
||||
?? new List<string> { "BTC", "XMR", "LTC", "DASH" };
|
||||
|
||||
await bot.EditMessageTextAsync(
|
||||
message.Chat.Id,
|
||||
|
||||
@ -189,7 +189,7 @@ namespace TeleBot.Handlers
|
||||
// Send products as carousel with images
|
||||
await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, categoryName, 1);
|
||||
|
||||
session.State = Models.SessionState.BrowsingProducts;
|
||||
session.State = Models.SessionState.ViewingProducts;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -11,9 +11,10 @@ namespace TeleBot.Models
|
||||
public DateTime CreatedAt { 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)
|
||||
{
|
||||
@ -25,6 +26,7 @@ namespace TeleBot.Models
|
||||
var newItem = new CartItem
|
||||
{
|
||||
ProductId = productId,
|
||||
VariationId = variationId,
|
||||
ProductName = productName,
|
||||
UnitPrice = price,
|
||||
Quantity = quantity
|
||||
@ -85,6 +87,7 @@ namespace TeleBot.Models
|
||||
public class CartItem
|
||||
{
|
||||
public Guid ProductId { get; set; }
|
||||
public Guid? VariationId { get; set; }
|
||||
public string ProductName { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; }
|
||||
public decimal UnitPrice { get; set; }
|
||||
|
||||
@ -4,6 +4,8 @@ using System.Threading.Tasks;
|
||||
using Hangfire;
|
||||
using Hangfire.LiteDB;
|
||||
using LittleShop.Client.Extensions;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@ -14,8 +16,7 @@ using TeleBot;
|
||||
using TeleBot.Handlers;
|
||||
using TeleBot.Services;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
public static string BrandName ?? "Little Shop";
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
// Configuration
|
||||
builder.Configuration
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
@ -23,6 +24,9 @@ builder.Configuration
|
||||
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
|
||||
.AddEnvironmentVariables();
|
||||
|
||||
// Add MVC Controllers for webhook endpoints
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// Serilog
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Information()
|
||||
@ -49,7 +53,8 @@ builder.Services.AddLittleShopClient(options =>
|
||||
options.TimeoutSeconds = 30;
|
||||
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>();
|
||||
@ -80,10 +85,10 @@ builder.Services.AddSingleton<ICommandHandler, CommandHandler>();
|
||||
builder.Services.AddSingleton<ICallbackHandler, CallbackHandler>();
|
||||
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.AddSingleton<BotManagerService>();
|
||||
builder.Services.AddHostedService<BotManagerService>();
|
||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<BotManagerService>());
|
||||
|
||||
// Message Delivery Service - Single instance
|
||||
builder.Services.AddSingleton<MessageDeliveryService>();
|
||||
@ -94,11 +99,26 @@ builder.Services.AddHostedService<MessageDeliveryService>(sp => sp.GetRequiredSe
|
||||
builder.Services.AddHttpClient<ProductCarouselService>();
|
||||
builder.Services.AddSingleton<IProductCarouselService, ProductCarouselService>();
|
||||
|
||||
// Bot Service
|
||||
builder.Services.AddHostedService<TelegramBotService>();
|
||||
// Bot Service - Single instance
|
||||
builder.Services.AddSingleton<TelegramBotService>();
|
||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<TelegramBotService>());
|
||||
|
||||
// Build and run
|
||||
var host = builder.Build();
|
||||
// Build the application
|
||||
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
|
||||
{
|
||||
@ -106,8 +126,9 @@ try
|
||||
Log.Information("Privacy Mode: {PrivacyMode}", builder.Configuration["Privacy:Mode"]);
|
||||
Log.Information("Ephemeral by Default: {Ephemeral}", builder.Configuration["Privacy:EphemeralByDefault"]);
|
||||
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)
|
||||
{
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
@ -20,9 +21,12 @@ namespace TeleBot.Services
|
||||
private readonly SessionManager _sessionManager;
|
||||
private Timer? _heartbeatTimer;
|
||||
private Timer? _metricsTimer;
|
||||
private Timer? _settingsSyncTimer;
|
||||
private string? _botKey;
|
||||
private Guid? _botId;
|
||||
private readonly Dictionary<string, decimal> _metricsBuffer;
|
||||
private TelegramBotService? _telegramBotService;
|
||||
private string? _lastKnownBotToken;
|
||||
|
||||
public BotManagerService(
|
||||
IConfiguration configuration,
|
||||
@ -37,6 +41,11 @@ namespace TeleBot.Services
|
||||
_metricsBuffer = new Dictionary<string, decimal>();
|
||||
}
|
||||
|
||||
public void SetTelegramBotService(TelegramBotService telegramBotService)
|
||||
{
|
||||
_telegramBotService = telegramBotService;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@ -64,6 +73,9 @@ namespace TeleBot.Services
|
||||
// Start metrics timer (every 60 seconds)
|
||||
_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");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -76,6 +88,7 @@ namespace TeleBot.Services
|
||||
{
|
||||
_heartbeatTimer?.Change(Timeout.Infinite, 0);
|
||||
_metricsTimer?.Change(Timeout.Infinite, 0);
|
||||
_settingsSyncTimer?.Change(Timeout.Infinite, 0);
|
||||
|
||||
// Send final metrics before stopping
|
||||
SendMetrics(null);
|
||||
@ -161,23 +174,42 @@ namespace TeleBot.Services
|
||||
{
|
||||
if (string.IsNullOrEmpty(_botKey)) return;
|
||||
|
||||
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 settings = await GetSettingsAsync();
|
||||
if (settings != null)
|
||||
{
|
||||
var settingsJson = await response.Content.ReadAsStringAsync();
|
||||
var settings = JsonSerializer.Deserialize<Dictionary<string, object>>(settingsJson);
|
||||
|
||||
// Apply settings to configuration
|
||||
// This would update the running configuration with server settings
|
||||
_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)
|
||||
{
|
||||
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()
|
||||
{
|
||||
_heartbeatTimer?.Dispose();
|
||||
_metricsTimer?.Dispose();
|
||||
_settingsSyncTimer?.Dispose();
|
||||
}
|
||||
|
||||
// DTOs for API responses
|
||||
|
||||
@ -9,8 +9,10 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Telegram.Bot;
|
||||
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 TeleBot.UI;
|
||||
|
||||
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 SendSingleProductWithImageAsync(ITelegramBotClient botClient, long chatId, Product product);
|
||||
Task<InputOnlineFile?> GetProductImageAsync(Product product);
|
||||
Task<InputFile?> GetProductImageAsync(Product product);
|
||||
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
|
||||
{
|
||||
@ -256,16 +258,16 @@ namespace TeleBot.Services
|
||||
var cacheKey = $"{product.Id}_{photo.Id}";
|
||||
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
|
||||
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)
|
||||
{
|
||||
@ -281,7 +283,8 @@ namespace TeleBot.Services
|
||||
if (string.IsNullOrEmpty(imageUrl))
|
||||
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 &&
|
||||
response.Content.Headers.ContentType?.MediaType?.StartsWith("image/") == true;
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@ -24,8 +26,10 @@ namespace TeleBot
|
||||
private readonly ICallbackHandler _callbackHandler;
|
||||
private readonly IMessageHandler _messageHandler;
|
||||
private readonly IMessageDeliveryService _messageDeliveryService;
|
||||
private readonly BotManagerService _botManagerService;
|
||||
private ITelegramBotClient? _botClient;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private string? _currentBotToken;
|
||||
|
||||
public TelegramBotService(
|
||||
IConfiguration configuration,
|
||||
@ -34,7 +38,8 @@ namespace TeleBot
|
||||
ICommandHandler commandHandler,
|
||||
ICallbackHandler callbackHandler,
|
||||
IMessageHandler messageHandler,
|
||||
IMessageDeliveryService messageDeliveryService)
|
||||
IMessageDeliveryService messageDeliveryService,
|
||||
BotManagerService botManagerService)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
@ -43,17 +48,28 @@ namespace TeleBot
|
||||
_callbackHandler = callbackHandler;
|
||||
_messageHandler = messageHandler;
|
||||
_messageDeliveryService = messageDeliveryService;
|
||||
_botManagerService = botManagerService;
|
||||
}
|
||||
|
||||
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")
|
||||
{
|
||||
_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;
|
||||
}
|
||||
|
||||
_currentBotToken = botToken;
|
||||
|
||||
_botClient = new TelegramBotClient(botToken);
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
@ -132,5 +148,78 @@ namespace TeleBot
|
||||
_logger.LogError(exception, "Bot error: {ErrorMessage}", errorMessage);
|
||||
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(),
|
||||
Url = "https://via.placeholder.com/300x200.jpg",
|
||||
IsMain = true
|
||||
SortOrder = 0 // Use SortOrder = 0 to indicate main photo
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -329,12 +329,49 @@ namespace TeleBot.UI
|
||||
return new InlineKeyboardMarkup(new[]
|
||||
{
|
||||
new[] {
|
||||
InlineKeyboardButton.WithCallbackData("🛒 Buy Now", $"add:{productId}:1"),
|
||||
InlineKeyboardButton.WithCallbackData("🛒 Quick Buy", $"quickbuy:{productId}:1"),
|
||||
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)
|
||||
{
|
||||
return new InlineKeyboardMarkup(new[]
|
||||
|
||||
@ -12,13 +12,13 @@ namespace TeleBot.UI
|
||||
{
|
||||
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" +
|
||||
"🖼️ *New Feature:* Browse products with beautiful image carousels!\n\n" +
|
||||
"How can I help you today?";
|
||||
}
|
||||
|
||||
return $"🔒 *Welcome to {Program.BrandName}*\n\n" +
|
||||
return $"🔒 *Welcome to {BotConfig.BrandName}*\n\n" +
|
||||
"🛡️ *Your Privacy Matters:*\n" +
|
||||
"• No account required\n" +
|
||||
"• Ephemeral sessions by default\n" +
|
||||
@ -85,7 +85,18 @@ namespace TeleBot.UI
|
||||
var sb = new StringBuilder();
|
||||
|
||||
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))
|
||||
{
|
||||
@ -123,6 +134,45 @@ namespace TeleBot.UI
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string FormatProductWithVariations(Product product)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"🛍️ *{product.Name}*\n");
|
||||
|
||||
if (product.Variations?.Any() == true)
|
||||
{
|
||||
sb.AppendLine("📦 *Available Options:*\n");
|
||||
foreach (var variation in product.Variations.OrderBy(v => v.Quantity))
|
||||
{
|
||||
var savings = variation.Quantity > 1
|
||||
? $" (${variation.PricePerUnit:F2} each)"
|
||||
: "";
|
||||
sb.AppendLine($"• *{variation.Name}*: ${variation.Price:F2}{savings}");
|
||||
if (!string.IsNullOrEmpty(variation.Description))
|
||||
{
|
||||
sb.AppendLine($" _{variation.Description}_");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"💰 *Price:* ${product.Price:F2}");
|
||||
}
|
||||
|
||||
sb.AppendLine($"\n⚖️ *Weight:* {product.Weight} {product.WeightUnit}");
|
||||
sb.AppendLine($"📁 *Category:* {product.CategoryName ?? "Uncategorized"}");
|
||||
|
||||
if (!string.IsNullOrEmpty(product.Description))
|
||||
{
|
||||
sb.AppendLine($"\n📝 *Description:*\n{product.Description}");
|
||||
}
|
||||
|
||||
sb.AppendLine("\n*Select an option to add to cart:*");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string FormatCart(ShoppingCart cart)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
@ -275,7 +325,7 @@ namespace TeleBot.UI
|
||||
"/cancel - Cancel current operation\n" +
|
||||
"/delete - Delete all your data\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()
|
||||
|
||||
@ -6,16 +6,21 @@
|
||||
},
|
||||
"BotManager": {
|
||||
"ApiKey": "",
|
||||
"Comment": "This will be populated after first registration"
|
||||
"Comment": "This will be populated after first registration with admin panel"
|
||||
},
|
||||
"Telegram": {
|
||||
"BotToken": "7880403661:AAGma1wAyoHsmG45iO6VvHCqzimhJX1pp14",
|
||||
"BotToken": "",
|
||||
"AdminChatId": "",
|
||||
"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": {
|
||||
"ApiUrl": "https://localhost:5001",
|
||||
"ApiUrl": "http://localhost:8080",
|
||||
"OnionUrl": "",
|
||||
"Username": "admin",
|
||||
"Password": "admin",
|
||||
@ -66,11 +71,14 @@
|
||||
"Cryptocurrencies": [
|
||||
"BTC",
|
||||
"XMR",
|
||||
"USDT",
|
||||
"LTC",
|
||||
"ETH",
|
||||
"ZEC",
|
||||
"DASH",
|
||||
"DOGE"
|
||||
]
|
||||
"DASH"
|
||||
],
|
||||
"Kestrel": {
|
||||
"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
|
||||
# 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
|
||||
|
||||
# LittleShop Deployment Script for Hostinger VPS
|
||||
# Usage: ./deploy-to-hostinger.sh
|
||||
# LittleShop + TeleBot Deployment Script for Hostinger
|
||||
# This script deploys both LittleShop and TeleBot to a Hostinger VPS
|
||||
# with BTCPay Server integration
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# 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"
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
@ -19,118 +12,105 @@ GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}"
|
||||
}
|
||||
echo -e "${GREEN}======================================${NC}"
|
||||
echo -e "${GREEN}LittleShop + TeleBot Hostinger Deploy${NC}"
|
||||
echo -e "${GREEN}======================================${NC}"
|
||||
|
||||
warn() {
|
||||
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}"
|
||||
}
|
||||
# Check if .env file exists
|
||||
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() {
|
||||
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}"
|
||||
# Load environment variables
|
||||
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
|
||||
}
|
||||
|
||||
# Check if SSH key exists
|
||||
if [ ! -f "$SSH_KEY" ]; then
|
||||
error "SSH key not found at $SSH_KEY"
|
||||
fi
|
||||
|
||||
# Check if required files exist
|
||||
if [ ! -f "hostinger-docker-compose.yml" ]; then
|
||||
error "hostinger-docker-compose.yml not found"
|
||||
if [ -z "$TELEGRAM_ADMIN_CHAT_ID" ]; then
|
||||
echo -e "${RED}Error: TELEGRAM_ADMIN_CHAT_ID not set in .env file${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "env.hostinger" ]; then
|
||||
warn "env.hostinger not found - you'll need to configure environment variables manually"
|
||||
fi
|
||||
echo -e "${GREEN}Configuration validated successfully${NC}"
|
||||
|
||||
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
|
||||
log "Testing SSH connection..."
|
||||
ssh -i "$SSH_KEY" -p "$HOSTINGER_PORT" -o ConnectTimeout=10 "$HOSTINGER_USER@$HOSTINGER_HOST" "echo 'SSH connection successful'" || error "SSH connection failed"
|
||||
# Stop existing containers (if any)
|
||||
echo -e "${YELLOW}Stopping existing containers...${NC}"
|
||||
docker-compose -f docker-compose.hostinger.yml down
|
||||
|
||||
# Create remote directory
|
||||
log "Creating remote directory structure..."
|
||||
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"
|
||||
# Start services
|
||||
echo -e "${YELLOW}Starting services...${NC}"
|
||||
docker-compose -f docker-compose.hostinger.yml up -d
|
||||
|
||||
# Copy files to server
|
||||
log "Copying application files..."
|
||||
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..."
|
||||
# Wait for services to be ready
|
||||
echo -e "${YELLOW}Waiting for services to start...${NC}"
|
||||
sleep 10
|
||||
|
||||
# Check if container is running
|
||||
if docker-compose ps | grep -q "Up"; then
|
||||
echo "✅ Deployment successful!"
|
||||
echo "Container status:"
|
||||
docker-compose ps
|
||||
echo ""
|
||||
echo "Checking application health..."
|
||||
# Check service health
|
||||
echo -e "${YELLOW}Checking service health...${NC}"
|
||||
|
||||
# Try to curl the health endpoint
|
||||
if curl -f http://localhost:8081/api/test > /dev/null 2>&1; then
|
||||
echo "✅ Application is responding on port 8081"
|
||||
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"
|
||||
# Check LittleShop
|
||||
if curl -f -s http://localhost:8080/health > /dev/null; then
|
||||
echo -e "${GREEN}✓ LittleShop is running${NC}"
|
||||
else
|
||||
echo "❌ Deployment failed - containers not running"
|
||||
docker-compose logs
|
||||
exit 1
|
||||
echo -e "${RED}✗ LittleShop health check failed${NC}"
|
||||
fi
|
||||
EOF
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log "🎉 Deployment completed successfully!"
|
||||
log "Application should be available at:"
|
||||
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"
|
||||
# Check TeleBot logs
|
||||
if docker logs littleshop-telebot 2>&1 | grep -q "Starting TeleBot"; then
|
||||
echo -e "${GREEN}✓ TeleBot is running${NC}"
|
||||
else
|
||||
error "Deployment failed!"
|
||||
echo -e "${RED}✗ TeleBot may not be running correctly${NC}"
|
||||
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