Compare commits
No commits in common. "521bff2c7d33289263eeb2ae052c63387602df43" and "1aed286facc6e11e4fc5fd057bb6d2d5cc95c6d4" have entirely different histories.
521bff2c7d
...
1aed286fac
491
CLAUDE.md
491
CLAUDE.md
@ -1,78 +1,357 @@
|
|||||||
# LittleShop - E-Commerce Platform
|
# LittleShop Development Progress
|
||||||
|
|
||||||
## Overview
|
> 📋 **See [ROADMAP.md](./ROADMAP.md) for development priorities and security fixes**
|
||||||
|
> 📌 **See [WORKING_BASELINE_2024-09-24.md](./WORKING_BASELINE_2024-09-24.md) for current working configuration**
|
||||||
|
> ⚠️ **See [Deployment Best Practices](#deployment-best-practices) below for critical deployment requirements**
|
||||||
|
|
||||||
LittleShop is an ASP.NET Core 9.0 e-commerce platform with integrated Telegram bot support and cryptocurrency payment processing.
|
## Project Status: ✅ FULLY OPERATIONAL - OCTOBER 4, 2025
|
||||||
|
|
||||||
## Architecture
|
### 🔧 **CRITICAL INCIDENT RESOLVED (October 4, 2025)**
|
||||||
|
|
||||||
|
**Production Outage & Recovery:**
|
||||||
|
- **Incident**: Database schema mismatch caused complete system failure
|
||||||
|
- **Root Cause**: Code deployed without applying database migrations
|
||||||
|
- **Impact**: 502 errors, TeleBot offline, Product catalog unavailable
|
||||||
|
- **Resolution**: Database restored from backup, migrations applied, networking fixed
|
||||||
|
- **Prevention**: Enhanced CI/CD pipeline with automatic migration support
|
||||||
|
|
||||||
|
**Key Lessons Learned:**
|
||||||
|
1. ❌ **NEVER deploy code changes without corresponding database migrations**
|
||||||
|
2. ✅ **CI/CD now automatically applies migrations** from `LittleShop/Migrations/*.sql`
|
||||||
|
3. ✅ **Always verify container networking** (docker-compose prefixes network names)
|
||||||
|
4. ✅ **Maintain regular database backups** (saved production data)
|
||||||
|
|
||||||
|
### 🚀 **CURRENT PRODUCTION STATUS**
|
||||||
|
|
||||||
|
**All Systems Operational:**
|
||||||
|
- ✅ **LittleShop API**: Running at `http://littleshop:5000` (internal) / `http://localhost:5100` (host)
|
||||||
|
- ✅ **TeleBot**: Connected via `littleshop_littleshop-network`, authenticated with API
|
||||||
|
- ✅ **Nginx Proxy Manager**: Proxying `https://admin.dark.side` → `littleshop:5000`
|
||||||
|
- ✅ **Database**: SQLite with variant pricing migrations applied (508KB)
|
||||||
|
- ✅ **Networks**: Proper isolation with `littleshop_littleshop-network` and `silverpay_silverpay-network`
|
||||||
|
|
||||||
|
**Production Configuration:**
|
||||||
|
- **Server**: srv1002428.hstgr.cloud (31.97.57.205)
|
||||||
|
- **Container Names**: `littleshop`, `telebot-service`, `nginx-proxy-manager`
|
||||||
|
- **Docker Networks**: `littleshop_littleshop-network`, `silverpay_silverpay-network`
|
||||||
|
- **Volume**: `littleshop_littleshop_data` (note the docker-compose prefix!)
|
||||||
|
- **Database**: `/app/data/littleshop-production.db` inside container
|
||||||
|
|
||||||
|
## Deployment Best Practices
|
||||||
|
|
||||||
|
### **Pre-Deployment Checklist**
|
||||||
|
1. ✅ Verify all database migrations are in `LittleShop/Migrations/` and committed
|
||||||
|
2. ✅ Test migrations locally before deploying to production
|
||||||
|
3. ✅ Ensure docker-compose.yml matches production configuration
|
||||||
|
4. ✅ Verify TeleBot API URL points to `http://littleshop:5000` (NOT `littleshop-admin:8080`)
|
||||||
|
5. ✅ Check network names include docker-compose prefix (e.g., `littleshop_littleshop-network`)
|
||||||
|
|
||||||
|
### **CI/CD Pipeline Workflow**
|
||||||
|
The `.gitlab-ci.yml` pipeline automatically:
|
||||||
|
1. Builds Docker images with `--no-cache`
|
||||||
|
2. Copies images to production VPS via SSH
|
||||||
|
3. Stops running containers
|
||||||
|
4. **Applies database migrations** (with automatic backup)
|
||||||
|
5. Starts LittleShop with `docker-compose up -d`
|
||||||
|
6. Starts TeleBot with correct API URL and network connections
|
||||||
|
7. Runs health checks on product catalog API
|
||||||
|
|
||||||
|
### **Manual Deployment Commands** (Emergency Use Only)
|
||||||
|
```bash
|
||||||
|
# Connect to production server
|
||||||
|
ssh -i ~/.ssh/littleshop_deploy_key -p 2255 sysadmin@srv1002428.hstgr.cloud
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
cd /opt/littleshop
|
||||||
|
docker stop telebot-service littleshop
|
||||||
|
docker rm telebot-service
|
||||||
|
|
||||||
|
# Apply migration manually
|
||||||
|
docker run --rm -v littleshop_littleshop_data:/data -v $(pwd)/LittleShop/Migrations:/migrations alpine sh -c '
|
||||||
|
apk add sqlite
|
||||||
|
sqlite3 /data/littleshop-production.db < /migrations/YourMigration.sql
|
||||||
|
'
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
docker-compose up -d
|
||||||
|
docker run -d --name telebot-service --network silverpay_silverpay-network \
|
||||||
|
-e LittleShop__ApiUrl=http://littleshop:5000 localhost:5000/telebot:latest
|
||||||
|
docker network connect littleshop_littleshop-network telebot-service
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Database Migration Format**
|
||||||
|
Place migration files in `LittleShop/Migrations/` with `.sql` extension:
|
||||||
|
```sql
|
||||||
|
-- Migration: Description of changes
|
||||||
|
-- Date: YYYY-MM-DD
|
||||||
|
|
||||||
|
ALTER TABLE TableName ADD COLUMN NewColumn DataType;
|
||||||
|
CREATE INDEX IF NOT EXISTS IndexName ON TableName (ColumnName);
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Network Architecture**
|
||||||
|
```
|
||||||
|
nginx-proxy-manager ──┐
|
||||||
|
│
|
||||||
|
├─── littleshop_littleshop-network ─── littleshop:5000
|
||||||
|
│ └── telebot-service
|
||||||
|
│
|
||||||
|
telebot-service ──────┴─── silverpay_silverpay-network ─── tor-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Status: ✅ FULLY OPERATIONAL BASELINE - SEPTEMBER 24, 2024
|
||||||
|
|
||||||
|
### 🎯 **WORKING BASELINE ESTABLISHED (September 24, 2024, 20:15 UTC)**
|
||||||
|
|
||||||
|
**All systems operational and integrated:**
|
||||||
|
- ✅ **TeleBot**: Fixed checkout flow (single address message), no duplicate commands
|
||||||
|
- ✅ **LittleShop Admin**: CSRF tokens fixed, Pending Payment tab added, rebranded to TeleShop
|
||||||
|
- ✅ **SilverPay**: Payment creation fixed (fiat_amount field), currency conversion working
|
||||||
|
- ✅ **Integration**: All containers on same network, DNS resolution working
|
||||||
|
- ✅ **Payments**: GBP → Crypto conversion with live rates (£10 = 0.00011846 BTC @ £84,415/BTC)
|
||||||
|
|
||||||
|
### 🚀 **FULL SYSTEM DEPLOYMENT (September 20, 2025)** ✅
|
||||||
|
|
||||||
|
#### **Production Deployment Complete**
|
||||||
|
- **LittleShop API**: Running on srv1002428.hstgr.cloud:8080
|
||||||
|
- **SilverPAY Gateway**: Running on 31.97.57.205:8001
|
||||||
|
- **Database**: PostgreSQL and Redis operational
|
||||||
|
- **E2E Testing**: Core functionality verified
|
||||||
|
- **Git Status**: All changes committed and pushed (commit: 13aa20f)
|
||||||
|
|
||||||
|
#### **E2E Test Results**
|
||||||
|
- ✅ Health checks passing
|
||||||
|
- ✅ Product catalog operational (10 products, 3 categories)
|
||||||
|
- ✅ Order creation with validation working
|
||||||
|
- ✅ SilverPAY integration connected
|
||||||
|
- ⚠️ JWT authentication needs configuration
|
||||||
|
- ⚠️ Payment endpoint requires API key setup
|
||||||
|
|
||||||
|
#### **Configuration Required**
|
||||||
|
1. **JWT Secret**: Set environment variable on server
|
||||||
|
2. **SilverPAY API Key**: Configure in appsettings.Production.json
|
||||||
|
3. **Systemd Services**: Create for automatic startup
|
||||||
|
4. **Nginx**: Configure SSL and reverse proxy
|
||||||
|
5. **Logging**: Set up rotation and monitoring
|
||||||
|
|
||||||
|
#### **Access Points**
|
||||||
|
- **API**: http://srv1002428.hstgr.cloud:8080
|
||||||
|
- **Admin Panel**: http://srv1002428.hstgr.cloud:8080/Admin
|
||||||
|
- **API Docs**: http://srv1002428.hstgr.cloud:8080/swagger
|
||||||
|
- **SilverPAY**: http://31.97.57.205:8001
|
||||||
|
|
||||||
|
## Previous Updates: ✅ BTCPAY SERVER MULTI-CRYPTO CONFIGURED - SEPTEMBER 12, 2025
|
||||||
|
|
||||||
|
### 🚀 **BTCPAY SERVER INTEGRATION FIXED (September 19, 2025)** ✅
|
||||||
|
|
||||||
|
#### **Production Deployment Successful**
|
||||||
|
- **Fixed**: Invoice creation now uses GBP (fiat) instead of cryptocurrency
|
||||||
|
- **Fixed**: Proper checkout link generation for customer payments
|
||||||
|
- **Fixed**: Enhanced error logging and debugging
|
||||||
|
- **API Credentials**: Updated and working
|
||||||
|
- **Connection Status**: ✅ Connected to BTCPay v2.2.1
|
||||||
|
- **Store Configuration**: CvdvHoncGLM7TdMYRAG6Z15YuxQfxeMWRYwi9gvPhh5R
|
||||||
|
|
||||||
|
### 🚀 **BTCPAY SERVER DEPLOYMENT (September 11-12, 2025)** ✅
|
||||||
|
|
||||||
|
#### **Multi-Cryptocurrency BTCPay Server Configured** ✅
|
||||||
|
- **Host**: Hostinger VPS (srv1002428.hstgr.cloud, thebankofdebbie.giize.com)
|
||||||
|
- **Cryptocurrencies**: Bitcoin (BTC), Dogecoin (DOGE), Monero (XMR), Ethereum (ETH), Zcash (ZEC)
|
||||||
|
- **Network**: Tor integration with onion addresses for privacy
|
||||||
|
- **Storage**: Pruned mode configured (Bitcoin: 10GB max, Others: 3GB max)
|
||||||
|
- **Access**: Both clearnet HTTPS and Tor onion service available
|
||||||
|
|
||||||
|
#### **Critical Technical Breakthrough - Bitcoin Pruning Fix** ✅
|
||||||
|
- **Problem**: BTCPay Docker Compose YAML parsing broken - `BITCOIN_EXTRA_ARGS` not passed to container
|
||||||
|
- **Root Cause**: BTCPay's docker-compose generator creates corrupted multiline YAML that Docker can't parse
|
||||||
|
- **Multiple Failed Attempts**:
|
||||||
|
- ❌ Manual bitcoin.conf editing (overwritten by entrypoint script)
|
||||||
|
- ❌ docker-compose.yml direct editing (YAML formatting issues)
|
||||||
|
- ❌ .env file approach (not inherited properly)
|
||||||
|
- ❌ YAML format variations (`|-`, `|`, `>` - all failed)
|
||||||
|
- **SOLUTION**: `docker-compose.override.yml` with clean YAML formatting
|
||||||
|
- **Success Evidence**: `Prune configured to target 10000 MiB on disk for block and undo files.`
|
||||||
|
|
||||||
|
#### **BTCPay Configuration Details**
|
||||||
|
- **Bitcoin Core**: Pruned (10GB max), Tor-only networking (`onlynet=onion`)
|
||||||
|
- **Dogecoin**: Configured but needs pruning configuration applied
|
||||||
|
- **Monero**: Daemon operational, wallet configuration in progress
|
||||||
|
- **Ethereum**: Configured in BTCPay but container needs investigation
|
||||||
|
- **Zcash**: Wallet container present, main daemon needs configuration
|
||||||
|
- **Tor Integration**: Complete with hidden service generation
|
||||||
|
- **SSL**: Let's Encrypt certificates via nginx proxy
|
||||||
|
|
||||||
|
#### **Infrastructure Lessons Learned**
|
||||||
|
- **Docker Compose Override Files**: Survive BTCPay updates, proper way to customize configuration
|
||||||
|
- **BTCPay Template System**: The generated docker-compose.yml gets overwritten on updates
|
||||||
|
- **Bitcoin Container Entrypoint**: Completely overwrites bitcoin.conf from `BITCOIN_EXTRA_ARGS` environment variable
|
||||||
|
- **YAML Parsing Issues**: BTCPay's multiline string generation is fragile and often corrupted
|
||||||
|
- **Space Management**: Cryptocurrency daemons without pruning consume massive disk space (50-80GB each)
|
||||||
|
|
||||||
|
#### **Deployment Architecture**
|
||||||
|
- **VPS**: Hostinger Debian 13 (394GB storage, 239GB available after cleanup)
|
||||||
|
- **Docker Services**: 14 containers including Bitcoin, altcoin daemons, Tor, nginx, PostgreSQL
|
||||||
|
- **Network Security**: UFW firewall, SSH on port 2255, Fail2Ban monitoring
|
||||||
|
- **Tor Privacy**: All cryptocurrency P2P traffic routed through Tor network
|
||||||
|
- **SSL Termination**: nginx reverse proxy with Let's Encrypt certificates
|
||||||
|
|
||||||
|
## Project Status: ✅ COMPILATION ISSUES RESOLVED - SEPTEMBER 5, 2025
|
||||||
|
|
||||||
|
### 🔧 **LATEST TECHNICAL FIXES (September 5, 2025)** ✅
|
||||||
|
|
||||||
|
#### **Compilation Errors Resolved** ✅
|
||||||
|
- **CryptoCurrency Enum**: Restored all supported cryptocurrencies (XMR, USDT, ETH, ZEC, DOGE)
|
||||||
|
- **BotSimulator Fix**: Fixed string-to-int conversion error in payment creation
|
||||||
|
- **Security Update**: Updated SixLabors.ImageSharp to v3.1.8 (vulnerability fix)
|
||||||
|
- **Test Infrastructure**: Installed Playwright browsers for UI testing
|
||||||
|
|
||||||
|
#### **Build Status** ✅
|
||||||
|
- **Main Project**: Builds successfully with zero compilation errors
|
||||||
|
- **All Projects**: TeleBot, LittleShop.Client, and test projects compile cleanly
|
||||||
|
- **Package Warnings**: Only minor version resolution warnings remain (non-breaking)
|
||||||
|
|
||||||
|
### 🎯 **BOT/UI BASELINE (August 28, 2025)** ✅
|
||||||
|
|
||||||
|
#### **Complete TeleBot Integration** ✅
|
||||||
|
- **Customer Orders**: Full order history and details lookup working
|
||||||
|
- **Product Browsing**: Enhanced UI with individual product bubbles
|
||||||
|
- **Admin Authentication**: Fixed role-based authentication with proper claims
|
||||||
|
- **Bot Management**: Cleaned up development data, single active bot registration
|
||||||
|
- **Navigation Flow**: Improved UX with consistent back/menu navigation
|
||||||
|
- **Message Formatting**: Clean section headers without emojis, professional layout
|
||||||
|
|
||||||
|
#### **Technical Fixes Applied**
|
||||||
|
- **Customer Order Endpoints**: Added `/api/orders/by-customer/{customerId}/{id}` for secure customer access
|
||||||
|
- **Admin Role Claims**: Fixed missing "Admin" role claim in cookie authentication
|
||||||
|
- **AccessDenied View**: Created missing view to prevent 500 errors on unauthorized access
|
||||||
|
- **Bot Cleanup**: Removed 16 duplicate development bot registrations, kept 1 active
|
||||||
|
- **Product Bubble UI**: Individual product messages with Quick Buy/Details buttons
|
||||||
|
- **Navigation Enhancement**: Streamlined navigation with proper menu flow
|
||||||
|
|
||||||
|
### Completed Implementation (August 20, 2025)
|
||||||
|
|
||||||
|
#### 🏗️ **Architecture**
|
||||||
- **Framework**: ASP.NET Core 9.0 Web API + MVC
|
- **Framework**: ASP.NET Core 9.0 Web API + MVC
|
||||||
- **Database**: SQLite with Entity Framework Core
|
- **Database**: SQLite with Entity Framework Core
|
||||||
- **Authentication**: Dual-mode (Cookie for Admin Panel + JWT for API)
|
- **Authentication**: Dual-mode (Cookie for Admin Panel + JWT for API)
|
||||||
- **Structure**: Clean separation between Admin Panel (MVC) and Client API (Web API)
|
- **Structure**: Clean separation between Admin Panel (MVC) and Client API (Web API)
|
||||||
|
|
||||||
## Database Schema
|
#### 🗄️ **Database Schema** ✅
|
||||||
|
- **Tables**: Users, Categories, Products, ProductPhotos, Orders, OrderItems, CryptoPayments
|
||||||
|
- **Relationships**: Proper foreign keys and indexes
|
||||||
|
- **Enums**: ProductWeightUnit, OrderStatus, CryptoCurrency, PaymentStatus
|
||||||
|
- **Default Data**: Admin user (admin/admin) auto-seeded
|
||||||
|
|
||||||
**Core Tables:**
|
#### 🔐 **Authentication System** ✅
|
||||||
- Users (Staff authentication)
|
- **Admin Panel**: Cookie-based authentication for staff users
|
||||||
- Categories
|
- **Client API**: JWT authentication ready for client applications
|
||||||
- Products
|
- **Security**: PBKDF2 password hashing, proper claims-based authorization
|
||||||
- ProductPhotos
|
- **Users**: Staff-only user management (no customer accounts stored)
|
||||||
- ProductVariations (quantity-based pricing)
|
|
||||||
- Orders
|
|
||||||
- OrderItems
|
|
||||||
- CryptoPayments
|
|
||||||
|
|
||||||
**Key Features:**
|
#### 🛒 **Admin Panel (MVC)** ✅
|
||||||
- Proper foreign key relationships
|
- **Dashboard**: Overview with statistics and quick actions
|
||||||
- Product variations (e.g., 1 for £10, 2 for £19, 3 for £25)
|
- **Categories**: Full CRUD operations working
|
||||||
- Order workflow tracking with user accountability
|
- **Products**: Full CRUD operations working with photo upload support
|
||||||
- Soft delete support (IsActive flag)
|
- **Users**: Staff user management working
|
||||||
|
- **Orders**: Order management and status tracking
|
||||||
|
- **Views**: Bootstrap-based responsive UI with proper form binding
|
||||||
|
|
||||||
## Features
|
#### 🔌 **Client API (Web API)** ✅
|
||||||
|
- **Catalog Endpoints**:
|
||||||
|
- `GET /api/catalog/categories` - Public category listing
|
||||||
|
- `GET /api/catalog/products` - Public product listing
|
||||||
|
- **Order Management**:
|
||||||
|
- `POST /api/orders` - Create orders by identity reference
|
||||||
|
- `GET /api/orders/by-identity/{id}` - Get client orders
|
||||||
|
- `POST /api/orders/{id}/payments` - Create crypto payments
|
||||||
|
- `POST /api/orders/payments/webhook` - BTCPay Server webhooks
|
||||||
|
|
||||||
### Admin Panel (MVC)
|
#### 💰 **Multi-Cryptocurrency Support** ✅
|
||||||
- Dashboard with statistics
|
- **Supported Currencies**: BTC, XMR (Monero), USDT, LTC, ETH, ZEC (Zcash), DASH, DOGE
|
||||||
- Category management (CRUD)
|
- **BTCPay Server Integration**: Complete client implementation with webhook processing
|
||||||
- Product management with photo uploads
|
- **Privacy Design**: No customer personal data stored, identity reference only
|
||||||
- Product variations management
|
- **Payment Workflow**: Order → Payment generation → Blockchain monitoring → Status updates
|
||||||
- Order workflow management
|
|
||||||
- User management (staff only)
|
|
||||||
- Mobile-responsive design
|
|
||||||
|
|
||||||
### Client API (Web API)
|
#### 📦 **Features Implemented**
|
||||||
- Public product catalog
|
- **Product Management**: Name, description, weight/units, pricing, categories, photos
|
||||||
- Order creation and management
|
- **Order Workflow**: Creation → Payment → Processing → Shipping → Tracking
|
||||||
- Customer order lookup
|
- **File Upload**: Product photo management with alt text support
|
||||||
- Payment processing integration
|
- **Validation**: FluentValidation for input validation, server-side model validation
|
||||||
- Swagger documentation
|
- **Logging**: Comprehensive Serilog logging to console and files
|
||||||
|
- **Documentation**: Swagger API documentation with JWT authentication
|
||||||
|
|
||||||
### Payment System
|
### 🔧 **Technical Lessons Learned**
|
||||||
- Multi-cryptocurrency support (BTC, XMR, USDT, LTC, ETH, ZEC, DASH, DOGE)
|
|
||||||
- BTCPay Server integration
|
|
||||||
- Privacy-focused (no customer personal data stored)
|
|
||||||
- Webhook processing for payment status updates
|
|
||||||
|
|
||||||
### TeleBot Integration
|
#### **ASP.NET Core 9.0 Specifics**
|
||||||
- Product browsing with individual product bubbles
|
1. **Model Binding Issues**: Views need explicit model instances (`new CreateDto()`) for proper binding
|
||||||
- Customer order history and tracking
|
2. **Form Binding**: Using explicit `name` attributes more reliable than `asp-for` helpers in some cases
|
||||||
- Quick buy functionality
|
3. **Area Routing**: Requires proper route configuration and area attribute on controllers
|
||||||
- Professional message formatting
|
4. **View Engine**: Runtime changes to views require application restart in Production mode
|
||||||
|
|
||||||
## Default Credentials
|
#### **Entity Framework Core**
|
||||||
|
1. **SQLite Works Well**: Handles all complex relationships and transactions properly
|
||||||
|
2. **Query Splitting Warning**: Multi-include queries generate warnings but work correctly
|
||||||
|
3. **Migrations**: `EnsureCreated()` sufficient for development, migrations better for production
|
||||||
|
4. **Decimal Precision**: Proper `decimal(18,2)` and `decimal(18,8)` column types for currency
|
||||||
|
|
||||||
**Admin Account:**
|
#### **Authentication Architecture**
|
||||||
- Username: `admin`
|
1. **Dual Auth Schemes**: Successfully implemented both Cookie (MVC) and JWT (API) authentication
|
||||||
- Password: `admin`
|
2. **Claims-Based Security**: Works well for role-based authorization policies
|
||||||
|
3. **Password Security**: PBKDF2 with 100,000 iterations provides good security
|
||||||
|
4. **Session Management**: Cookie authentication handles admin panel sessions properly
|
||||||
|
|
||||||
## File Structure
|
#### **BTCPay Server Integration**
|
||||||
|
1. **Version Compatibility**: BTCPay Server Client v2.0 has different API than v1.x
|
||||||
|
2. **Package Dependencies**: NBitcoin version conflicts require careful package management
|
||||||
|
3. **Privacy Focus**: Self-hosted approach eliminates third-party data sharing
|
||||||
|
4. **Webhook Processing**: Proper async handling for payment status updates
|
||||||
|
|
||||||
|
#### **Development Challenges Solved**
|
||||||
|
1. **WSL Environment**: Required CMD.exe for .NET commands, file locking issues with hot reload
|
||||||
|
2. **View Compilation**: Views require app restart in Production mode to pick up changes
|
||||||
|
3. **Form Validation**: Empty validation summaries appear due to ModelState checking
|
||||||
|
4. **Static Files**: Proper configuration needed for product photo serving
|
||||||
|
|
||||||
|
### 🚀 **Current System Status**
|
||||||
|
|
||||||
|
#### **✅ Fully Working**
|
||||||
|
- Admin Panel authentication (admin/admin) with proper role claims
|
||||||
|
- Category management (Create, Read, Update, Delete)
|
||||||
|
- Product management (Create, Read, Update, Delete)
|
||||||
|
- User management for staff accounts
|
||||||
|
- Public API endpoints for client integration
|
||||||
|
- Database persistence and relationships
|
||||||
|
- Multi-cryptocurrency payment framework
|
||||||
|
- **TeleBot Integration**: Complete customer order system
|
||||||
|
- **Product Bubble UI**: Enhanced product browsing experience
|
||||||
|
- **Bot Management**: Clean single bot registration
|
||||||
|
- **Customer Orders**: Full order history and details access
|
||||||
|
- **Navigation Flow**: Improved UX with consistent menu navigation
|
||||||
|
|
||||||
|
#### **🔮 Ready for Tomorrow**
|
||||||
|
- Order creation and payment testing via TeleBot
|
||||||
|
- Multi-crypto payment workflow end-to-end test
|
||||||
|
- Royal Mail shipping integration
|
||||||
|
- Production deployment considerations
|
||||||
|
- Advanced bot features and automation
|
||||||
|
|
||||||
|
### 📁 **File Structure Created**
|
||||||
```
|
```
|
||||||
LittleShop/
|
LittleShop/
|
||||||
├── Controllers/ (Client API)
|
├── Controllers/ (Client API)
|
||||||
├── Areas/Admin/ (Admin Panel MVC)
|
│ ├── CatalogController.cs
|
||||||
|
│ ├── OrdersController.cs
|
||||||
|
│ ├── HomeController.cs
|
||||||
|
│ └── TestController.cs
|
||||||
|
├── Areas/Admin/ (Admin Panel)
|
||||||
│ ├── Controllers/
|
│ ├── Controllers/
|
||||||
│ └── Views/
|
│ │ ├── AccountController.cs
|
||||||
|
│ │ ├── DashboardController.cs
|
||||||
|
│ │ ├── CategoriesController.cs
|
||||||
|
│ │ ├── ProductsController.cs
|
||||||
|
│ │ ├── OrdersController.cs
|
||||||
|
│ │ └── UsersController.cs
|
||||||
|
│ └── Views/ (Bootstrap UI)
|
||||||
├── Services/ (Business Logic)
|
├── Services/ (Business Logic)
|
||||||
├── Models/ (Database Entities)
|
├── Models/ (Database Entities)
|
||||||
├── DTOs/ (Data Transfer Objects)
|
├── DTOs/ (Data Transfer Objects)
|
||||||
@ -81,27 +360,99 @@ LittleShop/
|
|||||||
└── wwwroot/uploads/ (File Storage)
|
└── wwwroot/uploads/ (File Storage)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Technical Notes
|
### 🎯 **Performance Notes**
|
||||||
|
- **Database**: SQLite performs well for development, 106KB with sample data
|
||||||
|
- **Startup Time**: ~2 seconds with database initialization
|
||||||
|
- **Memory Usage**: Efficient with proper service scoping
|
||||||
|
- **Query Performance**: EF Core generates optimal SQLite queries
|
||||||
|
|
||||||
### ASP.NET Core 9.0
|
### 🔒 **Security Implementation**
|
||||||
- Views need explicit model instances for proper binding
|
- **No KYC Requirements**: Privacy-focused design
|
||||||
- Area routing requires proper route configuration
|
- **Minimal Data Collection**: Only identity reference stored for customers
|
||||||
- Both Cookie (Admin) and JWT (API) authentication schemes
|
- **Self-Hosted Payments**: BTCPay Server eliminates third-party payment processors
|
||||||
|
- **Encrypted Storage**: Passwords properly hashed with salt
|
||||||
|
- **CORS Configuration**: Prepared for web client integration
|
||||||
|
|
||||||
### Entity Framework Core
|
## 🚀 **PRODUCT VARIATIONS & MOBILE WORKFLOW - SEPTEMBER 18, 2025** 🚀
|
||||||
- SQLite handles complex relationships efficiently
|
|
||||||
- Database initialization via `EnsureCreated()` for development
|
|
||||||
- Proper decimal precision for currency values
|
|
||||||
|
|
||||||
### Security
|
**Complete product variations system with mobile-responsive order workflow implemented!**
|
||||||
- PBKDF2 password hashing (100,000 iterations)
|
|
||||||
- Claims-based authorization
|
|
||||||
- No customer PII storage (privacy-focused)
|
|
||||||
- CORS configuration ready
|
|
||||||
|
|
||||||
## Development Environment
|
### **Key Achievements:**
|
||||||
|
- ✅ Product variations system (1 for £10, 2 for £19, 3 for £25)
|
||||||
|
- ✅ Enhanced order workflow (Accept → Packing → Dispatched → Delivered)
|
||||||
|
- ✅ Mobile-responsive interface (tables on desktop, cards on mobile)
|
||||||
|
- ✅ CSV import/export system for bulk product management
|
||||||
|
- ✅ Self-contained deployment (no external CDN dependencies)
|
||||||
|
- ✅ Enhanced dashboard with variations metrics
|
||||||
|
|
||||||
- **Platform**: Windows/WSL
|
### **Critical Technical Improvements:**
|
||||||
- **Command Shell**: cmd.exe recommended for .NET commands
|
|
||||||
- **Database**: SQLite (file-based, no server required)
|
#### **Product Variations Architecture** ✅
|
||||||
- **Hot Reload**: Views require app restart in Production mode
|
- **ProductVariation Model**: Quantity-based pricing with automatic price-per-unit calculation
|
||||||
|
- **Database Schema**: Proper relationships with UNIQUE constraints on ProductId+Quantity
|
||||||
|
- **Order Integration**: OrderItems support ProductVariationId for variation pricing
|
||||||
|
- **API Support**: Full REST endpoints for variation management
|
||||||
|
- **Admin Interface**: Complete CRUD with duplicate detection and user guidance
|
||||||
|
|
||||||
|
#### **Enhanced Order Workflow** ✅
|
||||||
|
- **Status Flow**: PendingPayment → PaymentReceived → Accepted → Packing → Dispatched → Delivered
|
||||||
|
- **User Tracking**: AcceptedByUser, PackedByUser, DispatchedByUser for accountability
|
||||||
|
- **Timeline Tracking**: AcceptedAt, PackingStartedAt, DispatchedAt timestamps
|
||||||
|
- **Smart Delivery Calculation**: Auto-calculates delivery dates (working days, skips weekends)
|
||||||
|
- **On Hold Workflow**: Side workflow for problem resolution with reason tracking
|
||||||
|
- **Tab-Based Interface**: Workflow-focused UI with badge counts for urgent items
|
||||||
|
|
||||||
|
#### **Mobile-First Design** ✅
|
||||||
|
- **Responsive Breakpoints**: `d-none d-lg-block` (desktop table) / `d-lg-none` (mobile cards)
|
||||||
|
- **Touch-Friendly UI**: Large buttons, card layouts, horizontal scrolling tabs
|
||||||
|
- **Adaptive Content**: Smart text switching (`Accept Orders` vs `Accept` on mobile)
|
||||||
|
- **Visual Status**: Color-coded borders and badges for at-a-glance status recognition
|
||||||
|
|
||||||
|
#### **Bulk Import System** ✅
|
||||||
|
- **CSV Format**: Supports products + variations in single file
|
||||||
|
- **Variations Import**: "Single Item:1:10.00;Twin Pack:2:19.00;Triple Pack:3:25.00" format
|
||||||
|
- **Category Resolution**: Uses category names instead of GUIDs
|
||||||
|
- **Error Reporting**: Detailed import results with row-level error reporting
|
||||||
|
- **Template System**: Download ready-to-use CSV templates
|
||||||
|
|
||||||
|
#### **Form Binding Resolution** ✅
|
||||||
|
- **Fixed ASP.NET Core Issue**: Changed from `asp-for` to explicit `name` attributes
|
||||||
|
- **Validation Enhancement**: Proper ModelState error display with Bootstrap styling
|
||||||
|
- **Cache Busting**: Added no-cache headers to ensure updated forms load
|
||||||
|
- **Debug Logging**: Console output for troubleshooting form submissions
|
||||||
|
|
||||||
|
### **Production Deployment Readiness** ✅
|
||||||
|
- **Self-Contained**: All external CDN dependencies replaced with local libraries
|
||||||
|
- **Isolated Networks**: Ready for air-gapped/restricted environments
|
||||||
|
- **Mobile Optimized**: End users can efficiently manage orders on mobile devices
|
||||||
|
- **Bulk Management**: CSV import/export for efficient product catalog management
|
||||||
|
|
||||||
|
## 🎉 **SYSTEM NOW PRODUCTION-READY** 🎉
|
||||||
|
|
||||||
|
**Complete e-commerce system with advanced features ready for mobile-first operations!** 🌟
|
||||||
|
|
||||||
|
## 🧪 **Testing Status (September 5, 2025)**
|
||||||
|
|
||||||
|
### **Current Test Results**
|
||||||
|
- **Build Status**: ✅ All projects compile successfully
|
||||||
|
- **Unit Tests**: ⚠️ 24/41 passing (59% pass rate)
|
||||||
|
- **Integration Tests**: ⚠️ Multiple service registration issues
|
||||||
|
- **UI Tests**: ✅ Playwright browsers installed and ready
|
||||||
|
|
||||||
|
### **Known Test Issues**
|
||||||
|
- **Push Notification Tests**: Service mocking configuration needs adjustment
|
||||||
|
- **Service Tests**: Some expect hard deletes but services use soft deletes (IsActive = false)
|
||||||
|
- **Integration Tests**: Test service registration doesn't match production services
|
||||||
|
- **Authentication Tests**: JWT vs Cookie authentication scheme mismatches
|
||||||
|
|
||||||
|
### **Test Maintenance Recommendations**
|
||||||
|
1. **Service Registration**: Update TestWebApplicationFactory to register all required services
|
||||||
|
2. **Test Expectations**: Align test expectations with actual service behavior (soft vs hard deletes)
|
||||||
|
3. **Authentication Setup**: Standardize test authentication configuration
|
||||||
|
4. **Mock Configuration**: Review and fix service mocking in unit tests
|
||||||
|
5. **Data Seeding**: Ensure consistent test data setup across test categories
|
||||||
|
|
||||||
|
### **Production Impact**
|
||||||
|
- ✅ **Zero Impact**: All compilation issues resolved, application runs successfully
|
||||||
|
- ✅ **Core Functionality**: All main features work as expected in production
|
||||||
|
- ⚠️ **Test Coverage**: Tests need maintenance but don't affect runtime operation
|
||||||
207
DEPLOY.md
207
DEPLOY.md
@ -1,207 +0,0 @@
|
|||||||
# TeleShop & TeleBot Docker Deployment Guide
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Docker installed on your local machine or build server
|
|
||||||
- Network access to the Docker registry at `10.8.0.1:5000`
|
|
||||||
- Git repository cloned locally
|
|
||||||
|
|
||||||
## Quick Deployment
|
|
||||||
|
|
||||||
### Option 1: Using the Deployment Script (Recommended)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Make script executable (if not already)
|
|
||||||
chmod +x deploy-to-registry.sh
|
|
||||||
|
|
||||||
# Run the deployment
|
|
||||||
./deploy-to-registry.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The script will:
|
|
||||||
1. ✅ Test registry connectivity
|
|
||||||
2. 📦 Build both TeleShop and TeleBot images
|
|
||||||
3. 🏷️ Tag images with `latest` and `clean-slate` tags
|
|
||||||
4. 🚀 Push all images to the registry
|
|
||||||
|
|
||||||
### Option 2: Manual Steps
|
|
||||||
|
|
||||||
If you prefer to run commands manually:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Build TeleShop image
|
|
||||||
docker build -f Dockerfile -t teleshop:latest .
|
|
||||||
|
|
||||||
# 2. Build TeleBot image
|
|
||||||
docker build -f Dockerfile.telebot -t telebot:latest .
|
|
||||||
|
|
||||||
# 3. Tag images for registry
|
|
||||||
docker tag teleshop:latest 10.8.0.1:5000/teleshop:latest
|
|
||||||
docker tag teleshop:latest 10.8.0.1:5000/teleshop:clean-slate
|
|
||||||
docker tag telebot:latest 10.8.0.1:5000/telebot:latest
|
|
||||||
docker tag telebot:latest 10.8.0.1:5000/telebot:clean-slate
|
|
||||||
|
|
||||||
# 4. Push to registry
|
|
||||||
docker push 10.8.0.1:5000/teleshop:latest
|
|
||||||
docker push 10.8.0.1:5000/teleshop:clean-slate
|
|
||||||
docker push 10.8.0.1:5000/telebot:latest
|
|
||||||
docker push 10.8.0.1:5000/telebot:clean-slate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Registry Configuration
|
|
||||||
|
|
||||||
### If Using HTTP (Insecure) Registry
|
|
||||||
|
|
||||||
The registry at `10.8.0.1:5000` is HTTP (not HTTPS). You need to configure Docker to allow insecure registries:
|
|
||||||
|
|
||||||
#### On Linux
|
|
||||||
Edit `/etc/docker/daemon.json`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"insecure-registries": ["10.8.0.1:5000"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Restart Docker:
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart docker
|
|
||||||
```
|
|
||||||
|
|
||||||
#### On Windows (Docker Desktop)
|
|
||||||
1. Open Docker Desktop
|
|
||||||
2. Go to Settings → Docker Engine
|
|
||||||
3. Add to the JSON configuration:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"insecure-registries": ["10.8.0.1:5000"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
4. Click "Apply & Restart"
|
|
||||||
|
|
||||||
#### On macOS (Docker Desktop)
|
|
||||||
1. Open Docker Desktop
|
|
||||||
2. Go to Preferences → Docker Engine
|
|
||||||
3. Add to the JSON configuration:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"insecure-registries": ["10.8.0.1:5000"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
4. Click "Apply & Restart"
|
|
||||||
|
|
||||||
## Verify Deployment
|
|
||||||
|
|
||||||
After pushing, verify the images are in the registry:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List all repositories
|
|
||||||
curl http://10.8.0.1:5000/v2/_catalog
|
|
||||||
|
|
||||||
# Expected output:
|
|
||||||
# {"repositories":["telebot","teleshop"]}
|
|
||||||
|
|
||||||
# List tags for TeleShop
|
|
||||||
curl http://10.8.0.1:5000/v2/teleshop/tags/list
|
|
||||||
|
|
||||||
# List tags for TeleBot
|
|
||||||
curl http://10.8.0.1:5000/v2/telebot/tags/list
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pulling Images
|
|
||||||
|
|
||||||
On your deployment server, pull the images:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pull TeleShop
|
|
||||||
docker pull 10.8.0.1:5000/teleshop:latest
|
|
||||||
# or specific version
|
|
||||||
docker pull 10.8.0.1:5000/teleshop:clean-slate
|
|
||||||
|
|
||||||
# Pull TeleBot
|
|
||||||
docker pull 10.8.0.1:5000/telebot:latest
|
|
||||||
# or specific version
|
|
||||||
docker pull 10.8.0.1:5000/telebot:clean-slate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using with docker-compose
|
|
||||||
|
|
||||||
Update your `docker-compose.yml` to use the registry images:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
littleshop:
|
|
||||||
image: 10.8.0.1:5000/teleshop:latest
|
|
||||||
# ... rest of configuration
|
|
||||||
|
|
||||||
telebot:
|
|
||||||
image: 10.8.0.1:5000/telebot:latest
|
|
||||||
# ... rest of configuration
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Cannot connect to registry
|
|
||||||
```bash
|
|
||||||
# Test connectivity
|
|
||||||
curl http://10.8.0.1:5000/v2/_catalog
|
|
||||||
|
|
||||||
# If this fails, check:
|
|
||||||
# - Network connectivity to 10.8.0.1
|
|
||||||
# - Registry service is running
|
|
||||||
# - Port 5000 is accessible (check firewall)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Permission denied when pushing
|
|
||||||
```bash
|
|
||||||
# Registry may require authentication. Contact your registry administrator.
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTTP request forbidden
|
|
||||||
```bash
|
|
||||||
# You need to configure the insecure registry setting (see above)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Image Details
|
|
||||||
|
|
||||||
### TeleShop Image
|
|
||||||
- **Base Image**: mcr.microsoft.com/dotnet/aspnet:9.0
|
|
||||||
- **Exposed Port**: 8080
|
|
||||||
- **Database**: SQLite at `/app/data/teleshop-prod.db`
|
|
||||||
- **Volumes**:
|
|
||||||
- `/app/data` - Database storage
|
|
||||||
- `/app/wwwroot/uploads` - Product images
|
|
||||||
- `/app/logs` - Application logs
|
|
||||||
|
|
||||||
### TeleBot Image
|
|
||||||
- **Base Image**: mcr.microsoft.com/dotnet/aspnet:9.0
|
|
||||||
- **Dependencies**: Connects to TeleShop API
|
|
||||||
- **Volumes**:
|
|
||||||
- `/app/logs` - Application logs
|
|
||||||
- `/app/data` - Bot data
|
|
||||||
- `/app/image_cache` - Cached product images
|
|
||||||
|
|
||||||
## Version Tags
|
|
||||||
|
|
||||||
- `latest` - Always points to the most recent build
|
|
||||||
- `clean-slate` - Baseline version with migrations, no sample data (current clean state)
|
|
||||||
|
|
||||||
## CI/CD Integration
|
|
||||||
|
|
||||||
You can integrate this deployment script into your CI/CD pipeline:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# GitLab CI example
|
|
||||||
deploy:
|
|
||||||
stage: deploy
|
|
||||||
script:
|
|
||||||
- ./deploy-to-registry.sh
|
|
||||||
only:
|
|
||||||
- main
|
|
||||||
- tags
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# GitHub Actions example
|
|
||||||
- name: Deploy to Registry
|
|
||||||
run: ./deploy-to-registry.sh
|
|
||||||
```
|
|
||||||
@ -74,7 +74,7 @@ ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=0 \
|
|||||||
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true \
|
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true \
|
||||||
ASPNETCORE_URLS=http://+:8080 \
|
ASPNETCORE_URLS=http://+:8080 \
|
||||||
ASPNETCORE_ENVIRONMENT=Production \
|
ASPNETCORE_ENVIRONMENT=Production \
|
||||||
ConnectionStrings__DefaultConnection="Data Source=/app/data/teleshop-prod.db;Cache=Shared" \
|
ConnectionStrings__DefaultConnection="Data Source=/app/data/littleshop-prod.db;Cache=Shared" \
|
||||||
SilverPay__BaseUrl="http://31.97.57.205:8001" \
|
SilverPay__BaseUrl="http://31.97.57.205:8001" \
|
||||||
SilverPay__ApiKey="your-api-key-here" \
|
SilverPay__ApiKey="your-api-key-here" \
|
||||||
TMPDIR=/tmp
|
TMPDIR=/tmp
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -9,7 +8,6 @@ using Microsoft.Extensions.Logging;
|
|||||||
using LittleShop.DTOs;
|
using LittleShop.DTOs;
|
||||||
using LittleShop.Enums;
|
using LittleShop.Enums;
|
||||||
using LittleShop.Services;
|
using LittleShop.Services;
|
||||||
using LittleShop.Models;
|
|
||||||
|
|
||||||
namespace LittleShop.Areas.Admin.Controllers;
|
namespace LittleShop.Areas.Admin.Controllers;
|
||||||
|
|
||||||
@ -20,20 +18,17 @@ public class BotsController : Controller
|
|||||||
private readonly IBotService _botService;
|
private readonly IBotService _botService;
|
||||||
private readonly IBotMetricsService _metricsService;
|
private readonly IBotMetricsService _metricsService;
|
||||||
private readonly ITelegramBotManagerService _telegramManager;
|
private readonly ITelegramBotManagerService _telegramManager;
|
||||||
private readonly IBotDiscoveryService _discoveryService;
|
|
||||||
private readonly ILogger<BotsController> _logger;
|
private readonly ILogger<BotsController> _logger;
|
||||||
|
|
||||||
public BotsController(
|
public BotsController(
|
||||||
IBotService botService,
|
IBotService botService,
|
||||||
IBotMetricsService metricsService,
|
IBotMetricsService metricsService,
|
||||||
ITelegramBotManagerService telegramManager,
|
ITelegramBotManagerService telegramManager,
|
||||||
IBotDiscoveryService discoveryService,
|
|
||||||
ILogger<BotsController> logger)
|
ILogger<BotsController> logger)
|
||||||
{
|
{
|
||||||
_botService = botService;
|
_botService = botService;
|
||||||
_metricsService = metricsService;
|
_metricsService = metricsService;
|
||||||
_telegramManager = telegramManager;
|
_telegramManager = telegramManager;
|
||||||
_discoveryService = discoveryService;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,397 +379,4 @@ public class BotsController : Controller
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Remote Bot Discovery
|
|
||||||
|
|
||||||
// GET: Admin/Bots/DiscoverRemote
|
|
||||||
public IActionResult DiscoverRemote()
|
|
||||||
{
|
|
||||||
return View(new DiscoveryWizardViewModel());
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST: Admin/Bots/ProbeRemote
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> ProbeRemote(DiscoveryWizardViewModel model)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Probing remote TeleBot at {IpAddress}:{Port}", model.IpAddress, model.Port);
|
|
||||||
|
|
||||||
var result = await _discoveryService.ProbeRemoteBotAsync(model.IpAddress, model.Port);
|
|
||||||
|
|
||||||
if (result.Success && result.ProbeResponse != null)
|
|
||||||
{
|
|
||||||
model.ProbeResponse = result.ProbeResponse;
|
|
||||||
model.BotName = result.ProbeResponse.Name;
|
|
||||||
model.CurrentStep = 2;
|
|
||||||
model.SuccessMessage = "TeleBot discovered successfully!";
|
|
||||||
|
|
||||||
// Auto-select a personality if not already configured
|
|
||||||
if (string.IsNullOrEmpty(model.PersonalityName))
|
|
||||||
{
|
|
||||||
var personalities = new[] { "Alan", "Dave", "Sarah", "Mike", "Emma", "Tom" };
|
|
||||||
model.PersonalityName = personalities[new Random().Next(personalities.Length)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
model.ErrorMessage = result.Message;
|
|
||||||
model.CurrentStep = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return View("DiscoverRemote", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST: Admin/Bots/RegisterRemote
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> RegisterRemote(DiscoveryWizardViewModel model)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Registering remote bot: {BotName} at {IpAddress}:{Port}",
|
|
||||||
model.BotName, model.IpAddress, model.Port);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(model.BotName))
|
|
||||||
{
|
|
||||||
model.ErrorMessage = "Bot name is required";
|
|
||||||
model.CurrentStep = 2;
|
|
||||||
return View("DiscoverRemote", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Create the bot in the database
|
|
||||||
var registrationDto = new BotRegistrationDto
|
|
||||||
{
|
|
||||||
Name = model.BotName,
|
|
||||||
Description = model.Description,
|
|
||||||
Type = BotType.Telegram,
|
|
||||||
Version = model.ProbeResponse?.Version ?? "1.0.0",
|
|
||||||
PersonalityName = model.PersonalityName,
|
|
||||||
InitialSettings = new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
["discovery"] = new { remoteAddress = model.IpAddress, remotePort = model.Port }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var botResult = await _botService.RegisterBotAsync(registrationDto);
|
|
||||||
|
|
||||||
// Update the bot with remote discovery info
|
|
||||||
var bot = await _botService.GetBotByIdAsync(botResult.BotId);
|
|
||||||
if (bot != null)
|
|
||||||
{
|
|
||||||
// Update remote fields directly (we'll need to add this method to IBotService)
|
|
||||||
await UpdateBotRemoteInfoAsync(botResult.BotId, model.IpAddress, model.Port,
|
|
||||||
model.ProbeResponse?.InstanceId, DiscoveryStatus.Discovered);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the remote TeleBot with the BotKey
|
|
||||||
var initResult = await _discoveryService.InitializeRemoteBotAsync(botResult.BotId, model.IpAddress, model.Port);
|
|
||||||
|
|
||||||
if (initResult.Success)
|
|
||||||
{
|
|
||||||
// Update status to Initialized
|
|
||||||
await UpdateBotRemoteInfoAsync(botResult.BotId, model.IpAddress, model.Port,
|
|
||||||
model.ProbeResponse?.InstanceId, DiscoveryStatus.Initialized);
|
|
||||||
|
|
||||||
model.BotId = botResult.BotId;
|
|
||||||
model.BotKey = botResult.BotKey;
|
|
||||||
model.CurrentStep = 3;
|
|
||||||
model.SuccessMessage = "Bot registered and initialized! Now enter the Telegram bot token.";
|
|
||||||
|
|
||||||
_logger.LogInformation("Remote bot registered and initialized: {BotId}", botResult.BotId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
model.ErrorMessage = $"Bot registered but initialization failed: {initResult.Message}";
|
|
||||||
model.BotId = botResult.BotId;
|
|
||||||
model.BotKey = botResult.BotKey;
|
|
||||||
model.CurrentStep = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
return View("DiscoverRemote", model);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to register remote bot");
|
|
||||||
model.ErrorMessage = $"Registration failed: {ex.Message}";
|
|
||||||
model.CurrentStep = 2;
|
|
||||||
return View("DiscoverRemote", model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST: Admin/Bots/ConfigureRemote
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> ConfigureRemote(DiscoveryWizardViewModel model)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Configuring remote bot {BotId} with Telegram token", model.BotId);
|
|
||||||
|
|
||||||
if (!model.BotId.HasValue)
|
|
||||||
{
|
|
||||||
model.ErrorMessage = "Bot ID is missing";
|
|
||||||
model.CurrentStep = 1;
|
|
||||||
return View("DiscoverRemote", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(model.BotToken))
|
|
||||||
{
|
|
||||||
model.ErrorMessage = "Telegram bot token is required";
|
|
||||||
model.CurrentStep = 3;
|
|
||||||
return View("DiscoverRemote", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the token first
|
|
||||||
if (!await ValidateTelegramToken(model.BotToken))
|
|
||||||
{
|
|
||||||
model.ErrorMessage = "Invalid Telegram bot token. Please check and try again.";
|
|
||||||
model.CurrentStep = 3;
|
|
||||||
return View("DiscoverRemote", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Push configuration to the remote TeleBot
|
|
||||||
var configResult = await _discoveryService.PushConfigurationAsync(model.BotId.Value, model.BotToken);
|
|
||||||
|
|
||||||
if (configResult.Success)
|
|
||||||
{
|
|
||||||
// Update bot settings with the token
|
|
||||||
var bot = await _botService.GetBotByIdAsync(model.BotId.Value);
|
|
||||||
if (bot != null)
|
|
||||||
{
|
|
||||||
var settings = bot.Settings ?? new Dictionary<string, object>();
|
|
||||||
settings["telegram"] = new { botToken = model.BotToken };
|
|
||||||
await _botService.UpdateBotSettingsAsync(model.BotId.Value,
|
|
||||||
new UpdateBotSettingsDto { Settings = settings });
|
|
||||||
|
|
||||||
// Update discovery status to Configured
|
|
||||||
await UpdateBotRemoteInfoAsync(model.BotId.Value,
|
|
||||||
bot.RemoteAddress ?? model.IpAddress,
|
|
||||||
bot.RemotePort ?? model.Port,
|
|
||||||
bot.RemoteInstanceId,
|
|
||||||
DiscoveryStatus.Configured);
|
|
||||||
|
|
||||||
// Activate the bot
|
|
||||||
await _botService.UpdateBotStatusAsync(model.BotId.Value, BotStatus.Active);
|
|
||||||
}
|
|
||||||
|
|
||||||
TempData["Success"] = $"Remote bot configured successfully! Telegram: @{configResult.TelegramUsername}";
|
|
||||||
return RedirectToAction(nameof(Details), new { id = model.BotId.Value });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
model.ErrorMessage = $"Configuration failed: {configResult.Message}";
|
|
||||||
model.CurrentStep = 3;
|
|
||||||
return View("DiscoverRemote", model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to configure remote bot");
|
|
||||||
model.ErrorMessage = $"Configuration failed: {ex.Message}";
|
|
||||||
model.CurrentStep = 3;
|
|
||||||
return View("DiscoverRemote", model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST: Admin/Bots/CheckRemoteStatus/5
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> CheckRemoteStatus(Guid id)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Checking remote status for bot {BotId}", id);
|
|
||||||
|
|
||||||
var bot = await _botService.GetBotByIdAsync(id);
|
|
||||||
if (bot == null)
|
|
||||||
{
|
|
||||||
TempData["Error"] = "Bot not found";
|
|
||||||
return RedirectToAction(nameof(Index));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bot.IsRemote)
|
|
||||||
{
|
|
||||||
TempData["Error"] = "This is not a remote bot";
|
|
||||||
return RedirectToAction(nameof(Details), new { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await _discoveryService.ProbeRemoteBotAsync(bot.RemoteAddress!, bot.RemotePort ?? 5000);
|
|
||||||
|
|
||||||
if (result.Success && result.ProbeResponse != null)
|
|
||||||
{
|
|
||||||
// Update discovery status
|
|
||||||
await UpdateBotRemoteInfoAsync(id, bot.RemoteAddress!, bot.RemotePort ?? 5000,
|
|
||||||
result.ProbeResponse.InstanceId, result.ProbeResponse.Status);
|
|
||||||
|
|
||||||
var statusMessage = result.ProbeResponse.IsConfigured
|
|
||||||
? $"Bot is online and configured. Telegram: @{result.ProbeResponse.TelegramUsername}"
|
|
||||||
: "Bot is online but not yet configured with a Telegram token.";
|
|
||||||
|
|
||||||
TempData["Success"] = statusMessage;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Update status to indicate offline
|
|
||||||
await UpdateBotRemoteInfoAsync(id, bot.RemoteAddress!, bot.RemotePort ?? 5000,
|
|
||||||
bot.RemoteInstanceId, DiscoveryStatus.Offline);
|
|
||||||
|
|
||||||
TempData["Error"] = $"Remote bot is not responding: {result.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to check remote status for bot {BotId}", id);
|
|
||||||
TempData["Error"] = $"Failed to check status: {ex.Message}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return RedirectToAction(nameof(Details), new { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET: Admin/Bots/RepushConfig/5
|
|
||||||
public async Task<IActionResult> RepushConfig(Guid id)
|
|
||||||
{
|
|
||||||
var bot = await _botService.GetBotByIdAsync(id);
|
|
||||||
if (bot == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
if (!bot.IsRemote)
|
|
||||||
{
|
|
||||||
TempData["Error"] = "This is not a remote bot";
|
|
||||||
return RedirectToAction(nameof(Details), new { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get existing token from settings
|
|
||||||
string? existingToken = null;
|
|
||||||
if (bot.Settings.TryGetValue("telegram", out var telegramObj))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var telegramJson = JsonSerializer.Serialize(telegramObj);
|
|
||||||
var telegramDict = JsonSerializer.Deserialize<Dictionary<string, object>>(telegramJson);
|
|
||||||
if (telegramDict?.TryGetValue("botToken", out var tokenObj) == true)
|
|
||||||
{
|
|
||||||
existingToken = tokenObj?.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewData["ExistingToken"] = existingToken;
|
|
||||||
ViewData["HasExistingToken"] = !string.IsNullOrEmpty(existingToken);
|
|
||||||
|
|
||||||
return View(bot);
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST: Admin/Bots/RepushConfig/5
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> RepushConfig(Guid id, string botToken, bool useExistingToken = false)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Re-pushing configuration to remote bot {BotId}", id);
|
|
||||||
|
|
||||||
var bot = await _botService.GetBotByIdAsync(id);
|
|
||||||
if (bot == null)
|
|
||||||
{
|
|
||||||
TempData["Error"] = "Bot not found";
|
|
||||||
return RedirectToAction(nameof(Index));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bot.IsRemote)
|
|
||||||
{
|
|
||||||
TempData["Error"] = "This is not a remote bot";
|
|
||||||
return RedirectToAction(nameof(Details), new { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
// If using existing token, retrieve it
|
|
||||||
if (useExistingToken)
|
|
||||||
{
|
|
||||||
if (bot.Settings.TryGetValue("telegram", out var telegramObj))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var telegramJson = JsonSerializer.Serialize(telegramObj);
|
|
||||||
var telegramDict = JsonSerializer.Deserialize<Dictionary<string, object>>(telegramJson);
|
|
||||||
if (telegramDict?.TryGetValue("botToken", out var tokenObj) == true)
|
|
||||||
{
|
|
||||||
botToken = tokenObj?.ToString() ?? string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(botToken))
|
|
||||||
{
|
|
||||||
TempData["Error"] = "Bot token is required";
|
|
||||||
return RedirectToAction(nameof(RepushConfig), new { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the token
|
|
||||||
if (!await ValidateTelegramToken(botToken))
|
|
||||||
{
|
|
||||||
TempData["Error"] = "Invalid Telegram bot token";
|
|
||||||
return RedirectToAction(nameof(RepushConfig), new { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// First, re-initialize if needed
|
|
||||||
var probeResult = await _discoveryService.ProbeRemoteBotAsync(bot.RemoteAddress!, bot.RemotePort ?? 5000);
|
|
||||||
|
|
||||||
if (!probeResult.Success)
|
|
||||||
{
|
|
||||||
TempData["Error"] = $"Cannot reach remote bot: {probeResult.Message}";
|
|
||||||
return RedirectToAction(nameof(Details), new { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
// If bot is not initialized, initialize first
|
|
||||||
if (probeResult.ProbeResponse?.Status == DiscoveryStatus.AwaitingDiscovery ||
|
|
||||||
probeResult.ProbeResponse?.Status == DiscoveryStatus.Discovered)
|
|
||||||
{
|
|
||||||
var initResult = await _discoveryService.InitializeRemoteBotAsync(id, bot.RemoteAddress!, bot.RemotePort ?? 5000);
|
|
||||||
if (!initResult.Success)
|
|
||||||
{
|
|
||||||
TempData["Error"] = $"Failed to initialize remote bot: {initResult.Message}";
|
|
||||||
return RedirectToAction(nameof(Details), new { id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the configuration
|
|
||||||
var configResult = await _discoveryService.PushConfigurationAsync(id, botToken);
|
|
||||||
|
|
||||||
if (configResult.Success)
|
|
||||||
{
|
|
||||||
// Update bot settings with the token
|
|
||||||
var settings = bot.Settings ?? new Dictionary<string, object>();
|
|
||||||
settings["telegram"] = new { botToken = botToken };
|
|
||||||
await _botService.UpdateBotSettingsAsync(id, new UpdateBotSettingsDto { Settings = settings });
|
|
||||||
|
|
||||||
// Update discovery status
|
|
||||||
await UpdateBotRemoteInfoAsync(id, bot.RemoteAddress!, bot.RemotePort ?? 5000,
|
|
||||||
probeResult.ProbeResponse?.InstanceId, DiscoveryStatus.Configured);
|
|
||||||
|
|
||||||
TempData["Success"] = $"Configuration pushed successfully! Telegram: @{configResult.TelegramUsername}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TempData["Error"] = $"Failed to push configuration: {configResult.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to re-push configuration to bot {BotId}", id);
|
|
||||||
TempData["Error"] = $"Failed to push configuration: {ex.Message}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return RedirectToAction(nameof(Details), new { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper method to update bot remote info
|
|
||||||
private async Task UpdateBotRemoteInfoAsync(Guid botId, string ipAddress, int port, string? instanceId, string status)
|
|
||||||
{
|
|
||||||
await _botService.UpdateRemoteInfoAsync(botId, ipAddress, port, instanceId, status);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
@ -128,77 +128,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (Model.IsRemote)
|
|
||||||
{
|
|
||||||
<div class="card mb-3 border-info">
|
|
||||||
<div class="card-header bg-info text-white">
|
|
||||||
<h5 class="mb-0"><i class="fas fa-satellite-dish"></i> Remote Connection</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<dl class="row mb-0">
|
|
||||||
<dt class="col-sm-4">Remote Address</dt>
|
|
||||||
<dd class="col-sm-8"><code>@Model.RemoteAddress:@Model.RemotePort</code></dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4">Instance ID</dt>
|
|
||||||
<dd class="col-sm-8"><code>@(Model.RemoteInstanceId ?? "N/A")</code></dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4">Discovery Status</dt>
|
|
||||||
<dd class="col-sm-8">
|
|
||||||
@switch (Model.DiscoveryStatus)
|
|
||||||
{
|
|
||||||
case "Configured":
|
|
||||||
<span class="badge bg-success">@Model.DiscoveryStatus</span>
|
|
||||||
break;
|
|
||||||
case "Initialized":
|
|
||||||
<span class="badge bg-info">@Model.DiscoveryStatus</span>
|
|
||||||
break;
|
|
||||||
case "Discovered":
|
|
||||||
<span class="badge bg-warning">@Model.DiscoveryStatus</span>
|
|
||||||
break;
|
|
||||||
case "Offline":
|
|
||||||
case "Error":
|
|
||||||
<span class="badge bg-danger">@Model.DiscoveryStatus</span>
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
<span class="badge bg-secondary">@Model.DiscoveryStatus</span>
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4">Last Discovery</dt>
|
|
||||||
<dd class="col-sm-8">
|
|
||||||
@if (Model.LastDiscoveryAt.HasValue)
|
|
||||||
{
|
|
||||||
@Model.LastDiscoveryAt.Value.ToString("yyyy-MM-dd HH:mm:ss")
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span class="text-muted">Never</span>
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<form asp-area="Admin" asp-controller="Bots" asp-action="CheckRemoteStatus" asp-route-id="@Model.Id" method="post" class="d-inline">
|
|
||||||
@Html.AntiForgeryToken()
|
|
||||||
<button type="submit" class="btn btn-sm btn-outline-info">
|
|
||||||
<i class="fas fa-sync"></i> Check Status
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
@if (Model.DiscoveryStatus == "Initialized" || Model.DiscoveryStatus == "Configured")
|
|
||||||
{
|
|
||||||
<a href="/Admin/Bots/RepushConfig/@Model.Id" class="btn btn-sm btn-outline-warning">
|
|
||||||
<i class="fas fa-upload"></i> Re-push Config
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">30-Day Metrics Summary</h5>
|
<h5 class="mb-0">30-Day Metrics Summary</h5>
|
||||||
|
|||||||
@ -1,286 +0,0 @@
|
|||||||
@model LittleShop.DTOs.DiscoveryWizardViewModel
|
|
||||||
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Discover Remote TeleBot";
|
|
||||||
}
|
|
||||||
|
|
||||||
<h1>Discover Remote TeleBot</h1>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8">
|
|
||||||
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
||||||
<i class="fas fa-exclamation-triangle"></i> @Model.ErrorMessage
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(Model.SuccessMessage))
|
|
||||||
{
|
|
||||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
||||||
<i class="fas fa-check-circle"></i> @Model.SuccessMessage
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (Model.CurrentStep == 1)
|
|
||||||
{
|
|
||||||
<!-- Step 1: Discovery -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0"><i class="fas fa-search"></i> Step 1: Discover TeleBot Instance</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="text-muted">
|
|
||||||
Enter the IP address and port of the TeleBot instance you want to connect.
|
|
||||||
The TeleBot must be running and configured with the same discovery secret.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form asp-area="Admin" asp-controller="Bots" asp-action="ProbeRemote" method="post">
|
|
||||||
@Html.AntiForgeryToken()
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<label for="IpAddress" class="form-label">IP Address / Hostname</label>
|
|
||||||
<input name="IpAddress" id="IpAddress" value="@Model.IpAddress" class="form-control"
|
|
||||||
placeholder="e.g., 192.168.1.100 or telebot.example.com" required />
|
|
||||||
<small class="text-muted">The IP address or hostname where TeleBot is running</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label for="Port" class="form-label">Port</label>
|
|
||||||
<input name="Port" id="Port" type="number" value="@(Model.Port == 0 ? 5010 : Model.Port)" class="form-control"
|
|
||||||
min="1" max="65535" required />
|
|
||||||
<small class="text-muted">Default: 5010</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2 d-md-flex">
|
|
||||||
<button type="submit" class="btn btn-primary me-md-2">
|
|
||||||
<i class="fas fa-satellite-dish"></i> Probe TeleBot
|
|
||||||
</button>
|
|
||||||
<a href="/Admin/Bots" class="btn btn-secondary">Cancel</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (Model.CurrentStep == 2)
|
|
||||||
{
|
|
||||||
<!-- Step 2: Registration -->
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header bg-success text-white">
|
|
||||||
<h5 class="mb-0"><i class="fas fa-check-circle"></i> TeleBot Discovered!</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<table class="table table-sm">
|
|
||||||
<tr>
|
|
||||||
<th width="150">Instance ID:</th>
|
|
||||||
<td><code>@Model.ProbeResponse?.InstanceId</code></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Name:</th>
|
|
||||||
<td>@Model.ProbeResponse?.Name</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Version:</th>
|
|
||||||
<td>@Model.ProbeResponse?.Version</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Status:</th>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-@(Model.ProbeResponse?.Status == "Bootstrap" ? "warning" : "info")">
|
|
||||||
@Model.ProbeResponse?.Status
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Address:</th>
|
|
||||||
<td>@Model.IpAddress:@Model.Port</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0"><i class="fas fa-robot"></i> Step 2: Register Bot</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form asp-area="Admin" asp-controller="Bots" asp-action="RegisterRemote" method="post">
|
|
||||||
@Html.AntiForgeryToken()
|
|
||||||
|
|
||||||
<!-- Hidden fields to preserve discovery data -->
|
|
||||||
<input type="hidden" name="IpAddress" value="@Model.IpAddress" />
|
|
||||||
<input type="hidden" name="Port" value="@Model.Port" />
|
|
||||||
<input type="hidden" name="ProbeResponse.InstanceId" value="@Model.ProbeResponse?.InstanceId" />
|
|
||||||
<input type="hidden" name="ProbeResponse.Name" value="@Model.ProbeResponse?.Name" />
|
|
||||||
<input type="hidden" name="ProbeResponse.Version" value="@Model.ProbeResponse?.Version" />
|
|
||||||
<input type="hidden" name="ProbeResponse.Status" value="@Model.ProbeResponse?.Status" />
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="BotName" class="form-label">Bot Name</label>
|
|
||||||
<input name="BotName" id="BotName" value="@Model.BotName" class="form-control"
|
|
||||||
placeholder="e.g., Production TeleBot" required />
|
|
||||||
<small class="text-muted">A friendly name to identify this bot</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="PersonalityName" class="form-label">Personality</label>
|
|
||||||
<select name="PersonalityName" id="PersonalityName" class="form-select">
|
|
||||||
<option value="Alan" @(Model.PersonalityName == "Alan" ? "selected" : "")>Alan (Professional)</option>
|
|
||||||
<option value="Dave" @(Model.PersonalityName == "Dave" ? "selected" : "")>Dave (Casual)</option>
|
|
||||||
<option value="Sarah" @(Model.PersonalityName == "Sarah" ? "selected" : "")>Sarah (Helpful)</option>
|
|
||||||
<option value="Mike" @(Model.PersonalityName == "Mike" ? "selected" : "")>Mike (Direct)</option>
|
|
||||||
<option value="Emma" @(Model.PersonalityName == "Emma" ? "selected" : "")>Emma (Friendly)</option>
|
|
||||||
<option value="Tom" @(Model.PersonalityName == "Tom" ? "selected" : "")>Tom (Efficient)</option>
|
|
||||||
</select>
|
|
||||||
<small class="text-muted">Bot conversation style</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="Description" class="form-label">Description (Optional)</label>
|
|
||||||
<textarea name="Description" id="Description" class="form-control" rows="2"
|
|
||||||
placeholder="Brief description of this bot's purpose">@Model.Description</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2 d-md-flex">
|
|
||||||
<button type="submit" class="btn btn-success me-md-2">
|
|
||||||
<i class="fas fa-link"></i> Register & Initialize
|
|
||||||
</button>
|
|
||||||
<a href="/Admin/Bots/DiscoverRemote" class="btn btn-secondary">
|
|
||||||
<i class="fas fa-arrow-left"></i> Back
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (Model.CurrentStep == 3)
|
|
||||||
{
|
|
||||||
<!-- Step 3: Configuration -->
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header bg-info text-white">
|
|
||||||
<h5 class="mb-0"><i class="fas fa-key"></i> Bot Registered - API Key</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
@if (!string.IsNullOrEmpty(Model.BotKey))
|
|
||||||
{
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<strong>Save this Bot Key securely!</strong> It won't be shown again.
|
|
||||||
</div>
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<input type="text" class="form-control font-monospace" value="@Model.BotKey" id="botKeyInput" readonly />
|
|
||||||
<button class="btn btn-outline-secondary" type="button" onclick="copyBotKey()">
|
|
||||||
<i class="fas fa-copy"></i> Copy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0"><i class="fas fa-telegram"></i> Step 3: Configure Telegram Token</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="text-muted">
|
|
||||||
Now enter the Telegram bot token from BotFather to activate this bot.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form asp-area="Admin" asp-controller="Bots" asp-action="ConfigureRemote" method="post">
|
|
||||||
@Html.AntiForgeryToken()
|
|
||||||
|
|
||||||
<!-- Hidden fields -->
|
|
||||||
<input type="hidden" name="BotId" value="@Model.BotId" />
|
|
||||||
<input type="hidden" name="BotKey" value="@Model.BotKey" />
|
|
||||||
<input type="hidden" name="IpAddress" value="@Model.IpAddress" />
|
|
||||||
<input type="hidden" name="Port" value="@Model.Port" />
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="BotToken" class="form-label">Telegram Bot Token</label>
|
|
||||||
<input name="BotToken" id="BotToken" value="@Model.BotToken" class="form-control font-monospace"
|
|
||||||
placeholder="123456789:ABCdefGHIjklMNOpqrsTUVwxyz" required />
|
|
||||||
<small class="text-muted">
|
|
||||||
Get this from <a href="https://t.me/BotFather" target="_blank">@@BotFather</a> on Telegram
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2 d-md-flex">
|
|
||||||
<button type="submit" class="btn btn-success me-md-2">
|
|
||||||
<i class="fas fa-rocket"></i> Configure & Activate Bot
|
|
||||||
</button>
|
|
||||||
<a href="/Admin/Bots" class="btn btn-secondary">
|
|
||||||
Skip (configure later)
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Wizard Progress</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<ul class="list-unstyled">
|
|
||||||
<li class="@(Model.CurrentStep == 1 ? "text-primary fw-bold" : Model.CurrentStep > 1 ? "text-success" : "text-muted")">
|
|
||||||
<i class="fas fa-@(Model.CurrentStep == 1 ? "search" : Model.CurrentStep > 1 ? "check" : "circle")"></i>
|
|
||||||
1. Discover TeleBot
|
|
||||||
</li>
|
|
||||||
<li class="@(Model.CurrentStep == 2 ? "text-primary fw-bold" : Model.CurrentStep > 2 ? "text-success" : "text-muted")">
|
|
||||||
<i class="fas fa-@(Model.CurrentStep == 2 ? "robot" : Model.CurrentStep > 2 ? "check" : "circle")"></i>
|
|
||||||
2. Register Bot
|
|
||||||
</li>
|
|
||||||
<li class="@(Model.CurrentStep == 3 ? "text-primary fw-bold" : "text-muted")">
|
|
||||||
<i class="fas fa-@(Model.CurrentStep == 3 ? "telegram" : "circle")"></i>
|
|
||||||
3. Configure Telegram
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mt-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Requirements</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<ul class="small">
|
|
||||||
<li>TeleBot must be running</li>
|
|
||||||
<li>Same discovery secret on both sides</li>
|
|
||||||
<li>Network connectivity to TeleBot</li>
|
|
||||||
<li>Valid Telegram bot token</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (Model.CurrentStep >= 2)
|
|
||||||
{
|
|
||||||
<div class="card mt-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Connection Info</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="small mb-1"><strong>Address:</strong> @Model.IpAddress</p>
|
|
||||||
<p class="small mb-0"><strong>Port:</strong> @Model.Port</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<script>
|
|
||||||
function copyBotKey() {
|
|
||||||
var input = document.getElementById('botKeyInput');
|
|
||||||
input.select();
|
|
||||||
input.setSelectionRange(0, 99999);
|
|
||||||
navigator.clipboard.writeText(input.value).then(function() {
|
|
||||||
alert('Bot Key copied to clipboard!');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
@ -7,9 +7,6 @@
|
|||||||
<h1>Bot Management</h1>
|
<h1>Bot Management</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="/Admin/Bots/DiscoverRemote" class="btn btn-success">
|
|
||||||
<i class="fas fa-satellite-dish"></i> Discover Remote Bot
|
|
||||||
</a>
|
|
||||||
<a href="/Admin/Bots/Wizard" class="btn btn-primary">
|
<a href="/Admin/Bots/Wizard" class="btn btn-primary">
|
||||||
<i class="fas fa-magic"></i> Create Telegram Bot (Wizard)
|
<i class="fas fa-magic"></i> Create Telegram Bot (Wizard)
|
||||||
</a>
|
</a>
|
||||||
@ -139,12 +136,6 @@
|
|||||||
{
|
{
|
||||||
<span class="badge bg-secondary ms-2">@bot.PersonalityName</span>
|
<span class="badge bg-secondary ms-2">@bot.PersonalityName</span>
|
||||||
}
|
}
|
||||||
@if (bot.IsRemote)
|
|
||||||
{
|
|
||||||
<span class="badge bg-info ms-1" title="Remote bot at @bot.RemoteAddress:@bot.RemotePort">
|
|
||||||
<i class="fas fa-satellite-dish"></i> Remote
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
@if (!string.IsNullOrEmpty(bot.Description))
|
@if (!string.IsNullOrEmpty(bot.Description))
|
||||||
{
|
{
|
||||||
<br />
|
<br />
|
||||||
@ -190,37 +181,6 @@
|
|||||||
<span class="badge bg-dark">@bot.Status</span>
|
<span class="badge bg-dark">@bot.Status</span>
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@if (bot.IsRemote)
|
|
||||||
{
|
|
||||||
<br />
|
|
||||||
@switch (bot.DiscoveryStatus)
|
|
||||||
{
|
|
||||||
case "Configured":
|
|
||||||
<span class="badge bg-success small mt-1" title="Remote bot is fully configured">
|
|
||||||
<i class="fas fa-check"></i> Configured
|
|
||||||
</span>
|
|
||||||
break;
|
|
||||||
case "Initialized":
|
|
||||||
<span class="badge bg-info small mt-1" title="Remote bot initialized, awaiting config">
|
|
||||||
<i class="fas fa-clock"></i> Initialized
|
|
||||||
</span>
|
|
||||||
break;
|
|
||||||
case "Discovered":
|
|
||||||
<span class="badge bg-warning small mt-1" title="Remote bot discovered, needs setup">
|
|
||||||
<i class="fas fa-exclamation"></i> Needs Setup
|
|
||||||
</span>
|
|
||||||
break;
|
|
||||||
case "Offline":
|
|
||||||
case "Error":
|
|
||||||
<span class="badge bg-danger small mt-1" title="Remote bot is offline or errored">
|
|
||||||
<i class="fas fa-times"></i> @bot.DiscoveryStatus
|
|
||||||
</span>
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
<span class="badge bg-secondary small mt-1">@bot.DiscoveryStatus</span>
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-primary">@bot.ActiveSessions</span>
|
<span class="badge bg-primary">@bot.ActiveSessions</span>
|
||||||
|
|||||||
@ -1,177 +0,0 @@
|
|||||||
@model LittleShop.DTOs.BotDto
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = $"Re-push Configuration - {Model.Name}";
|
|
||||||
var hasExistingToken = (bool)(ViewData["HasExistingToken"] ?? false);
|
|
||||||
var existingToken = ViewData["ExistingToken"] as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
<h1>Re-push Configuration</h1>
|
|
||||||
<h4 class="text-muted">@Model.Name</h4>
|
|
||||||
|
|
||||||
<nav aria-label="breadcrumb" class="mb-4">
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li class="breadcrumb-item"><a href="/Admin/Bots">Bots</a></li>
|
|
||||||
<li class="breadcrumb-item"><a href="/Admin/Bots/Details/@Model.Id">@Model.Name</a></li>
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">Re-push Config</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
@if (TempData["Error"] != null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
||||||
@TempData["Error"]
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div class="card border-info">
|
|
||||||
<div class="card-header bg-info text-white">
|
|
||||||
<h5 class="mb-0"><i class="fas fa-upload"></i> Push Configuration to Remote Bot</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="text-muted mb-4">
|
|
||||||
This will push the Telegram bot token to the remote TeleBot instance at
|
|
||||||
<code>@Model.RemoteAddress:@Model.RemotePort</code>.
|
|
||||||
Use this when the remote bot has been restarted and needs reconfiguration.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
@if (hasExistingToken)
|
|
||||||
{
|
|
||||||
<div class="alert alert-success">
|
|
||||||
<h6><i class="fas fa-check-circle"></i> Existing Token Found</h6>
|
|
||||||
<p class="mb-2">A Telegram bot token is already stored for this bot.</p>
|
|
||||||
<form asp-area="Admin" asp-controller="Bots" asp-action="RepushConfig" asp-route-id="@Model.Id" method="post" class="d-inline">
|
|
||||||
@Html.AntiForgeryToken()
|
|
||||||
<input type="hidden" name="useExistingToken" value="true" />
|
|
||||||
<button type="submit" class="btn btn-success">
|
|
||||||
<i class="fas fa-sync"></i> Re-push Existing Token
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<p class="text-muted">Or provide a new token:</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
<form asp-area="Admin" asp-controller="Bots" asp-action="RepushConfig" asp-route-id="@Model.Id" method="post">
|
|
||||||
@Html.AntiForgeryToken()
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="botToken" class="form-label">Telegram Bot Token</label>
|
|
||||||
<input type="text" class="form-control" id="botToken" name="botToken"
|
|
||||||
placeholder="123456789:ABCDefGHIjklMNOpqrsTUVwxyz" required />
|
|
||||||
<div class="form-text">
|
|
||||||
Get this from <a href="https://t.me/BotFather" target="_blank">@@BotFather</a> on Telegram.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<i class="fas fa-upload"></i> Push New Token
|
|
||||||
</button>
|
|
||||||
<a href="/Admin/Bots/Details/@Model.Id" class="btn btn-secondary">
|
|
||||||
Cancel
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mt-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Current Remote Status</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<dl class="row mb-0">
|
|
||||||
<dt class="col-sm-4">Remote Address</dt>
|
|
||||||
<dd class="col-sm-8"><code>@Model.RemoteAddress:@Model.RemotePort</code></dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4">Instance ID</dt>
|
|
||||||
<dd class="col-sm-8"><code>@(Model.RemoteInstanceId ?? "N/A")</code></dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4">Discovery Status</dt>
|
|
||||||
<dd class="col-sm-8">
|
|
||||||
@switch (Model.DiscoveryStatus)
|
|
||||||
{
|
|
||||||
case "Configured":
|
|
||||||
<span class="badge bg-success">@Model.DiscoveryStatus</span>
|
|
||||||
break;
|
|
||||||
case "Initialized":
|
|
||||||
<span class="badge bg-info">@Model.DiscoveryStatus</span>
|
|
||||||
break;
|
|
||||||
case "Discovered":
|
|
||||||
<span class="badge bg-warning">@Model.DiscoveryStatus</span>
|
|
||||||
break;
|
|
||||||
case "Offline":
|
|
||||||
case "Error":
|
|
||||||
<span class="badge bg-danger">@Model.DiscoveryStatus</span>
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
<span class="badge bg-secondary">@Model.DiscoveryStatus</span>
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4">Last Discovery</dt>
|
|
||||||
<dd class="col-sm-8">
|
|
||||||
@if (Model.LastDiscoveryAt.HasValue)
|
|
||||||
{
|
|
||||||
@Model.LastDiscoveryAt.Value.ToString("yyyy-MM-dd HH:mm:ss")
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span class="text-muted">Never</span>
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Instructions</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<ol class="ps-3">
|
|
||||||
<li class="mb-2">Ensure the remote TeleBot is running and accessible</li>
|
|
||||||
<li class="mb-2">If the bot was just restarted, it may be in "Awaiting Discovery" mode</li>
|
|
||||||
<li class="mb-2">Enter the Telegram bot token from @@BotFather</li>
|
|
||||||
<li class="mb-2">Click "Push New Token" to configure the remote bot</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h6>When to use this:</h6>
|
|
||||||
<ul class="ps-3 text-muted small">
|
|
||||||
<li>After TeleBot container restart</li>
|
|
||||||
<li>When changing the Telegram bot token</li>
|
|
||||||
<li>If the remote bot lost its configuration</li>
|
|
||||||
<li>After infrastructure recovery</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mt-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Quick Actions</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-grid gap-2">
|
|
||||||
<form asp-area="Admin" asp-controller="Bots" asp-action="CheckRemoteStatus" asp-route-id="@Model.Id" method="post" class="d-inline">
|
|
||||||
@Html.AntiForgeryToken()
|
|
||||||
<button type="submit" class="btn btn-outline-info w-100">
|
|
||||||
<i class="fas fa-sync"></i> Check Remote Status
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<a href="/Admin/Bots/Details/@Model.Id" class="btn btn-outline-secondary">
|
|
||||||
<i class="fas fa-arrow-left"></i> Back to Details
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,146 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace LittleShop.DTOs;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Input for discovering a remote TeleBot instance
|
|
||||||
/// </summary>
|
|
||||||
public class RemoteBotDiscoveryDto
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
[StringLength(255)]
|
|
||||||
public string IpAddress { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[Range(1, 65535)]
|
|
||||||
public int Port { get; set; } = 5010;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Response from TeleBot's discovery probe endpoint
|
|
||||||
/// </summary>
|
|
||||||
public class DiscoveryProbeResponse
|
|
||||||
{
|
|
||||||
public string InstanceId { get; set; } = string.Empty;
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string Version { get; set; } = string.Empty;
|
|
||||||
public string Status { get; set; } = string.Empty;
|
|
||||||
public bool HasToken { get; set; }
|
|
||||||
public bool IsConfigured { get; set; }
|
|
||||||
public bool IsInitialized { get; set; }
|
|
||||||
public string? TelegramUsername { get; set; }
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Input for registering a discovered remote bot
|
|
||||||
/// </summary>
|
|
||||||
public class RemoteBotRegistrationDto
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
[StringLength(255)]
|
|
||||||
public string IpAddress { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[Range(1, 65535)]
|
|
||||||
public int Port { get; set; } = 5010;
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[StringLength(100)]
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[StringLength(500)]
|
|
||||||
public string Description { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[StringLength(50)]
|
|
||||||
public string PersonalityName { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Instance ID from the discovery probe response
|
|
||||||
/// </summary>
|
|
||||||
public string? RemoteInstanceId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Input for configuring a remote bot with Telegram credentials
|
|
||||||
/// </summary>
|
|
||||||
public class RemoteBotConfigureDto
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
public Guid BotId { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public string BotToken { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public Dictionary<string, object>? Settings { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Result of a discovery probe operation
|
|
||||||
/// </summary>
|
|
||||||
public class DiscoveryResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
public DiscoveryProbeResponse? ProbeResponse { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Result of initializing a remote bot
|
|
||||||
/// </summary>
|
|
||||||
public class InitializeResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
public string? InstanceId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Result of configuring a remote bot
|
|
||||||
/// </summary>
|
|
||||||
public class ConfigureResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
public string? TelegramUsername { get; set; }
|
|
||||||
public string? TelegramDisplayName { get; set; }
|
|
||||||
public string? TelegramId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// View model for the discovery wizard
|
|
||||||
/// </summary>
|
|
||||||
public class DiscoveryWizardViewModel
|
|
||||||
{
|
|
||||||
// Step 1: Discovery
|
|
||||||
public string IpAddress { get; set; } = string.Empty;
|
|
||||||
public int Port { get; set; } = 5010;
|
|
||||||
|
|
||||||
// Step 2: Registration (populated after probe)
|
|
||||||
public DiscoveryProbeResponse? ProbeResponse { get; set; }
|
|
||||||
public string BotName { get; set; } = string.Empty;
|
|
||||||
public string Description { get; set; } = string.Empty;
|
|
||||||
public string PersonalityName { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
// Step 3: Configuration (populated after registration)
|
|
||||||
public Guid? BotId { get; set; }
|
|
||||||
public string? BotKey { get; set; }
|
|
||||||
public string BotToken { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
// Step tracking
|
|
||||||
public int CurrentStep { get; set; } = 1;
|
|
||||||
public string? ErrorMessage { get; set; }
|
|
||||||
public string? SuccessMessage { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Discovery status constants
|
|
||||||
/// </summary>
|
|
||||||
public static class DiscoveryStatus
|
|
||||||
{
|
|
||||||
public const string Local = "Local";
|
|
||||||
public const string AwaitingDiscovery = "AwaitingDiscovery";
|
|
||||||
public const string Discovered = "Discovered";
|
|
||||||
public const string Initialized = "Initialized";
|
|
||||||
public const string Configured = "Configured";
|
|
||||||
public const string Offline = "Offline";
|
|
||||||
public const string Error = "Error";
|
|
||||||
}
|
|
||||||
@ -24,23 +24,6 @@ public class BotDto
|
|||||||
public string PersonalityName { get; set; } = string.Empty;
|
public string PersonalityName { get; set; } = string.Empty;
|
||||||
public Dictionary<string, object> Settings { get; set; } = new();
|
public Dictionary<string, object> Settings { get; set; } = new();
|
||||||
|
|
||||||
// Remote Discovery Fields
|
|
||||||
public string? RemoteAddress { get; set; }
|
|
||||||
public int? RemotePort { get; set; }
|
|
||||||
public DateTime? LastDiscoveryAt { get; set; }
|
|
||||||
public string DiscoveryStatus { get; set; } = "Local";
|
|
||||||
public string? RemoteInstanceId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates if this is a remotely discovered bot
|
|
||||||
/// </summary>
|
|
||||||
public bool IsRemote => !string.IsNullOrEmpty(RemoteAddress);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Full remote endpoint URL
|
|
||||||
/// </summary>
|
|
||||||
public string? RemoteEndpoint => IsRemote ? $"{RemoteAddress}:{RemotePort}" : null;
|
|
||||||
|
|
||||||
// Metrics summary
|
// Metrics summary
|
||||||
public int TotalSessions { get; set; }
|
public int TotalSessions { get; set; }
|
||||||
public int ActiveSessions { get; set; }
|
public int ActiveSessions { get; set; }
|
||||||
|
|||||||
@ -52,36 +52,6 @@ public class Bot
|
|||||||
[StringLength(50)]
|
[StringLength(50)]
|
||||||
public string PersonalityName { get; set; } = string.Empty;
|
public string PersonalityName { get; set; } = string.Empty;
|
||||||
|
|
||||||
// Remote Discovery Fields
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// IP address or hostname of the remote TeleBot instance
|
|
||||||
/// </summary>
|
|
||||||
[StringLength(255)]
|
|
||||||
public string? RemoteAddress { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Port number for the remote TeleBot instance
|
|
||||||
/// </summary>
|
|
||||||
public int? RemotePort { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Timestamp of last successful discovery probe
|
|
||||||
/// </summary>
|
|
||||||
public DateTime? LastDiscoveryAt { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Discovery status: Local, Discovered, Initialized, Configured, Offline
|
|
||||||
/// </summary>
|
|
||||||
[StringLength(50)]
|
|
||||||
public string DiscoveryStatus { get; set; } = "Local";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Instance ID returned by the remote TeleBot
|
|
||||||
/// </summary>
|
|
||||||
[StringLength(100)]
|
|
||||||
public string? RemoteInstanceId { get; set; }
|
|
||||||
|
|
||||||
// Navigation properties
|
// Navigation properties
|
||||||
public virtual ICollection<BotMetric> Metrics { get; set; } = new List<BotMetric>();
|
public virtual ICollection<BotMetric> Metrics { get; set; } = new List<BotMetric>();
|
||||||
public virtual ICollection<BotSession> Sessions { get; set; } = new List<BotSession>();
|
public virtual ICollection<BotSession> Sessions { get; set; } = new List<BotSession>();
|
||||||
|
|||||||
@ -226,7 +226,6 @@ builder.Services.AddScoped<IDataSeederService, DataSeederService>();
|
|||||||
builder.Services.AddScoped<IBotService, BotService>();
|
builder.Services.AddScoped<IBotService, BotService>();
|
||||||
builder.Services.AddScoped<IBotMetricsService, BotMetricsService>();
|
builder.Services.AddScoped<IBotMetricsService, BotMetricsService>();
|
||||||
builder.Services.AddScoped<IBotContactService, BotContactService>();
|
builder.Services.AddScoped<IBotContactService, BotContactService>();
|
||||||
builder.Services.AddHttpClient<IBotDiscoveryService, BotDiscoveryService>();
|
|
||||||
builder.Services.AddScoped<IMessageDeliveryService, MessageDeliveryService>();
|
builder.Services.AddScoped<IMessageDeliveryService, MessageDeliveryService>();
|
||||||
builder.Services.AddScoped<ICustomerService, CustomerService>();
|
builder.Services.AddScoped<ICustomerService, CustomerService>();
|
||||||
builder.Services.AddScoped<ICustomerMessageService, CustomerMessageService>();
|
builder.Services.AddScoped<ICustomerMessageService, CustomerMessageService>();
|
||||||
@ -432,13 +431,15 @@ app.MapGet("/api/version", () =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply database migrations
|
// Apply database migrations and seed data
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
var context = scope.ServiceProvider.GetRequiredService<LittleShopContext>();
|
var context = scope.ServiceProvider.GetRequiredService<LittleShopContext>();
|
||||||
|
|
||||||
// Always use migrations for consistent database initialization
|
// Use proper migrations in production, EnsureCreated only for development/testing
|
||||||
Log.Information("Applying database migrations...");
|
if (app.Environment.IsProduction())
|
||||||
|
{
|
||||||
|
Log.Information("Production environment: Applying database migrations...");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
context.Database.Migrate();
|
context.Database.Migrate();
|
||||||
@ -449,12 +450,33 @@ using (var scope = app.Services.CreateScope())
|
|||||||
Log.Fatal(ex, "Database migration failed. Application cannot start.");
|
Log.Fatal(ex, "Database migration failed. Application cannot start.");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Information("Development/Testing environment: Using EnsureCreated");
|
||||||
|
context.Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
// Seed default admin user only
|
// Seed default admin user
|
||||||
var authService = scope.ServiceProvider.GetRequiredService<IAuthService>();
|
var authService = scope.ServiceProvider.GetRequiredService<IAuthService>();
|
||||||
await authService.SeedDefaultUserAsync();
|
await authService.SeedDefaultUserAsync();
|
||||||
|
|
||||||
Log.Information("Database initialization complete - fresh install ready");
|
// Seed sample data - only in development
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
Log.Information("Development environment: Seeding sample data");
|
||||||
|
var dataSeeder = scope.ServiceProvider.GetRequiredService<IDataSeederService>();
|
||||||
|
await dataSeeder.SeedSampleDataAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed system settings - enable test currencies only in development
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
Log.Information("Development environment: Enabling test currencies");
|
||||||
|
var systemSettings = scope.ServiceProvider.GetRequiredService<ISystemSettingsService>();
|
||||||
|
await systemSettings.SetTestCurrencyEnabledAsync("TBTC", true);
|
||||||
|
await systemSettings.SetTestCurrencyEnabledAsync("TLTC", true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Information("LittleShop API starting up...");
|
Log.Information("LittleShop API starting up...");
|
||||||
|
|||||||
@ -1,450 +0,0 @@
|
|||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using LittleShop.DTOs;
|
|
||||||
using LittleShop.Models;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace LittleShop.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Service for discovering and configuring remote TeleBot instances.
|
|
||||||
/// Handles communication with TeleBot's discovery API endpoints.
|
|
||||||
/// </summary>
|
|
||||||
public class BotDiscoveryService : IBotDiscoveryService
|
|
||||||
{
|
|
||||||
private readonly ILogger<BotDiscoveryService> _logger;
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
private readonly IBotService _botService;
|
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true,
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
||||||
};
|
|
||||||
|
|
||||||
public BotDiscoveryService(
|
|
||||||
ILogger<BotDiscoveryService> logger,
|
|
||||||
IConfiguration configuration,
|
|
||||||
HttpClient httpClient,
|
|
||||||
IBotService botService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_configuration = configuration;
|
|
||||||
_httpClient = httpClient;
|
|
||||||
_botService = botService;
|
|
||||||
|
|
||||||
// Configure default timeout
|
|
||||||
var timeoutSeconds = _configuration.GetValue<int>("BotDiscovery:ConnectionTimeoutSeconds", 10);
|
|
||||||
_httpClient.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DiscoveryResult> ProbeRemoteBotAsync(string ipAddress, int port)
|
|
||||||
{
|
|
||||||
var endpoint = BuildEndpoint(ipAddress, port, "/api/discovery/probe");
|
|
||||||
_logger.LogInformation("Probing remote TeleBot at {Endpoint}", endpoint);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
|
|
||||||
AddDiscoverySecret(request);
|
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var content = await response.Content.ReadAsStringAsync();
|
|
||||||
var probeResponse = JsonSerializer.Deserialize<DiscoveryProbeResponse>(content, JsonOptions);
|
|
||||||
|
|
||||||
_logger.LogInformation("Successfully probed TeleBot: {InstanceId}, Status: {Status}",
|
|
||||||
probeResponse?.InstanceId, probeResponse?.Status);
|
|
||||||
|
|
||||||
return new DiscoveryResult
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Message = "Discovery successful",
|
|
||||||
ProbeResponse = probeResponse
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Discovery probe rejected: invalid discovery secret");
|
|
||||||
return new DiscoveryResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Invalid discovery secret. Ensure the shared secret matches on both LittleShop and TeleBot."
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var errorContent = await response.Content.ReadAsStringAsync();
|
|
||||||
_logger.LogWarning("Discovery probe failed: {StatusCode} - {Content}",
|
|
||||||
response.StatusCode, errorContent);
|
|
||||||
return new DiscoveryResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = $"Discovery failed: {response.StatusCode}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Discovery probe timed out for {IpAddress}:{Port}", ipAddress, port);
|
|
||||||
return new DiscoveryResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Connection timed out. Ensure the TeleBot instance is running and accessible."
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Discovery probe connection failed for {IpAddress}:{Port}", ipAddress, port);
|
|
||||||
return new DiscoveryResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = $"Connection failed: {ex.Message}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Unexpected error during discovery probe");
|
|
||||||
return new DiscoveryResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = $"Unexpected error: {ex.Message}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<InitializeResult> InitializeRemoteBotAsync(Guid botId, string ipAddress, int port)
|
|
||||||
{
|
|
||||||
var endpoint = BuildEndpoint(ipAddress, port, "/api/discovery/initialize");
|
|
||||||
_logger.LogInformation("Initializing remote TeleBot at {Endpoint} for bot {BotId}", endpoint, botId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Get the bot to retrieve the BotKey
|
|
||||||
var bot = await _botService.GetBotByIdAsync(botId);
|
|
||||||
if (bot == null)
|
|
||||||
{
|
|
||||||
return new InitializeResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Bot not found"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the BotKey securely
|
|
||||||
var botKey = await _botService.GetBotKeyAsync(botId);
|
|
||||||
if (string.IsNullOrEmpty(botKey))
|
|
||||||
{
|
|
||||||
return new InitializeResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Bot key not found"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload = new
|
|
||||||
{
|
|
||||||
BotKey = botKey,
|
|
||||||
WebhookSecret = _configuration["BotDiscovery:WebhookSecret"] ?? "",
|
|
||||||
LittleShopUrl = GetLittleShopUrl()
|
|
||||||
};
|
|
||||||
|
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
|
||||||
AddDiscoverySecret(request);
|
|
||||||
request.Content = new StringContent(
|
|
||||||
JsonSerializer.Serialize(payload, JsonOptions),
|
|
||||||
Encoding.UTF8,
|
|
||||||
"application/json");
|
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var content = await response.Content.ReadAsStringAsync();
|
|
||||||
var initResponse = JsonSerializer.Deserialize<InitializeResponse>(content, JsonOptions);
|
|
||||||
|
|
||||||
_logger.LogInformation("Successfully initialized TeleBot: {InstanceId}", initResponse?.InstanceId);
|
|
||||||
|
|
||||||
// Update bot's discovery status
|
|
||||||
await UpdateBotDiscoveryStatus(botId, DiscoveryStatus.Initialized, ipAddress, port, initResponse?.InstanceId);
|
|
||||||
|
|
||||||
return new InitializeResult
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Message = "TeleBot initialized successfully",
|
|
||||||
InstanceId = initResponse?.InstanceId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
|
||||||
{
|
|
||||||
return new InitializeResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Invalid discovery secret"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var errorContent = await response.Content.ReadAsStringAsync();
|
|
||||||
_logger.LogWarning("Initialization failed: {StatusCode} - {Content}",
|
|
||||||
response.StatusCode, errorContent);
|
|
||||||
return new InitializeResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = $"Initialization failed: {response.StatusCode}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error during TeleBot initialization");
|
|
||||||
return new InitializeResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = $"Initialization error: {ex.Message}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ConfigureResult> PushConfigurationAsync(Guid botId, string botToken, Dictionary<string, object>? settings = null)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Pushing configuration to bot {BotId}", botId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Get the bot details
|
|
||||||
var bot = await _botService.GetBotByIdAsync(botId);
|
|
||||||
if (bot == null)
|
|
||||||
{
|
|
||||||
return new ConfigureResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Bot not found"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(bot.RemoteAddress) || !bot.RemotePort.HasValue)
|
|
||||||
{
|
|
||||||
return new ConfigureResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Bot does not have remote address configured"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the BotKey securely
|
|
||||||
var botKey = await _botService.GetBotKeyAsync(botId);
|
|
||||||
if (string.IsNullOrEmpty(botKey))
|
|
||||||
{
|
|
||||||
return new ConfigureResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Bot key not found"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var endpoint = BuildEndpoint(bot.RemoteAddress, bot.RemotePort.Value, "/api/discovery/configure");
|
|
||||||
|
|
||||||
var payload = new
|
|
||||||
{
|
|
||||||
BotToken = botToken,
|
|
||||||
Settings = settings
|
|
||||||
};
|
|
||||||
|
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
|
||||||
request.Headers.Add("X-Bot-Key", botKey);
|
|
||||||
request.Content = new StringContent(
|
|
||||||
JsonSerializer.Serialize(payload, JsonOptions),
|
|
||||||
Encoding.UTF8,
|
|
||||||
"application/json");
|
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var content = await response.Content.ReadAsStringAsync();
|
|
||||||
var configResponse = JsonSerializer.Deserialize<ConfigureResponse>(content, JsonOptions);
|
|
||||||
|
|
||||||
_logger.LogInformation("Successfully configured TeleBot: @{Username}", configResponse?.TelegramUsername);
|
|
||||||
|
|
||||||
// Update bot's discovery status and platform info
|
|
||||||
await UpdateBotDiscoveryStatus(botId, DiscoveryStatus.Configured, bot.RemoteAddress, bot.RemotePort.Value, bot.RemoteInstanceId);
|
|
||||||
|
|
||||||
// Update platform info
|
|
||||||
if (!string.IsNullOrEmpty(configResponse?.TelegramUsername))
|
|
||||||
{
|
|
||||||
await _botService.UpdatePlatformInfoAsync(botId, new UpdatePlatformInfoDto
|
|
||||||
{
|
|
||||||
PlatformUsername = configResponse.TelegramUsername,
|
|
||||||
PlatformDisplayName = configResponse.TelegramDisplayName ?? configResponse.TelegramUsername,
|
|
||||||
PlatformId = configResponse.TelegramId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ConfigureResult
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Message = "Configuration pushed successfully",
|
|
||||||
TelegramUsername = configResponse?.TelegramUsername,
|
|
||||||
TelegramDisplayName = configResponse?.TelegramDisplayName,
|
|
||||||
TelegramId = configResponse?.TelegramId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
|
||||||
{
|
|
||||||
return new ConfigureResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Invalid bot key. The bot may need to be re-initialized."
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var errorContent = await response.Content.ReadAsStringAsync();
|
|
||||||
_logger.LogWarning("Configuration push failed: {StatusCode} - {Content}",
|
|
||||||
response.StatusCode, errorContent);
|
|
||||||
return new ConfigureResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = $"Configuration failed: {response.StatusCode}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error pushing configuration to TeleBot");
|
|
||||||
return new ConfigureResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = $"Configuration error: {ex.Message}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> TestConnectivityAsync(string ipAddress, int port)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var endpoint = BuildEndpoint(ipAddress, port, "/api/discovery/probe");
|
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
|
|
||||||
AddDiscoverySecret(request);
|
|
||||||
|
|
||||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
||||||
var response = await _httpClient.SendAsync(request, cts.Token);
|
|
||||||
|
|
||||||
return response.IsSuccessStatusCode;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DiscoveryProbeResponse?> GetRemoteStatusAsync(string ipAddress, int port, string botKey)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var endpoint = BuildEndpoint(ipAddress, port, "/api/discovery/status");
|
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
|
|
||||||
request.Headers.Add("X-Bot-Key", botKey);
|
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var content = await response.Content.ReadAsStringAsync();
|
|
||||||
return JsonSerializer.Deserialize<DiscoveryProbeResponse>(content, JsonOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Private Methods
|
|
||||||
|
|
||||||
private string BuildEndpoint(string ipAddress, int port, string path)
|
|
||||||
{
|
|
||||||
// Use HTTP for local/private networks, HTTPS for public
|
|
||||||
var scheme = IsPrivateNetwork(ipAddress) ? "http" : "https";
|
|
||||||
return $"{scheme}://{ipAddress}:{port}{path}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsPrivateNetwork(string ipAddress)
|
|
||||||
{
|
|
||||||
// Check if IP is in private ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x, localhost)
|
|
||||||
if (ipAddress == "localhost" || ipAddress == "127.0.0.1")
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (System.Net.IPAddress.TryParse(ipAddress, out var ip))
|
|
||||||
{
|
|
||||||
var bytes = ip.GetAddressBytes();
|
|
||||||
if (bytes.Length == 4)
|
|
||||||
{
|
|
||||||
if (bytes[0] == 10) return true;
|
|
||||||
if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) return true;
|
|
||||||
if (bytes[0] == 192 && bytes[1] == 168) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddDiscoverySecret(HttpRequestMessage request)
|
|
||||||
{
|
|
||||||
var secret = _configuration["BotDiscovery:SharedSecret"];
|
|
||||||
if (!string.IsNullOrEmpty(secret))
|
|
||||||
{
|
|
||||||
request.Headers.Add("X-Discovery-Secret", secret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetLittleShopUrl()
|
|
||||||
{
|
|
||||||
// Return the public URL for LittleShop API
|
|
||||||
return _configuration["BotDiscovery:LittleShopApiUrl"]
|
|
||||||
?? _configuration["Kestrel:Endpoints:Https:Url"]
|
|
||||||
?? "http://localhost:5000";
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateBotDiscoveryStatus(Guid botId, string status, string ipAddress, int port, string? instanceId)
|
|
||||||
{
|
|
||||||
var bot = await _botService.GetBotByIdAsync(botId);
|
|
||||||
if (bot != null)
|
|
||||||
{
|
|
||||||
// Update via direct database access would be better, but for now use a workaround
|
|
||||||
// This would typically be done through a dedicated method on IBotService
|
|
||||||
_logger.LogInformation("Updating bot {BotId} discovery status to {Status}", botId, status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Response DTOs
|
|
||||||
|
|
||||||
private class InitializeResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
public string? InstanceId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ConfigureResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
public string? TelegramUsername { get; set; }
|
|
||||||
public string? TelegramDisplayName { get; set; }
|
|
||||||
public string? TelegramId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@ -323,31 +323,6 @@ public class BotService : IBotService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> UpdateRemoteInfoAsync(Guid botId, string remoteAddress, int remotePort, string? instanceId, string discoveryStatus)
|
|
||||||
{
|
|
||||||
var bot = await _context.Bots.FindAsync(botId);
|
|
||||||
if (bot == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bot.RemoteAddress = remoteAddress;
|
|
||||||
bot.RemotePort = remotePort;
|
|
||||||
bot.RemoteInstanceId = instanceId;
|
|
||||||
bot.DiscoveryStatus = discoveryStatus;
|
|
||||||
bot.LastDiscoveryAt = DateTime.UtcNow;
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogInformation("Updated remote info for bot {BotId}: {Address}:{Port} (Status: {Status})",
|
|
||||||
botId, remoteAddress, remotePort, discoveryStatus);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string?> GetBotKeyAsync(Guid botId)
|
|
||||||
{
|
|
||||||
var bot = await _context.Bots.FindAsync(botId);
|
|
||||||
return bot?.BotKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BotDto MapToDto(Bot bot)
|
private BotDto MapToDto(Bot bot)
|
||||||
{
|
{
|
||||||
var settings = new Dictionary<string, object>();
|
var settings = new Dictionary<string, object>();
|
||||||
@ -380,13 +355,6 @@ public class BotService : IBotService
|
|||||||
PlatformId = bot.PlatformId,
|
PlatformId = bot.PlatformId,
|
||||||
PersonalityName = bot.PersonalityName,
|
PersonalityName = bot.PersonalityName,
|
||||||
Settings = settings,
|
Settings = settings,
|
||||||
// Remote Discovery Fields
|
|
||||||
RemoteAddress = bot.RemoteAddress,
|
|
||||||
RemotePort = bot.RemotePort,
|
|
||||||
LastDiscoveryAt = bot.LastDiscoveryAt,
|
|
||||||
DiscoveryStatus = bot.DiscoveryStatus,
|
|
||||||
RemoteInstanceId = bot.RemoteInstanceId,
|
|
||||||
// Metrics
|
|
||||||
TotalSessions = bot.Sessions.Count,
|
TotalSessions = bot.Sessions.Count,
|
||||||
ActiveSessions = activeSessions,
|
ActiveSessions = activeSessions,
|
||||||
TotalRevenue = totalRevenue,
|
TotalRevenue = totalRevenue,
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
using LittleShop.DTOs;
|
|
||||||
|
|
||||||
namespace LittleShop.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Service for discovering and configuring remote TeleBot instances
|
|
||||||
/// </summary>
|
|
||||||
public interface IBotDiscoveryService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Probe a remote TeleBot instance to discover its status
|
|
||||||
/// </summary>
|
|
||||||
Task<DiscoveryResult> ProbeRemoteBotAsync(string ipAddress, int port);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize a remote TeleBot instance with a BotKey
|
|
||||||
/// </summary>
|
|
||||||
Task<InitializeResult> InitializeRemoteBotAsync(Guid botId, string ipAddress, int port);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Push configuration (bot token and settings) to a remote TeleBot instance
|
|
||||||
/// </summary>
|
|
||||||
Task<ConfigureResult> PushConfigurationAsync(Guid botId, string botToken, Dictionary<string, object>? settings = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Test basic connectivity to a remote address
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> TestConnectivityAsync(string ipAddress, int port);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the status of a remote TeleBot instance
|
|
||||||
/// </summary>
|
|
||||||
Task<DiscoveryProbeResponse?> GetRemoteStatusAsync(string ipAddress, int port, string botKey);
|
|
||||||
}
|
|
||||||
@ -23,6 +23,4 @@ public interface IBotService
|
|||||||
Task<bool> ValidateBotKeyAsync(string botKey);
|
Task<bool> ValidateBotKeyAsync(string botKey);
|
||||||
Task<string> GenerateBotKeyAsync();
|
Task<string> GenerateBotKeyAsync();
|
||||||
Task<bool> UpdatePlatformInfoAsync(Guid botId, UpdatePlatformInfoDto dto);
|
Task<bool> UpdatePlatformInfoAsync(Guid botId, UpdatePlatformInfoDto dto);
|
||||||
Task<bool> UpdateRemoteInfoAsync(Guid botId, string remoteAddress, int remotePort, string? instanceId, string discoveryStatus);
|
|
||||||
Task<string?> GetBotKeyAsync(Guid botId);
|
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Data Source=teleshop-dev.db"
|
"DefaultConnection": "Data Source=littleshop-dev.db"
|
||||||
},
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Key": "DEVELOPMENT_USE_DOTNET_USER_SECRETS_OR_ENV_VAR",
|
"Key": "DEVELOPMENT_USE_DOTNET_USER_SECRETS_OR_ENV_VAR",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Data Source=/app/data/teleshop.db"
|
"DefaultConnection": "Data Source=/app/data/littleshop.db"
|
||||||
},
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Key": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!",
|
"Key": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!",
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Data Source=teleshop-production.db"
|
"DefaultConnection": "Data Source=littleshop-production.db"
|
||||||
},
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Key": "${JWT_SECRET_KEY}",
|
"Key": "${JWT_SECRET_KEY}",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Data Source=teleshop.db"
|
"DefaultConnection": "Data Source=littleshop.db"
|
||||||
},
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Key": "",
|
"Key": "",
|
||||||
@ -47,14 +47,6 @@
|
|||||||
"172.16.0.0/12"
|
"172.16.0.0/12"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"BotDiscovery": {
|
|
||||||
"SharedSecret": "CHANGE_THIS_SHARED_SECRET_32_CHARS",
|
|
||||||
"ConnectionTimeoutSeconds": 10,
|
|
||||||
"ProbeRetryAttempts": 3,
|
|
||||||
"WebhookSecret": "",
|
|
||||||
"LittleShopApiUrl": "",
|
|
||||||
"Comment": "SharedSecret must match TeleBot Discovery:Secret. LittleShopApiUrl is the public URL for TeleBot to call back."
|
|
||||||
},
|
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
@ -1,255 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using TeleBot.DTOs;
|
|
||||||
using TeleBot.Services;
|
|
||||||
|
|
||||||
namespace TeleBot.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// API controller for remote discovery and configuration from LittleShop.
|
|
||||||
/// Enables server-initiated bot registration and configuration.
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
public class DiscoveryController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly ILogger<DiscoveryController> _logger;
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly BotManagerService _botManagerService;
|
|
||||||
private readonly TelegramBotService _telegramBotService;
|
|
||||||
|
|
||||||
public DiscoveryController(
|
|
||||||
ILogger<DiscoveryController> logger,
|
|
||||||
IConfiguration configuration,
|
|
||||||
BotManagerService botManagerService,
|
|
||||||
TelegramBotService telegramBotService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_configuration = configuration;
|
|
||||||
_botManagerService = botManagerService;
|
|
||||||
_telegramBotService = telegramBotService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Probe endpoint for LittleShop to discover this TeleBot instance.
|
|
||||||
/// Returns current status and configuration state.
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("probe")]
|
|
||||||
public IActionResult Probe()
|
|
||||||
{
|
|
||||||
// Validate discovery secret
|
|
||||||
if (!ValidateDiscoverySecret())
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Discovery probe rejected: invalid or missing X-Discovery-Secret");
|
|
||||||
return Unauthorized(new { error = "Invalid discovery secret" });
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Discovery probe received from {RemoteIp}", GetRemoteIp());
|
|
||||||
|
|
||||||
var response = new DiscoveryProbeResponse
|
|
||||||
{
|
|
||||||
InstanceId = _botManagerService.InstanceId,
|
|
||||||
Name = _configuration["BotInfo:Name"] ?? "TeleBot",
|
|
||||||
Version = _configuration["BotInfo:Version"] ?? "1.0.0",
|
|
||||||
Status = _botManagerService.CurrentStatus,
|
|
||||||
HasToken = _botManagerService.HasBotToken,
|
|
||||||
IsConfigured = _botManagerService.IsConfigured,
|
|
||||||
IsInitialized = _botManagerService.IsInitialized,
|
|
||||||
TelegramUsername = _botManagerService.TelegramUsername,
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
return Ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize this TeleBot instance with a BotKey from LittleShop.
|
|
||||||
/// This is the first step after discovery - assigns the bot to LittleShop.
|
|
||||||
/// </summary>
|
|
||||||
[HttpPost("initialize")]
|
|
||||||
public async Task<IActionResult> Initialize([FromBody] DiscoveryInitializeRequest request)
|
|
||||||
{
|
|
||||||
// Validate discovery secret
|
|
||||||
if (!ValidateDiscoverySecret())
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Discovery initialize rejected: invalid or missing X-Discovery-Secret");
|
|
||||||
return Unauthorized(new { error = "Invalid discovery secret" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(request.BotKey))
|
|
||||||
{
|
|
||||||
return BadRequest(new DiscoveryInitializeResponse
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "BotKey is required"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Initializing TeleBot from LittleShop discovery. Remote IP: {RemoteIp}", GetRemoteIp());
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await _botManagerService.InitializeFromDiscoveryAsync(
|
|
||||||
request.BotKey,
|
|
||||||
request.WebhookSecret,
|
|
||||||
request.LittleShopUrl);
|
|
||||||
|
|
||||||
if (result.Success)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("TeleBot initialized successfully with BotKey");
|
|
||||||
return Ok(new DiscoveryInitializeResponse
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Message = "TeleBot initialized successfully",
|
|
||||||
InstanceId = _botManagerService.InstanceId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning("TeleBot initialization failed: {Message}", result.Message);
|
|
||||||
return BadRequest(new DiscoveryInitializeResponse
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = result.Message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error during TeleBot initialization");
|
|
||||||
return StatusCode(500, new DiscoveryInitializeResponse
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Internal server error during initialization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configure this TeleBot instance with Telegram credentials.
|
|
||||||
/// Requires prior initialization (valid BotKey).
|
|
||||||
/// </summary>
|
|
||||||
[HttpPost("configure")]
|
|
||||||
public async Task<IActionResult> Configure([FromBody] DiscoveryConfigureRequest request)
|
|
||||||
{
|
|
||||||
// After initialization, use X-Bot-Key for authentication
|
|
||||||
if (!ValidateBotKey())
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Discovery configure rejected: invalid or missing X-Bot-Key");
|
|
||||||
return Unauthorized(new { error = "Invalid bot key" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(request.BotToken))
|
|
||||||
{
|
|
||||||
return BadRequest(new DiscoveryConfigureResponse
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "BotToken is required"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Configuring TeleBot with Telegram credentials");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await _botManagerService.ApplyRemoteConfigurationAsync(
|
|
||||||
request.BotToken,
|
|
||||||
request.Settings);
|
|
||||||
|
|
||||||
if (result.Success)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("TeleBot configured successfully. Telegram: @{Username}", result.TelegramUsername);
|
|
||||||
return Ok(new DiscoveryConfigureResponse
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Message = "TeleBot configured and operational",
|
|
||||||
TelegramUsername = result.TelegramUsername,
|
|
||||||
TelegramDisplayName = result.TelegramDisplayName,
|
|
||||||
TelegramId = result.TelegramId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning("TeleBot configuration failed: {Message}", result.Message);
|
|
||||||
return BadRequest(new DiscoveryConfigureResponse
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = result.Message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error during TeleBot configuration");
|
|
||||||
return StatusCode(500, new DiscoveryConfigureResponse
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Internal server error during configuration"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get current status of the bot (requires BotKey after initialization)
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("status")]
|
|
||||||
public IActionResult Status()
|
|
||||||
{
|
|
||||||
// Allow both discovery secret (pre-init) and bot key (post-init)
|
|
||||||
if (!ValidateDiscoverySecret() && !ValidateBotKey())
|
|
||||||
{
|
|
||||||
return Unauthorized(new { error = "Invalid credentials" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(new BotStatusUpdate
|
|
||||||
{
|
|
||||||
Status = _botManagerService.CurrentStatus,
|
|
||||||
IsOperational = _botManagerService.IsConfigured && _telegramBotService.IsRunning,
|
|
||||||
ActiveSessions = _botManagerService.ActiveSessionCount,
|
|
||||||
LastActivityAt = _botManagerService.LastActivityAt,
|
|
||||||
Metadata = new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
["instanceId"] = _botManagerService.InstanceId,
|
|
||||||
["version"] = _configuration["BotInfo:Version"] ?? "1.0.0",
|
|
||||||
["telegramUsername"] = _botManagerService.TelegramUsername ?? "",
|
|
||||||
["hasToken"] = _botManagerService.HasBotToken,
|
|
||||||
["isInitialized"] = _botManagerService.IsInitialized
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ValidateDiscoverySecret()
|
|
||||||
{
|
|
||||||
var providedSecret = Request.Headers["X-Discovery-Secret"].ToString();
|
|
||||||
var expectedSecret = _configuration["Discovery:Secret"];
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(expectedSecret))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Discovery secret not configured in appsettings.json");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !string.IsNullOrEmpty(providedSecret) &&
|
|
||||||
string.Equals(providedSecret, expectedSecret, StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ValidateBotKey()
|
|
||||||
{
|
|
||||||
var providedKey = Request.Headers["X-Bot-Key"].ToString();
|
|
||||||
var storedKey = _botManagerService.BotKey;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(storedKey))
|
|
||||||
{
|
|
||||||
return false; // Not initialized yet
|
|
||||||
}
|
|
||||||
|
|
||||||
return !string.IsNullOrEmpty(providedKey) &&
|
|
||||||
string.Equals(providedKey, storedKey, StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetRemoteIp()
|
|
||||||
{
|
|
||||||
return HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace TeleBot.DTOs;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Response returned when LittleShop probes this TeleBot instance
|
|
||||||
/// </summary>
|
|
||||||
public class DiscoveryProbeResponse
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Unique identifier for this TeleBot instance (generated on first startup)
|
|
||||||
/// </summary>
|
|
||||||
public string InstanceId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configured name of this bot
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// TeleBot version
|
|
||||||
/// </summary>
|
|
||||||
public string Version { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current operational status
|
|
||||||
/// </summary>
|
|
||||||
public string Status { get; set; } = "Bootstrap";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether a Telegram bot token has been configured
|
|
||||||
/// </summary>
|
|
||||||
public bool HasToken { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the bot is fully configured and operational
|
|
||||||
/// </summary>
|
|
||||||
public bool IsConfigured { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this instance has been initialized (has BotKey)
|
|
||||||
/// </summary>
|
|
||||||
public bool IsInitialized { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Telegram username if configured and operational
|
|
||||||
/// </summary>
|
|
||||||
public string? TelegramUsername { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Timestamp of probe response
|
|
||||||
/// </summary>
|
|
||||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Request to initialize this TeleBot instance from LittleShop
|
|
||||||
/// </summary>
|
|
||||||
public class DiscoveryInitializeRequest
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Bot key assigned by LittleShop for authentication
|
|
||||||
/// </summary>
|
|
||||||
public string BotKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Secret for webhook authentication
|
|
||||||
/// </summary>
|
|
||||||
public string WebhookSecret { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// LittleShop API URL (if different from discovery source)
|
|
||||||
/// </summary>
|
|
||||||
public string? LittleShopUrl { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Response after initialization
|
|
||||||
/// </summary>
|
|
||||||
public class DiscoveryInitializeResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
public string? InstanceId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Request to configure this TeleBot instance with Telegram credentials
|
|
||||||
/// </summary>
|
|
||||||
public class DiscoveryConfigureRequest
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Telegram Bot token from BotFather
|
|
||||||
/// </summary>
|
|
||||||
public string BotToken { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Additional settings to apply
|
|
||||||
/// </summary>
|
|
||||||
public Dictionary<string, object>? Settings { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Response after configuration
|
|
||||||
/// </summary>
|
|
||||||
public class DiscoveryConfigureResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Telegram bot username (e.g., @MyBot)
|
|
||||||
/// </summary>
|
|
||||||
public string? TelegramUsername { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Telegram bot display name
|
|
||||||
/// </summary>
|
|
||||||
public string? TelegramDisplayName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Telegram bot ID
|
|
||||||
/// </summary>
|
|
||||||
public string? TelegramId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Status update sent to indicate bot operational state
|
|
||||||
/// </summary>
|
|
||||||
public class BotStatusUpdate
|
|
||||||
{
|
|
||||||
public string Status { get; set; } = "Unknown";
|
|
||||||
public bool IsOperational { get; set; }
|
|
||||||
public int ActiveSessions { get; set; }
|
|
||||||
public DateTime LastActivityAt { get; set; }
|
|
||||||
public Dictionary<string, object>? Metadata { get; set; }
|
|
||||||
}
|
|
||||||
@ -106,16 +106,6 @@ builder.Services.AddHttpClient<BotManagerService>()
|
|||||||
builder.Services.AddSingleton<BotManagerService>();
|
builder.Services.AddSingleton<BotManagerService>();
|
||||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<BotManagerService>());
|
builder.Services.AddHostedService(provider => provider.GetRequiredService<BotManagerService>());
|
||||||
|
|
||||||
// Liveness Service - Monitors LittleShop connectivity and triggers shutdown on failure
|
|
||||||
builder.Services.AddHttpClient<LivenessService>()
|
|
||||||
.ConfigurePrimaryHttpMessageHandler(sp =>
|
|
||||||
{
|
|
||||||
var logger = sp.GetRequiredService<ILoggerFactory>().CreateLogger("Liveness");
|
|
||||||
return Socks5HttpHandler.CreateDirect(logger);
|
|
||||||
});
|
|
||||||
builder.Services.AddSingleton<LivenessService>();
|
|
||||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<LivenessService>());
|
|
||||||
|
|
||||||
// Message Delivery Service - Single instance
|
// Message Delivery Service - Single instance
|
||||||
builder.Services.AddSingleton<MessageDeliveryService>();
|
builder.Services.AddSingleton<MessageDeliveryService>();
|
||||||
builder.Services.AddSingleton<IMessageDeliveryService>(sp => sp.GetRequiredService<MessageDeliveryService>());
|
builder.Services.AddSingleton<IMessageDeliveryService>(sp => sp.GetRequiredService<MessageDeliveryService>());
|
||||||
@ -165,8 +155,6 @@ try
|
|||||||
Log.Information("Privacy Mode: {PrivacyMode}", builder.Configuration["Privacy:Mode"]);
|
Log.Information("Privacy Mode: {PrivacyMode}", builder.Configuration["Privacy:Mode"]);
|
||||||
Log.Information("Ephemeral by Default: {Ephemeral}", builder.Configuration["Privacy:EphemeralByDefault"]);
|
Log.Information("Ephemeral by Default: {Ephemeral}", builder.Configuration["Privacy:EphemeralByDefault"]);
|
||||||
Log.Information("Tor Enabled: {Tor}", builder.Configuration["Privacy:EnableTor"]);
|
Log.Information("Tor Enabled: {Tor}", builder.Configuration["Privacy:EnableTor"]);
|
||||||
Log.Information("Discovery endpoint: GET /api/discovery/probe");
|
|
||||||
Log.Information("LittleShop API: {ApiUrl}", builder.Configuration["LittleShop:ApiUrl"]);
|
|
||||||
Log.Information("Webhook endpoints available at /api/webhook");
|
Log.Information("Webhook endpoints available at /api/webhook");
|
||||||
|
|
||||||
await app.RunAsync();
|
await app.RunAsync();
|
||||||
|
|||||||
@ -11,43 +11,22 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace TeleBot.Services;
|
namespace TeleBot.Services
|
||||||
|
{
|
||||||
/// <summary>
|
|
||||||
/// Manages bot lifecycle, LittleShop communication, and server-initiated configuration.
|
|
||||||
/// Operates in Bootstrap mode until initialized by LittleShop discovery.
|
|
||||||
/// </summary>
|
|
||||||
public class BotManagerService : IHostedService, IDisposable
|
public class BotManagerService : IHostedService, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<BotManagerService> _logger;
|
private readonly ILogger<BotManagerService> _logger;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly SessionManager _sessionManager;
|
private readonly SessionManager _sessionManager;
|
||||||
|
|
||||||
private Timer? _heartbeatTimer;
|
private Timer? _heartbeatTimer;
|
||||||
private Timer? _metricsTimer;
|
private Timer? _metricsTimer;
|
||||||
private Timer? _settingsSyncTimer;
|
private Timer? _settingsSyncTimer;
|
||||||
|
|
||||||
private string? _botKey;
|
private string? _botKey;
|
||||||
private Guid? _botId;
|
private Guid? _botId;
|
||||||
private string? _webhookSecret;
|
|
||||||
private string? _telegramUsername;
|
|
||||||
private string? _telegramDisplayName;
|
|
||||||
private string? _telegramId;
|
|
||||||
private string? _currentBotToken;
|
|
||||||
|
|
||||||
private readonly Dictionary<string, decimal> _metricsBuffer;
|
private readonly Dictionary<string, decimal> _metricsBuffer;
|
||||||
private TelegramBotService? _telegramBotService;
|
private TelegramBotService? _telegramBotService;
|
||||||
private string _instanceId;
|
private string? _lastKnownBotToken;
|
||||||
private string _currentStatus = "Bootstrap";
|
|
||||||
private DateTime _lastActivityAt = DateTime.UtcNow;
|
|
||||||
|
|
||||||
// Status constants
|
|
||||||
public const string STATUS_BOOTSTRAP = "Bootstrap";
|
|
||||||
public const string STATUS_INITIALIZED = "Initialized";
|
|
||||||
public const string STATUS_CONFIGURING = "Configuring";
|
|
||||||
public const string STATUS_OPERATIONAL = "Operational";
|
|
||||||
public const string STATUS_ERROR = "Error";
|
|
||||||
|
|
||||||
public BotManagerService(
|
public BotManagerService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
@ -60,60 +39,8 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_metricsBuffer = new Dictionary<string, decimal>();
|
_metricsBuffer = new Dictionary<string, decimal>();
|
||||||
|
|
||||||
// Generate or load instance ID
|
|
||||||
_instanceId = LoadOrGenerateInstanceId();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Public Properties
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unique identifier for this TeleBot instance
|
|
||||||
/// </summary>
|
|
||||||
public string InstanceId => _instanceId;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current operational status
|
|
||||||
/// </summary>
|
|
||||||
public string CurrentStatus => _currentStatus;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether a Telegram bot token has been configured
|
|
||||||
/// </summary>
|
|
||||||
public bool HasBotToken => !string.IsNullOrEmpty(_currentBotToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the bot is fully configured and operational
|
|
||||||
/// </summary>
|
|
||||||
public bool IsConfigured => HasBotToken && IsInitialized;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this instance has been initialized with a BotKey
|
|
||||||
/// </summary>
|
|
||||||
public bool IsInitialized => !string.IsNullOrEmpty(_botKey);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Telegram username if operational
|
|
||||||
/// </summary>
|
|
||||||
public string? TelegramUsername => _telegramUsername;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The BotKey assigned by LittleShop
|
|
||||||
/// </summary>
|
|
||||||
public string? BotKey => _botKey;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Number of active sessions
|
|
||||||
/// </summary>
|
|
||||||
public int ActiveSessionCount => _sessionManager?.GetActiveSessions().Count() ?? 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Last activity timestamp
|
|
||||||
/// </summary>
|
|
||||||
public DateTime LastActivityAt => _lastActivityAt;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
public void SetTelegramBotService(TelegramBotService telegramBotService)
|
public void SetTelegramBotService(TelegramBotService telegramBotService)
|
||||||
{
|
{
|
||||||
_telegramBotService = telegramBotService;
|
_telegramBotService = telegramBotService;
|
||||||
@ -121,32 +48,65 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
|
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("BotManagerService starting...");
|
try
|
||||||
|
{
|
||||||
// Check if already configured (from previous session or config file)
|
// Check if bot key exists in configuration
|
||||||
_botKey = _configuration["BotManager:ApiKey"];
|
_botKey = _configuration["BotManager:ApiKey"];
|
||||||
_currentBotToken = _configuration["Telegram:BotToken"];
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(_botKey) && _botKey != "YOUR_BOT_KEY_HERE")
|
if (string.IsNullOrEmpty(_botKey))
|
||||||
{
|
{
|
||||||
// Previously initialized - verify with LittleShop and start
|
// Try to find existing bot registration by Telegram username first
|
||||||
_logger.LogInformation("Found existing BotKey, attempting to resume operation");
|
var botUsername = await GetTelegramBotUsernameAsync();
|
||||||
_currentStatus = STATUS_INITIALIZED;
|
|
||||||
|
|
||||||
// Start heartbeat and metrics if we have a valid token
|
if (!string.IsNullOrEmpty(botUsername))
|
||||||
if (!string.IsNullOrEmpty(_currentBotToken) && _currentBotToken != "YOUR_BOT_TOKEN_HERE")
|
|
||||||
{
|
{
|
||||||
await StartOperationalTimersAsync();
|
var existingBot = await FindExistingBotByPlatformAsync(botUsername);
|
||||||
_currentStatus = STATUS_OPERATIONAL;
|
|
||||||
|
if (existingBot != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Found existing bot registration for @{Username} (ID: {BotId}). Using existing bot.",
|
||||||
|
botUsername, existingBot.Id);
|
||||||
|
_botKey = existingBot.BotKey;
|
||||||
|
_botId = existingBot.Id;
|
||||||
|
|
||||||
|
// Update platform info in case it changed
|
||||||
|
await UpdatePlatformInfoAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No existing bot found for @{Username}. Registering new bot.", botUsername);
|
||||||
|
await RegisterBotAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Bootstrap mode - wait for LittleShop discovery
|
_logger.LogWarning("Could not determine bot username. Registering new bot.");
|
||||||
_currentStatus = STATUS_BOOTSTRAP;
|
await RegisterBotAsync();
|
||||||
_logger.LogInformation("TeleBot starting in Bootstrap mode. Waiting for LittleShop discovery...");
|
}
|
||||||
_logger.LogInformation("Instance ID: {InstanceId}", _instanceId);
|
}
|
||||||
_logger.LogInformation("Discovery endpoint: GET /api/discovery/probe");
|
else
|
||||||
|
{
|
||||||
|
// Authenticate existing bot
|
||||||
|
await AuthenticateBotAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync settings from server
|
||||||
|
await SyncSettingsAsync();
|
||||||
|
|
||||||
|
// Start heartbeat timer (every 30 seconds)
|
||||||
|
_heartbeatTimer = new Timer(SendHeartbeat, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to start bot manager service");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,136 +119,98 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
// Send final metrics before stopping
|
// Send final metrics before stopping
|
||||||
SendMetrics(null);
|
SendMetrics(null);
|
||||||
|
|
||||||
_logger.LogInformation("BotManagerService stopped");
|
_logger.LogInformation("Bot manager service stopped");
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Discovery Methods
|
private async Task RegisterBotAsync()
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize this TeleBot instance from LittleShop discovery
|
|
||||||
/// </summary>
|
|
||||||
public async Task<(bool Success, string Message)> InitializeFromDiscoveryAsync(
|
|
||||||
string botKey,
|
|
||||||
string? webhookSecret,
|
|
||||||
string? littleShopUrl)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(botKey))
|
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
||||||
|
var registrationData = new
|
||||||
{
|
{
|
||||||
return (false, "BotKey is required");
|
Name = _configuration["BotInfo:Name"] ?? "TeleBot",
|
||||||
}
|
Description = _configuration["BotInfo:Description"] ?? "Telegram E-commerce Bot",
|
||||||
|
Type = 0, // Telegram
|
||||||
// Check if already initialized with a different key
|
Version = _configuration["BotInfo:Version"] ?? "1.0.0",
|
||||||
if (!string.IsNullOrEmpty(_botKey) && _botKey != botKey)
|
InitialSettings = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Attempted to reinitialize with different BotKey. Current: {Current}, New: {New}",
|
["telegram"] = new
|
||||||
_botKey.Substring(0, 8), botKey.Substring(0, 8));
|
|
||||||
return (false, "Already initialized with a different BotKey");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
_botKey = botKey;
|
botToken = _configuration["Telegram:BotToken"],
|
||||||
_webhookSecret = webhookSecret ?? string.Empty;
|
webhookUrl = _configuration["Telegram:WebhookUrl"]
|
||||||
|
},
|
||||||
// Update LittleShop URL if provided
|
["privacy"] = new
|
||||||
if (!string.IsNullOrEmpty(littleShopUrl))
|
|
||||||
{
|
{
|
||||||
// Note: In production, this would update the configuration
|
mode = _configuration["Privacy:Mode"],
|
||||||
_logger.LogInformation("LittleShop URL override: {Url}", littleShopUrl);
|
enableTor = _configuration.GetValue<bool>("Privacy:EnableTor")
|
||||||
}
|
|
||||||
|
|
||||||
_currentStatus = STATUS_INITIALIZED;
|
|
||||||
_logger.LogInformation("TeleBot initialized with BotKey: {KeyPrefix}...", botKey.Substring(0, Math.Min(8, botKey.Length)));
|
|
||||||
|
|
||||||
// Save BotKey for persistence (in production, save to secure storage)
|
|
||||||
await SaveConfigurationAsync();
|
|
||||||
|
|
||||||
return (true, "Initialized successfully");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error during initialization");
|
|
||||||
_currentStatus = STATUS_ERROR;
|
|
||||||
return (false, $"Initialization error: {ex.Message}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
var json = JsonSerializer.Serialize(registrationData);
|
||||||
/// Apply remote configuration (bot token and settings) from LittleShop
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
/// </summary>
|
|
||||||
public async Task<(bool Success, string Message, string? TelegramUsername, string? TelegramDisplayName, string? TelegramId)> ApplyRemoteConfigurationAsync(
|
var response = await _httpClient.PostAsync($"{apiUrl}/api/bots/register", content);
|
||||||
string botToken,
|
|
||||||
Dictionary<string, object>? settings)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
if (!IsInitialized)
|
var responseJson = await response.Content.ReadAsStringAsync();
|
||||||
{
|
var result = JsonSerializer.Deserialize<BotRegistrationResponse>(responseJson);
|
||||||
return (false, "Must be initialized before configuration", null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(botToken))
|
_botKey = result?.BotKey;
|
||||||
{
|
_botId = result?.BotId;
|
||||||
return (false, "BotToken is required", null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentStatus = STATUS_CONFIGURING;
|
_logger.LogInformation("Bot registered successfully. Bot ID: {BotId}", _botId);
|
||||||
_logger.LogInformation("Applying remote configuration...");
|
_logger.LogWarning("IMPORTANT: Save this bot key securely: {BotKey}", _botKey);
|
||||||
|
|
||||||
try
|
// Update platform info immediately after registration
|
||||||
{
|
|
||||||
// Validate token with Telegram API
|
|
||||||
var telegramInfo = await ValidateTelegramTokenAsync(botToken);
|
|
||||||
if (telegramInfo == null)
|
|
||||||
{
|
|
||||||
_currentStatus = STATUS_INITIALIZED;
|
|
||||||
return (false, "Invalid Telegram bot token", null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store token and update Telegram info
|
|
||||||
_currentBotToken = botToken;
|
|
||||||
_telegramUsername = telegramInfo.Username;
|
|
||||||
_telegramDisplayName = telegramInfo.FirstName;
|
|
||||||
_telegramId = telegramInfo.Id.ToString();
|
|
||||||
|
|
||||||
// Apply additional settings if provided
|
|
||||||
if (settings != null)
|
|
||||||
{
|
|
||||||
await ApplySettingsAsync(settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start/restart the Telegram bot with new token
|
|
||||||
if (_telegramBotService != null)
|
|
||||||
{
|
|
||||||
await _telegramBotService.UpdateBotTokenAsync(botToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start operational timers
|
|
||||||
await StartOperationalTimersAsync();
|
|
||||||
|
|
||||||
// Update platform info with LittleShop
|
|
||||||
await UpdatePlatformInfoAsync();
|
await UpdatePlatformInfoAsync();
|
||||||
|
|
||||||
_currentStatus = STATUS_OPERATIONAL;
|
// Save bot key to configuration or secure storage
|
||||||
_lastActivityAt = DateTime.UtcNow;
|
// In production, this should be saved securely
|
||||||
|
|
||||||
_logger.LogInformation("TeleBot configured and operational. Telegram: @{Username}", _telegramUsername);
|
|
||||||
|
|
||||||
// Save configuration for persistence
|
|
||||||
await SaveConfigurationAsync();
|
|
||||||
|
|
||||||
return (true, "Configuration applied successfully", _telegramUsername, _telegramDisplayName, _telegramId);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error applying remote configuration");
|
_logger.LogError("Failed to register bot: {StatusCode}", response.StatusCode);
|
||||||
_currentStatus = STATUS_ERROR;
|
|
||||||
return (false, $"Configuration error: {ex.Message}", null, null, null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
private async Task AuthenticateBotAsync()
|
||||||
|
{
|
||||||
|
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
||||||
|
var authData = new { BotKey = _botKey };
|
||||||
|
|
||||||
#region Settings and Metrics
|
var json = JsonSerializer.Serialize(authData);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"{apiUrl}/api/bots/authenticate", content);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var responseJson = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonSerializer.Deserialize<BotDto>(responseJson);
|
||||||
|
|
||||||
|
_botId = result?.Id;
|
||||||
|
_logger.LogInformation("Bot authenticated successfully. Bot ID: {BotId}", _botId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError("Failed to authenticate bot: {StatusCode}", response.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SyncSettingsAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_botKey)) return;
|
||||||
|
|
||||||
|
var settings = await GetSettingsAsync();
|
||||||
|
if (settings != null)
|
||||||
|
{
|
||||||
|
// 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()
|
public async Task<Dictionary<string, object>?> GetSettingsAsync()
|
||||||
{
|
{
|
||||||
@ -317,123 +239,6 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RecordMetric(string name, decimal value)
|
|
||||||
{
|
|
||||||
lock (_metricsBuffer)
|
|
||||||
{
|
|
||||||
if (_metricsBuffer.ContainsKey(name))
|
|
||||||
_metricsBuffer[name] += value;
|
|
||||||
else
|
|
||||||
_metricsBuffer[name] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastActivityAt = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Guid?> StartSessionAsync(string sessionIdentifier, string platform = "Telegram")
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(_botKey)) return null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
|
||||||
var sessionData = new
|
|
||||||
{
|
|
||||||
SessionIdentifier = sessionIdentifier,
|
|
||||||
Platform = platform,
|
|
||||||
Language = "en",
|
|
||||||
Country = "",
|
|
||||||
IsAnonymous = true,
|
|
||||||
Metadata = new Dictionary<string, object>()
|
|
||||||
};
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(sessionData);
|
|
||||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
|
||||||
|
|
||||||
_httpClient.DefaultRequestHeaders.Clear();
|
|
||||||
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
|
|
||||||
|
|
||||||
var response = await _httpClient.PostAsync($"{apiUrl}/api/bots/sessions/start", content);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var responseJson = await response.Content.ReadAsStringAsync();
|
|
||||||
var result = JsonSerializer.Deserialize<SessionDto>(responseJson);
|
|
||||||
_lastActivityAt = DateTime.UtcNow;
|
|
||||||
return result?.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to start session");
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateSessionAsync(Guid sessionId, int? orderCount = null, int? messageCount = null, decimal? totalSpent = null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(_botKey)) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
|
||||||
var updateData = new
|
|
||||||
{
|
|
||||||
OrderCount = orderCount,
|
|
||||||
MessageCount = messageCount,
|
|
||||||
TotalSpent = totalSpent
|
|
||||||
};
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(updateData);
|
|
||||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
|
||||||
|
|
||||||
_httpClient.DefaultRequestHeaders.Clear();
|
|
||||||
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
|
|
||||||
|
|
||||||
await _httpClient.PutAsync($"{apiUrl}/api/bots/sessions/{sessionId}", content);
|
|
||||||
_lastActivityAt = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to update session");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Private Methods
|
|
||||||
|
|
||||||
private string LoadOrGenerateInstanceId()
|
|
||||||
{
|
|
||||||
// Try to load from config/file
|
|
||||||
var configuredId = _configuration["BotInfo:InstanceId"];
|
|
||||||
if (!string.IsNullOrEmpty(configuredId))
|
|
||||||
{
|
|
||||||
return configuredId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new instance ID
|
|
||||||
var newId = $"telebot-{Guid.NewGuid():N}".Substring(0, 24);
|
|
||||||
_logger.LogInformation("Generated new instance ID: {InstanceId}", newId);
|
|
||||||
return newId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartOperationalTimersAsync()
|
|
||||||
{
|
|
||||||
// Start heartbeat timer (every 30 seconds)
|
|
||||||
_heartbeatTimer = new Timer(SendHeartbeat, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
|
|
||||||
|
|
||||||
// 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("Operational timers started");
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void SendHeartbeat(object? state)
|
private async void SendHeartbeat(object? state)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_botKey)) return;
|
if (string.IsNullOrEmpty(_botKey)) return;
|
||||||
@ -446,12 +251,11 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
var heartbeatData = new
|
var heartbeatData = new
|
||||||
{
|
{
|
||||||
Version = _configuration["BotInfo:Version"] ?? "1.0.0",
|
Version = _configuration["BotInfo:Version"] ?? "1.0.0",
|
||||||
IpAddress = "REDACTED",
|
IpAddress = "REDACTED", // SECURITY: Never send real IP address
|
||||||
ActiveSessions = activeSessions,
|
ActiveSessions = activeSessions,
|
||||||
Status = new Dictionary<string, object>
|
Status = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
["healthy"] = true,
|
["healthy"] = true,
|
||||||
["status"] = _currentStatus,
|
|
||||||
["uptime"] = DateTime.UtcNow.Subtract(AppDomain.CurrentDomain.BaseDirectory != null
|
["uptime"] = DateTime.UtcNow.Subtract(AppDomain.CurrentDomain.BaseDirectory != null
|
||||||
? new System.IO.DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory).CreationTimeUtc
|
? new System.IO.DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory).CreationTimeUtc
|
||||||
: DateTime.UtcNow).TotalSeconds
|
: DateTime.UtcNow).TotalSeconds
|
||||||
@ -481,6 +285,7 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
||||||
var metrics = new List<object>();
|
var metrics = new List<object>();
|
||||||
|
|
||||||
|
// Collect metrics from buffer
|
||||||
lock (_metricsBuffer)
|
lock (_metricsBuffer)
|
||||||
{
|
{
|
||||||
foreach (var metric in _metricsBuffer)
|
foreach (var metric in _metricsBuffer)
|
||||||
@ -515,6 +320,85 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RecordMetric(string name, decimal value)
|
||||||
|
{
|
||||||
|
lock (_metricsBuffer)
|
||||||
|
{
|
||||||
|
if (_metricsBuffer.ContainsKey(name))
|
||||||
|
_metricsBuffer[name] += value;
|
||||||
|
else
|
||||||
|
_metricsBuffer[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Guid?> StartSessionAsync(string sessionIdentifier, string platform = "Telegram")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_botKey)) return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
||||||
|
var sessionData = new
|
||||||
|
{
|
||||||
|
SessionIdentifier = sessionIdentifier,
|
||||||
|
Platform = platform,
|
||||||
|
Language = "en",
|
||||||
|
Country = "",
|
||||||
|
IsAnonymous = true,
|
||||||
|
Metadata = new Dictionary<string, object>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(sessionData);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
_httpClient.DefaultRequestHeaders.Clear();
|
||||||
|
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"{apiUrl}/api/bots/sessions/start", content);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var responseJson = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonSerializer.Deserialize<SessionDto>(responseJson);
|
||||||
|
return result?.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to start session");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateSessionAsync(Guid sessionId, int? orderCount = null, int? messageCount = null, decimal? totalSpent = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_botKey)) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
||||||
|
var updateData = new
|
||||||
|
{
|
||||||
|
OrderCount = orderCount,
|
||||||
|
MessageCount = messageCount,
|
||||||
|
TotalSpent = totalSpent
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(updateData);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
_httpClient.DefaultRequestHeaders.Clear();
|
||||||
|
_httpClient.DefaultRequestHeaders.Add("X-Bot-Key", _botKey);
|
||||||
|
|
||||||
|
await _httpClient.PutAsync($"{apiUrl}/api/bots/sessions/{sessionId}", content);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to update session");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private int GetMetricType(string metricName)
|
private int GetMetricType(string metricName)
|
||||||
{
|
{
|
||||||
return metricName.ToLower() switch
|
return metricName.ToLower() switch
|
||||||
@ -539,11 +423,13 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
var telegramSettings = telegramElement.EnumerateObject().ToDictionary(p => p.Name, p => p.Value.ToString());
|
var telegramSettings = telegramElement.EnumerateObject().ToDictionary(p => p.Name, p => p.Value.ToString());
|
||||||
if (telegramSettings.TryGetValue("botToken", out var token))
|
if (telegramSettings.TryGetValue("botToken", out var token))
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(token) && token != _currentBotToken)
|
// Check if token has changed
|
||||||
|
if (!string.IsNullOrEmpty(token) && token != _lastKnownBotToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Bot token has changed. Updating bot...");
|
_logger.LogInformation("Bot token has changed. Updating bot...");
|
||||||
_currentBotToken = token;
|
_lastKnownBotToken = token;
|
||||||
|
|
||||||
|
// Update the TelegramBotService if available
|
||||||
if (_telegramBotService != null)
|
if (_telegramBotService != null)
|
||||||
{
|
{
|
||||||
await _telegramBotService.UpdateBotTokenAsync(token);
|
await _telegramBotService.UpdateBotTokenAsync(token);
|
||||||
@ -559,26 +445,73 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TelegramBotInfo?> ValidateTelegramTokenAsync(string botToken)
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_heartbeatTimer?.Dispose();
|
||||||
|
_metricsTimer?.Dispose();
|
||||||
|
_settingsSyncTimer?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> GetTelegramBotUsernameAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var botToken = _configuration["Telegram:BotToken"];
|
||||||
|
if (string.IsNullOrEmpty(botToken) || botToken == "YOUR_BOT_TOKEN_HERE")
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Bot token not configured in appsettings.json");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Telegram API to get bot info
|
||||||
var response = await _httpClient.GetAsync($"https://api.telegram.org/bot{botToken}/getMe");
|
var response = await _httpClient.GetAsync($"https://api.telegram.org/bot{botToken}/getMe");
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var responseJson = await response.Content.ReadAsStringAsync();
|
var responseJson = await response.Content.ReadAsStringAsync();
|
||||||
var result = JsonSerializer.Deserialize<TelegramGetMeResponse>(responseJson);
|
var result = JsonSerializer.Deserialize<TelegramGetMeResponse>(responseJson);
|
||||||
return result?.Result;
|
return result?.Result?.Username;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Telegram token validation failed: {StatusCode}", response.StatusCode);
|
_logger.LogWarning("Failed to get bot info from Telegram: {StatusCode}", response.StatusCode);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error validating Telegram token");
|
_logger.LogError(ex, "Error getting Telegram bot username");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<BotDto?> FindExistingBotByPlatformAsync(string platformUsername)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
||||||
|
const int telegramBotType = 0; // BotType.Telegram enum value
|
||||||
|
|
||||||
|
var response = await _httpClient.GetAsync($"{apiUrl}/api/bots/by-platform/{telegramBotType}/{platformUsername}");
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var responseJson = await response.Content.ReadAsStringAsync();
|
||||||
|
var bot = JsonSerializer.Deserialize<BotDto>(responseJson);
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
return null; // Bot not found - this is expected for first registration
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Failed to check for existing bot: {StatusCode}", response.StatusCode);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error finding existing bot by platform username");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -588,15 +521,28 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
||||||
|
var botToken = _configuration["Telegram:BotToken"];
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_telegramUsername) || string.IsNullOrEmpty(_botKey))
|
if (string.IsNullOrEmpty(botToken) || string.IsNullOrEmpty(_botKey))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Get bot info from Telegram
|
||||||
|
var telegramResponse = await _httpClient.GetAsync($"https://api.telegram.org/bot{botToken}/getMe");
|
||||||
|
if (!telegramResponse.IsSuccessStatusCode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var telegramJson = await telegramResponse.Content.ReadAsStringAsync();
|
||||||
|
var telegramResult = JsonSerializer.Deserialize<TelegramGetMeResponse>(telegramJson);
|
||||||
|
|
||||||
|
if (telegramResult?.Result == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Update platform info in LittleShop
|
||||||
var updateData = new
|
var updateData = new
|
||||||
{
|
{
|
||||||
PlatformUsername = _telegramUsername,
|
PlatformUsername = telegramResult.Result.Username,
|
||||||
PlatformDisplayName = _telegramDisplayName ?? _telegramUsername,
|
PlatformDisplayName = telegramResult.Result.FirstName ?? telegramResult.Result.Username,
|
||||||
PlatformId = _telegramId
|
PlatformId = telegramResult.Result.Id.ToString()
|
||||||
};
|
};
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(updateData);
|
var json = JsonSerializer.Serialize(updateData);
|
||||||
@ -609,7 +555,7 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Updated platform info for @{Username}", _telegramUsername);
|
_logger.LogInformation("Updated platform info for @{Username}", telegramResult.Result.Username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -618,34 +564,21 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplySettingsAsync(Dictionary<string, object> settings)
|
// DTOs for API responses
|
||||||
|
private class BotRegistrationResponse
|
||||||
{
|
{
|
||||||
// Apply settings to runtime configuration
|
public Guid BotId { get; set; }
|
||||||
// In production, this would update various services based on settings
|
public string BotKey { get; set; } = string.Empty;
|
||||||
_logger.LogInformation("Applying {Count} setting categories", settings.Count);
|
public string Name { get; set; } = string.Empty;
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveConfigurationAsync()
|
private class BotDto
|
||||||
{
|
{
|
||||||
// In production, save BotKey and WebhookSecret to secure storage
|
public Guid Id { get; set; }
|
||||||
// For now, just log
|
public string Name { get; set; } = string.Empty;
|
||||||
_logger.LogInformation("Configuration saved. BotKey: {KeyPrefix}...",
|
public string BotKey { get; set; } = string.Empty;
|
||||||
_botKey?.Substring(0, Math.Min(8, _botKey?.Length ?? 0)) ?? "null");
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_heartbeatTimer?.Dispose();
|
|
||||||
_metricsTimer?.Dispose();
|
|
||||||
_settingsSyncTimer?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region DTOs
|
|
||||||
|
|
||||||
private class SessionDto
|
private class SessionDto
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
@ -667,6 +600,5 @@ public class BotManagerService : IHostedService, IDisposable
|
|||||||
public bool? CanReadAllGroupMessages { get; set; }
|
public bool? CanReadAllGroupMessages { get; set; }
|
||||||
public bool? SupportsInlineQueries { get; set; }
|
public bool? SupportsInlineQueries { get; set; }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
@ -1,200 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace TeleBot.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Background service that monitors LittleShop connectivity.
|
|
||||||
/// Triggers application shutdown after consecutive connectivity failures.
|
|
||||||
/// </summary>
|
|
||||||
public class LivenessService : BackgroundService
|
|
||||||
{
|
|
||||||
private readonly ILogger<LivenessService> _logger;
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly IHostApplicationLifetime _applicationLifetime;
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
private readonly BotManagerService _botManagerService;
|
|
||||||
|
|
||||||
private int _consecutiveFailures;
|
|
||||||
private DateTime? _firstFailureAt;
|
|
||||||
|
|
||||||
public LivenessService(
|
|
||||||
ILogger<LivenessService> logger,
|
|
||||||
IConfiguration configuration,
|
|
||||||
IHostApplicationLifetime applicationLifetime,
|
|
||||||
HttpClient httpClient,
|
|
||||||
BotManagerService botManagerService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_configuration = configuration;
|
|
||||||
_applicationLifetime = applicationLifetime;
|
|
||||||
_httpClient = httpClient;
|
|
||||||
_botManagerService = botManagerService;
|
|
||||||
_consecutiveFailures = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("LivenessService started");
|
|
||||||
|
|
||||||
// Wait for bot to be initialized before starting liveness checks
|
|
||||||
while (!_botManagerService.IsInitialized && !stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stoppingToken.IsCancellationRequested)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_logger.LogInformation("Bot initialized, starting LittleShop connectivity monitoring");
|
|
||||||
|
|
||||||
var checkIntervalSeconds = _configuration.GetValue<int>("Liveness:CheckIntervalSeconds", 30);
|
|
||||||
var failureThreshold = _configuration.GetValue<int>("Liveness:FailureThreshold", 10);
|
|
||||||
|
|
||||||
_logger.LogInformation("Liveness configuration: CheckInterval={CheckInterval}s, FailureThreshold={Threshold}",
|
|
||||||
checkIntervalSeconds, failureThreshold);
|
|
||||||
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(checkIntervalSeconds), stoppingToken);
|
|
||||||
|
|
||||||
var isConnected = await CheckLittleShopConnectivityAsync(stoppingToken);
|
|
||||||
|
|
||||||
if (isConnected)
|
|
||||||
{
|
|
||||||
if (_consecutiveFailures > 0)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("LittleShop connectivity restored after {Failures} failures", _consecutiveFailures);
|
|
||||||
}
|
|
||||||
_consecutiveFailures = 0;
|
|
||||||
_firstFailureAt = null;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_consecutiveFailures++;
|
|
||||||
_firstFailureAt ??= DateTime.UtcNow;
|
|
||||||
|
|
||||||
var totalDowntime = DateTime.UtcNow - _firstFailureAt.Value;
|
|
||||||
|
|
||||||
if (_consecutiveFailures >= failureThreshold)
|
|
||||||
{
|
|
||||||
_logger.LogCritical(
|
|
||||||
"LittleShop unreachable for {Downtime:F0} seconds ({Failures} consecutive failures). Initiating shutdown.",
|
|
||||||
totalDowntime.TotalSeconds, _consecutiveFailures);
|
|
||||||
|
|
||||||
// Trigger application shutdown
|
|
||||||
_applicationLifetime.StopApplication();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (_consecutiveFailures == 1)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("LittleShop connectivity check failed. Failure 1/{Threshold}", failureThreshold);
|
|
||||||
}
|
|
||||||
else if (_consecutiveFailures % 3 == 0) // Log every 3rd failure to avoid spam
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"LittleShop connectivity check failed. Failure {Failures}/{Threshold}. Downtime: {Downtime:F0}s",
|
|
||||||
_consecutiveFailures, failureThreshold, totalDowntime.TotalSeconds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// Normal shutdown
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error during liveness check");
|
|
||||||
_consecutiveFailures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("LivenessService stopped");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> CheckLittleShopConnectivityAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var apiUrl = _configuration["LittleShop:ApiUrl"];
|
|
||||||
if (string.IsNullOrEmpty(apiUrl))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("LittleShop:ApiUrl not configured");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var botKey = _botManagerService.BotKey;
|
|
||||||
if (string.IsNullOrEmpty(botKey))
|
|
||||||
{
|
|
||||||
// Not initialized yet, skip check
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the health endpoint or a lightweight endpoint
|
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"{apiUrl}/health");
|
|
||||||
request.Headers.Add("X-Bot-Key", botKey);
|
|
||||||
|
|
||||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(10)); // 10 second timeout
|
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request, cts.Token);
|
|
||||||
|
|
||||||
return response.IsSuccessStatusCode;
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
// Timeout
|
|
||||||
_logger.LogDebug("LittleShop connectivity check timed out");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("LittleShop connectivity check failed: {Message}", ex.Message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "LittleShop connectivity check error");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current liveness status
|
|
||||||
/// </summary>
|
|
||||||
public LivenessStatus GetStatus()
|
|
||||||
{
|
|
||||||
var failureThreshold = _configuration.GetValue<int>("Liveness:FailureThreshold", 10);
|
|
||||||
|
|
||||||
return new LivenessStatus
|
|
||||||
{
|
|
||||||
IsHealthy = _consecutiveFailures == 0,
|
|
||||||
ConsecutiveFailures = _consecutiveFailures,
|
|
||||||
FailureThreshold = failureThreshold,
|
|
||||||
FirstFailureAt = _firstFailureAt,
|
|
||||||
DowntimeSeconds = _firstFailureAt.HasValue
|
|
||||||
? (DateTime.UtcNow - _firstFailureAt.Value).TotalSeconds
|
|
||||||
: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the current liveness status
|
|
||||||
/// </summary>
|
|
||||||
public class LivenessStatus
|
|
||||||
{
|
|
||||||
public bool IsHealthy { get; set; }
|
|
||||||
public int ConsecutiveFailures { get; set; }
|
|
||||||
public int FailureThreshold { get; set; }
|
|
||||||
public DateTime? FirstFailureAt { get; set; }
|
|
||||||
public double DowntimeSeconds { get; set; }
|
|
||||||
}
|
|
||||||
@ -33,12 +33,6 @@ namespace TeleBot
|
|||||||
private ITelegramBotClient? _botClient;
|
private ITelegramBotClient? _botClient;
|
||||||
private CancellationTokenSource? _cancellationTokenSource;
|
private CancellationTokenSource? _cancellationTokenSource;
|
||||||
private string? _currentBotToken;
|
private string? _currentBotToken;
|
||||||
private bool _isRunning;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates whether the Telegram bot polling is currently running
|
|
||||||
/// </summary>
|
|
||||||
public bool IsRunning => _isRunning && _botClient != null;
|
|
||||||
|
|
||||||
public TelegramBotService(
|
public TelegramBotService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
@ -126,8 +120,6 @@ namespace TeleBot
|
|||||||
cancellationToken: _cancellationTokenSource.Token
|
cancellationToken: _cancellationTokenSource.Token
|
||||||
);
|
);
|
||||||
|
|
||||||
_isRunning = true;
|
|
||||||
|
|
||||||
var me = await _botClient.GetMeAsync(cancellationToken);
|
var me = await _botClient.GetMeAsync(cancellationToken);
|
||||||
_logger.LogInformation("Bot started: @{Username} ({Id})", me.Username, me.Id);
|
_logger.LogInformation("Bot started: @{Username} ({Id})", me.Username, me.Id);
|
||||||
|
|
||||||
@ -140,7 +132,6 @@ namespace TeleBot
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_isRunning = false;
|
|
||||||
_cancellationTokenSource?.Cancel();
|
_cancellationTokenSource?.Cancel();
|
||||||
_logger.LogInformation("Bot stopped");
|
_logger.LogInformation("Bot stopped");
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@ -282,8 +273,6 @@ namespace TeleBot
|
|||||||
cancellationToken: _cancellationTokenSource.Token
|
cancellationToken: _cancellationTokenSource.Token
|
||||||
);
|
);
|
||||||
|
|
||||||
_isRunning = true;
|
|
||||||
|
|
||||||
var me = await _botClient.GetMeAsync();
|
var me = await _botClient.GetMeAsync();
|
||||||
_logger.LogInformation("Bot restarted with new token: @{Username} ({Id})", me.Username, me.Id);
|
_logger.LogInformation("Bot restarted with new token: @{Username} ({Id})", me.Username, me.Id);
|
||||||
|
|
||||||
|
|||||||
@ -2,29 +2,18 @@
|
|||||||
"BotInfo": {
|
"BotInfo": {
|
||||||
"Name": "LittleShop TeleBot",
|
"Name": "LittleShop TeleBot",
|
||||||
"Description": "Privacy-focused e-commerce Telegram bot",
|
"Description": "Privacy-focused e-commerce Telegram bot",
|
||||||
"Version": "1.0.0",
|
"Version": "1.0.0"
|
||||||
"InstanceId": ""
|
|
||||||
},
|
},
|
||||||
"BotManager": {
|
"BotManager": {
|
||||||
"ApiKey": "",
|
"ApiKey": "",
|
||||||
"Comment": "Populated by LittleShop during discovery initialization"
|
"Comment": "This will be populated after first registration with admin panel"
|
||||||
},
|
},
|
||||||
"Telegram": {
|
"Telegram": {
|
||||||
"BotToken": "",
|
"BotToken": "8496279616:AAE7kV_riICbWxn6-MPFqcrWx7K8b4_NKq0",
|
||||||
"AdminChatId": "",
|
"AdminChatId": "123456789",
|
||||||
"WebhookUrl": "",
|
"WebhookUrl": "",
|
||||||
"UseWebhook": false,
|
"UseWebhook": false,
|
||||||
"Comment": "Bot token pushed from LittleShop during configuration"
|
"Comment": "Bot token will be fetched from admin panel API if BotManager:ApiKey is set"
|
||||||
},
|
|
||||||
"Discovery": {
|
|
||||||
"Secret": "CHANGE_THIS_SHARED_SECRET_32_CHARS",
|
|
||||||
"Enabled": true,
|
|
||||||
"Comment": "Shared secret for LittleShop discovery. Must match LittleShop BotDiscovery:SharedSecret"
|
|
||||||
},
|
|
||||||
"Liveness": {
|
|
||||||
"CheckIntervalSeconds": 30,
|
|
||||||
"FailureThreshold": 10,
|
|
||||||
"Comment": "Shutdown after 10 consecutive failures (5 minutes total)"
|
|
||||||
},
|
},
|
||||||
"Webhook": {
|
"Webhook": {
|
||||||
"Secret": "",
|
"Secret": "",
|
||||||
|
|||||||
@ -1,93 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Docker Registry Configuration
|
|
||||||
REGISTRY="10.8.0.1:5000"
|
|
||||||
TELESHOP_IMAGE="teleshop"
|
|
||||||
TELEBOT_IMAGE="telebot"
|
|
||||||
VERSION_TAG="clean-slate"
|
|
||||||
|
|
||||||
echo "=================================="
|
|
||||||
echo "TeleShop & TeleBot Docker Deployment"
|
|
||||||
echo "Registry: $REGISTRY"
|
|
||||||
echo "=================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check if Docker is available
|
|
||||||
if ! command -v docker &> /dev/null; then
|
|
||||||
echo "❌ Error: Docker is not installed or not in PATH"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if registry is accessible
|
|
||||||
echo "🔍 Testing registry connectivity..."
|
|
||||||
if curl -s "http://$REGISTRY/v2/_catalog" > /dev/null 2>&1; then
|
|
||||||
echo "✅ Registry is accessible at http://$REGISTRY"
|
|
||||||
else
|
|
||||||
echo "❌ Error: Cannot connect to registry at $REGISTRY"
|
|
||||||
echo "Please ensure the registry is running and accessible"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📦 Building TeleShop image..."
|
|
||||||
docker build -f Dockerfile -t $TELESHOP_IMAGE:latest . || {
|
|
||||||
echo "❌ Failed to build TeleShop image"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "✅ TeleShop image built successfully"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📦 Building TeleBot image..."
|
|
||||||
docker build -f Dockerfile.telebot -t $TELEBOT_IMAGE:latest . || {
|
|
||||||
echo "❌ Failed to build TeleBot image"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "✅ TeleBot image built successfully"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🏷️ Tagging images for registry..."
|
|
||||||
docker tag $TELESHOP_IMAGE:latest $REGISTRY/$TELESHOP_IMAGE:latest
|
|
||||||
docker tag $TELESHOP_IMAGE:latest $REGISTRY/$TELESHOP_IMAGE:$VERSION_TAG
|
|
||||||
docker tag $TELEBOT_IMAGE:latest $REGISTRY/$TELEBOT_IMAGE:latest
|
|
||||||
docker tag $TELEBOT_IMAGE:latest $REGISTRY/$TELEBOT_IMAGE:$VERSION_TAG
|
|
||||||
echo "✅ Images tagged successfully"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🚀 Pushing TeleShop to registry..."
|
|
||||||
docker push $REGISTRY/$TELESHOP_IMAGE:latest || {
|
|
||||||
echo "❌ Failed to push TeleShop:latest"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
docker push $REGISTRY/$TELESHOP_IMAGE:$VERSION_TAG || {
|
|
||||||
echo "❌ Failed to push TeleShop:$VERSION_TAG"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "✅ TeleShop pushed successfully"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🚀 Pushing TeleBot to registry..."
|
|
||||||
docker push $REGISTRY/$TELEBOT_IMAGE:latest || {
|
|
||||||
echo "❌ Failed to push TeleBot:latest"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
docker push $REGISTRY/$TELEBOT_IMAGE:$VERSION_TAG || {
|
|
||||||
echo "❌ Failed to push TeleBot:$VERSION_TAG"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "✅ TeleBot pushed successfully"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=================================="
|
|
||||||
echo "✅ Deployment Complete!"
|
|
||||||
echo "=================================="
|
|
||||||
echo ""
|
|
||||||
echo "Images pushed to registry:"
|
|
||||||
echo " - $REGISTRY/$TELESHOP_IMAGE:latest"
|
|
||||||
echo " - $REGISTRY/$TELESHOP_IMAGE:$VERSION_TAG"
|
|
||||||
echo " - $REGISTRY/$TELEBOT_IMAGE:latest"
|
|
||||||
echo " - $REGISTRY/$TELEBOT_IMAGE:$VERSION_TAG"
|
|
||||||
echo ""
|
|
||||||
echo "Verify with:"
|
|
||||||
echo " curl http://$REGISTRY/v2/_catalog"
|
|
||||||
echo ""
|
|
||||||
@ -11,7 +11,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
- ASPNETCORE_URLS=http://+:5000
|
- ASPNETCORE_URLS=http://+:5000
|
||||||
- ConnectionStrings__DefaultConnection=Data Source=/app/data/teleshop-prod.db
|
- ConnectionStrings__DefaultConnection=Data Source=/app/data/littleshop-prod.db
|
||||||
- Jwt__Key=LittleShop-Production-JWT-SecretKey-32Characters-2025
|
- Jwt__Key=LittleShop-Production-JWT-SecretKey-32Characters-2025
|
||||||
- Jwt__Issuer=LittleShop
|
- Jwt__Issuer=LittleShop
|
||||||
- Jwt__Audience=LittleShop
|
- Jwt__Audience=LittleShop
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user