Initial commit of LittleShop project (excluding large archives)
- BTCPay Server integration - TeleBot Telegram bot - Review system - Admin area - Docker deployment configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
22
TeleBot/.env.example
Normal file
22
TeleBot/.env.example
Normal file
@@ -0,0 +1,22 @@
|
||||
# Telegram Bot Configuration
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||
TELEGRAM_ADMIN_CHAT_ID=your_admin_chat_id_here
|
||||
|
||||
# LittleShop API Configuration
|
||||
LITTLESHOP_API_URL=https://your-littleshop-domain.com
|
||||
LITTLESHOP_USERNAME=admin
|
||||
LITTLESHOP_PASSWORD=your_secure_admin_password
|
||||
|
||||
# Security
|
||||
DATABASE_ENCRYPTION_KEY=your_secure_32_character_encryption_key_here
|
||||
|
||||
# Redis Configuration (optional)
|
||||
REDIS_ENABLED=false
|
||||
REDIS_CONNECTION_STRING=redis:6379
|
||||
REDIS_PASSWORD=your_secure_redis_password
|
||||
|
||||
# Background Jobs (optional)
|
||||
HANGFIRE_ENABLED=false
|
||||
|
||||
# Additional Settings
|
||||
TZ=UTC
|
||||
200
TeleBot/DEPLOYMENT.md
Normal file
200
TeleBot/DEPLOYMENT.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# TeleBot Docker Deployment on Portainer
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Portainer-01** instance running
|
||||
2. **LittleShop API** deployed and accessible
|
||||
3. **Telegram Bot Token** from @BotFather
|
||||
4. **Admin Chat ID** for notifications
|
||||
|
||||
## Quick Deployment Steps
|
||||
|
||||
### 1. Prepare Environment Variables
|
||||
|
||||
Copy `.env.example` to `.env` and configure:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` with your values:
|
||||
- `TELEGRAM_BOT_TOKEN` - Your bot token from @BotFather
|
||||
- `TELEGRAM_ADMIN_CHAT_ID` - Your Telegram chat ID for admin notifications
|
||||
- `LITTLESHOP_API_URL` - URL to your LittleShop API instance
|
||||
- `DATABASE_ENCRYPTION_KEY` - 32-character secure key for database encryption
|
||||
|
||||
### 2. Deploy via Portainer UI
|
||||
|
||||
1. **Access Portainer** at your portainer-01 URL
|
||||
2. **Navigate to Stacks** → **Add Stack**
|
||||
3. **Stack Name**: `littleshop-telebot`
|
||||
4. **Build Method**: Repository
|
||||
5. **Repository URL**: Your git repository URL
|
||||
6. **Repository Reference**: main/master
|
||||
7. **Compose Path**: `TeleBot/docker-compose.yml`
|
||||
8. **Environment Variables**: Upload your `.env` file or add manually
|
||||
|
||||
### 3. Deploy via Portainer API (Alternative)
|
||||
|
||||
```bash
|
||||
# Upload stack via Portainer API
|
||||
curl -X POST \
|
||||
http://portainer-01:9000/api/stacks \
|
||||
-H "X-API-Key: YOUR_PORTAINER_API_KEY" \
|
||||
-F "Name=littleshop-telebot" \
|
||||
-F "StackFileContent=@docker-compose.yml" \
|
||||
-F "Env=@.env"
|
||||
```
|
||||
|
||||
### 4. Manual Docker Deploy (If not using Portainer)
|
||||
|
||||
```bash
|
||||
# Build and start services
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f telebot
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## Configuration Details
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | `7880403661:AAGma1wAyoHsmG45iO6VvHCqzimhJX1pp14` |
|
||||
| `TELEGRAM_ADMIN_CHAT_ID` | Admin chat ID for notifications | `123456789` |
|
||||
| `LITTLESHOP_API_URL` | LittleShop API endpoint | `https://api.yourshop.com` |
|
||||
| `DATABASE_ENCRYPTION_KEY` | 32-char encryption key | `your_secure_32_character_key_here` |
|
||||
|
||||
### Optional Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `REDIS_ENABLED` | `false` | Enable Redis caching |
|
||||
| `HANGFIRE_ENABLED` | `false` | Enable background job processing |
|
||||
| `LITTLESHOP_USERNAME` | `admin` | API admin username |
|
||||
| `LITTLESHOP_PASSWORD` | `admin` | API admin password |
|
||||
|
||||
## Networking
|
||||
|
||||
The stack creates a `littleshop-network` bridge network for service communication.
|
||||
|
||||
### Connecting to External LittleShop API
|
||||
|
||||
If your LittleShop API runs on the host or different container:
|
||||
- Use `host.docker.internal:5001` for same-host deployment
|
||||
- Use `https://your-api-domain.com` for external API
|
||||
|
||||
## Persistent Storage
|
||||
|
||||
### Volumes Created
|
||||
|
||||
- `littleshop-telebot-data` - Bot database and application data
|
||||
- `littleshop-telebot-logs` - Application logs
|
||||
- `littleshop-redis-data` - Redis data (if enabled)
|
||||
|
||||
### Data Backup
|
||||
|
||||
```bash
|
||||
# Backup bot data
|
||||
docker run --rm -v littleshop-telebot-data:/data -v $(pwd):/backup alpine tar czf /backup/telebot-backup.tar.gz /data
|
||||
|
||||
# Restore bot data
|
||||
docker run --rm -v littleshop-telebot-data:/data -v $(pwd):/backup alpine tar xzf /backup/telebot-backup.tar.gz -C /
|
||||
```
|
||||
|
||||
## Health Monitoring
|
||||
|
||||
The bot includes health checks:
|
||||
- **Endpoint**: Container process check
|
||||
- **Interval**: 30 seconds
|
||||
- **Timeout**: 10 seconds
|
||||
- **Retries**: 3
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Database Encryption**: Use a strong 32-character encryption key
|
||||
2. **Redis Password**: Set secure Redis password if enabled
|
||||
3. **Network Isolation**: Bot runs in isolated Docker network
|
||||
4. **Non-Root User**: Container runs as non-root `telebot` user
|
||||
5. **Secret Management**: Use Docker secrets or external secret management
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check Container Status
|
||||
```bash
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
# All services
|
||||
docker-compose logs
|
||||
|
||||
# Bot only
|
||||
docker-compose logs telebot
|
||||
|
||||
# Follow logs
|
||||
docker-compose logs -f telebot
|
||||
```
|
||||
|
||||
### Access Container Shell
|
||||
```bash
|
||||
docker-compose exec telebot /bin/bash
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Bot Token Invalid**: Verify token with @BotFather
|
||||
2. **API Connection Failed**: Check `LITTLESHOP_API_URL` and network connectivity
|
||||
3. **Permission Denied**: Ensure proper file permissions on volumes
|
||||
4. **Build Failed**: Check Docker build context includes LittleShop.Client project
|
||||
|
||||
## Monitoring & Maintenance
|
||||
|
||||
### Log Rotation
|
||||
Logs are automatically rotated:
|
||||
- Max size: 10MB per file
|
||||
- Max files: 3 files retained
|
||||
|
||||
### Resource Usage
|
||||
Typical resource requirements:
|
||||
- **CPU**: 0.5 cores
|
||||
- **Memory**: 512MB
|
||||
- **Storage**: 1GB for data + logs
|
||||
|
||||
### Updates
|
||||
To update the bot:
|
||||
```bash
|
||||
# Pull latest changes
|
||||
git pull
|
||||
|
||||
# Rebuild and restart
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
## Integration with Portainer
|
||||
|
||||
### Stack Templates
|
||||
Create a custom template in Portainer for easy redeployment:
|
||||
|
||||
1. **Portainer** → **App Templates** → **Custom Templates**
|
||||
2. **Add Template** with docker-compose.yml content
|
||||
3. **Variables** section with environment variable definitions
|
||||
|
||||
### Webhooks
|
||||
Enable webhooks for automated deployments:
|
||||
1. **Stack** → **Webhooks** → **Create Webhook**
|
||||
2. Use webhook URL in CI/CD pipeline for automated updates
|
||||
|
||||
## Support
|
||||
|
||||
For deployment issues:
|
||||
1. Check container logs: `docker-compose logs telebot`
|
||||
2. Verify environment variables in Portainer stack
|
||||
3. Test API connectivity from container
|
||||
4. Review bot registration in LittleShop admin panel
|
||||
@@ -1,47 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TeleBot
|
||||
{
|
||||
public class BotScript
|
||||
{
|
||||
public string WelcomeText { get; set; }
|
||||
public Dictionary<Guid, BotOption> Questions { get; internal set; } = new Dictionary<Guid, BotOption>();
|
||||
public Dictionary<Guid, string> Answers { get; internal set; } = new Dictionary<Guid, string>();
|
||||
public int Stage { get; set; }
|
||||
|
||||
public static BotScript CreateBotScript(string welcomeText)
|
||||
{
|
||||
var bs = new BotScript();
|
||||
bs.WelcomeText = welcomeText;
|
||||
|
||||
return bs;
|
||||
}
|
||||
|
||||
public void AddScaledQuestion(string question)
|
||||
{
|
||||
AddQuestion(question, ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]);
|
||||
}
|
||||
public void AddQuestion(string question, string[] answers)
|
||||
{
|
||||
var q = new BotOption();
|
||||
q.Order = Questions.Count() + 1;
|
||||
q.Text = question;
|
||||
q.Options = answers;
|
||||
Questions.Add(q.Id,q);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class BotOption
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public int Order { get; set; }
|
||||
public string Text { get; set; }
|
||||
public string[] Options { get; set; }
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TeleBot
|
||||
{
|
||||
public class BotScript
|
||||
{
|
||||
public string WelcomeText { get; set; }
|
||||
public Dictionary<Guid, BotOption> Questions { get; internal set; } = new Dictionary<Guid, BotOption>();
|
||||
public Dictionary<Guid, string> Answers { get; internal set; } = new Dictionary<Guid, string>();
|
||||
public int Stage { get; set; }
|
||||
|
||||
public static BotScript CreateBotScript(string welcomeText)
|
||||
{
|
||||
var bs = new BotScript();
|
||||
bs.WelcomeText = welcomeText;
|
||||
|
||||
return bs;
|
||||
}
|
||||
|
||||
public void AddScaledQuestion(string question)
|
||||
{
|
||||
AddQuestion(question, ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]);
|
||||
}
|
||||
public void AddQuestion(string question, string[] answers)
|
||||
{
|
||||
var q = new BotOption();
|
||||
q.Order = Questions.Count() + 1;
|
||||
q.Text = question;
|
||||
q.Options = answers;
|
||||
Questions.Add(q.Id,q);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class BotOption
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public int Order { get; set; }
|
||||
public string Text { get; set; }
|
||||
public string[] Options { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
144
TeleBot/TeleBot/CAROUSEL_FEATURE.md
Normal file
144
TeleBot/TeleBot/CAROUSEL_FEATURE.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Product Image Carousel Feature
|
||||
|
||||
## Overview
|
||||
The TeleBot now supports displaying products with images in beautiful carousel format, making the shopping experience more visual and engaging.
|
||||
|
||||
## Features
|
||||
|
||||
### 🖼️ Image Carousels
|
||||
- **Product Images**: Automatically displays product images from the API
|
||||
- **Media Groups**: Groups up to 10 products per carousel for optimal viewing
|
||||
- **Image Caching**: Downloads and caches images locally for faster loading
|
||||
- **Fallback Support**: Gracefully falls back to text-only display if images fail
|
||||
|
||||
### 🛍️ Enhanced Browsing
|
||||
- **New Command**: `/products` - Browse all products with images
|
||||
- **Category Support**: `/products <categoryId>` - Browse specific category with images
|
||||
- **Pagination**: Navigate through multiple pages of products
|
||||
- **Single Product View**: Individual products shown with high-quality images
|
||||
|
||||
### 🎨 User Experience
|
||||
- **Visual Appeal**: Products displayed with images, names, prices, and descriptions
|
||||
- **Interactive Buttons**: Easy navigation and product selection
|
||||
- **Responsive Design**: Optimized for mobile and desktop viewing
|
||||
- **Fast Loading**: Cached images load instantly
|
||||
|
||||
## Usage
|
||||
|
||||
### Commands
|
||||
```
|
||||
/products - View all products with images
|
||||
/products <categoryId> - View products in specific category
|
||||
/browse - Browse categories (existing functionality)
|
||||
```
|
||||
|
||||
### Menu Options
|
||||
- **🖼️ View Products with Images** - Main menu option for image browsing
|
||||
- **🛍️ Browse Categories** - Traditional category browsing
|
||||
|
||||
### Callback Data Format
|
||||
```
|
||||
products:page:<pageNumber> - Pagination for all products
|
||||
products:<categoryId>:<page> - Pagination for specific category
|
||||
product:<productId> - View individual product with image
|
||||
```
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Services
|
||||
- **ProductCarouselService**: Handles image downloading, caching, and carousel generation
|
||||
- **Image Caching**: Local file system caching in `image_cache/` directory
|
||||
- **HTTP Client**: Configured for downloading product images
|
||||
- **Error Handling**: Graceful fallback to text-only display
|
||||
|
||||
### Image Processing
|
||||
- **Format Support**: JPG, PNG, WebP, and other common formats
|
||||
- **Validation**: Checks image URLs before downloading
|
||||
- **Caching Strategy**: Files cached with product and photo IDs
|
||||
- **Memory Management**: Streams images efficiently
|
||||
|
||||
### Telegram Integration
|
||||
- **Media Groups**: Uses `SendMediaGroupAsync` for carousels
|
||||
- **Photo Messages**: Individual products with `SendPhotoAsync`
|
||||
- **Inline Keyboards**: Navigation and interaction buttons
|
||||
- **Error Recovery**: Fallback to text messages if media fails
|
||||
|
||||
## Configuration
|
||||
|
||||
### Required Settings
|
||||
```json
|
||||
{
|
||||
"LittleShop": {
|
||||
"ApiUrl": "https://your-api-url.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Optional Settings
|
||||
```json
|
||||
{
|
||||
"Features": {
|
||||
"EnableQRCodes": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## File Structure
|
||||
```
|
||||
TeleBot/
|
||||
├── Services/
|
||||
│ └── ProductCarouselService.cs # Main carousel service
|
||||
├── Handlers/
|
||||
│ ├── CommandHandler.cs # Updated with /products command
|
||||
│ └── CallbackHandler.cs # Updated with carousel callbacks
|
||||
├── UI/
|
||||
│ ├── MenuBuilder.cs # Updated with new menu options
|
||||
│ └── MessageFormatter.cs # Updated with carousel support
|
||||
└── image_cache/ # Local image cache directory
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### For Users
|
||||
- **Visual Shopping**: See products before buying
|
||||
- **Better Experience**: More engaging than text-only browsing
|
||||
- **Faster Navigation**: Quick access to product images
|
||||
- **Mobile Friendly**: Optimized for mobile devices
|
||||
|
||||
### For Business
|
||||
- **Higher Conversion**: Visual products increase sales
|
||||
- **Professional Look**: Modern, polished appearance
|
||||
- **User Engagement**: More time spent browsing
|
||||
- **Competitive Edge**: Stand out from text-only bots
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
- **Image Optimization**: Automatic resizing and compression
|
||||
- **Lazy Loading**: Load images on demand
|
||||
- **Multiple Images**: Support for product galleries
|
||||
- **Image Search**: Search products by visual similarity
|
||||
- **Video Support**: Product videos in carousels
|
||||
|
||||
### Performance Improvements
|
||||
- **CDN Integration**: Use CDN for image delivery
|
||||
- **Progressive Loading**: Show low-res images first
|
||||
- **Batch Processing**: Optimize multiple image downloads
|
||||
- **Memory Optimization**: Better memory management
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
1. **Images Not Loading**: Check API image URLs and network connectivity
|
||||
2. **Slow Performance**: Clear image cache or check disk space
|
||||
3. **Memory Usage**: Monitor cache size and implement cleanup
|
||||
4. **API Errors**: Verify LittleShop API configuration
|
||||
|
||||
### Debug Information
|
||||
- Check logs for image download errors
|
||||
- Monitor cache directory size
|
||||
- Verify product photo data from API
|
||||
- Test with different image formats
|
||||
|
||||
## Support
|
||||
For issues or questions about the carousel feature, check the logs or contact the development team.
|
||||
53
TeleBot/TeleBot/Dockerfile
Normal file
53
TeleBot/TeleBot/Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
||||
# Use the official .NET 9.0 runtime as base image
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
# Use the SDK image for building
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy project files and dependencies
|
||||
COPY ["TeleBot/TeleBot/TeleBot.csproj", "TeleBot/TeleBot/"]
|
||||
COPY ["LittleShop.Client/LittleShop.Client.csproj", "LittleShop.Client/"]
|
||||
|
||||
# Restore dependencies
|
||||
RUN dotnet restore "TeleBot/TeleBot/TeleBot.csproj"
|
||||
|
||||
# Copy all source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
WORKDIR "/src/TeleBot/TeleBot"
|
||||
RUN dotnet build "TeleBot.csproj" -c Release -o /app/build
|
||||
|
||||
# Publish the application
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "TeleBot.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Final runtime image
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p logs
|
||||
RUN mkdir -p data
|
||||
|
||||
# Copy published application
|
||||
COPY --from=publish /app/publish .
|
||||
|
||||
# Set environment variables
|
||||
ENV DOTNET_ENVIRONMENT=Production
|
||||
ENV ASPNETCORE_URLS=
|
||||
ENV TZ=UTC
|
||||
|
||||
# Create non-root user for security
|
||||
RUN adduser --disabled-password --gecos '' --shell /bin/bash --home /app telebot
|
||||
RUN chown -R telebot:telebot /app
|
||||
USER telebot
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD pgrep -f "dotnet.*TeleBot" > /dev/null || exit 1
|
||||
|
||||
# Run the application
|
||||
ENTRYPOINT ["dotnet", "TeleBot.dll"]
|
||||
@@ -5,7 +5,9 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QRCoder;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Exceptions;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
using TeleBot.Models;
|
||||
using TeleBot.Services;
|
||||
using TeleBot.UI;
|
||||
@@ -22,6 +24,7 @@ namespace TeleBot.Handlers
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly ILittleShopService _shopService;
|
||||
private readonly IPrivacyService _privacyService;
|
||||
private readonly IProductCarouselService _carouselService;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<CallbackHandler> _logger;
|
||||
|
||||
@@ -29,12 +32,14 @@ namespace TeleBot.Handlers
|
||||
ISessionManager sessionManager,
|
||||
ILittleShopService shopService,
|
||||
IPrivacyService privacyService,
|
||||
IProductCarouselService carouselService,
|
||||
IConfiguration configuration,
|
||||
ILogger<CallbackHandler> logger)
|
||||
{
|
||||
_sessionManager = sessionManager;
|
||||
_shopService = shopService;
|
||||
_privacyService = privacyService;
|
||||
_carouselService = carouselService;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -45,11 +50,13 @@ namespace TeleBot.Handlers
|
||||
return;
|
||||
|
||||
var session = await _sessionManager.GetOrCreateSessionAsync(callbackQuery.From.Id);
|
||||
bool callbackAnswered = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Answer callback to remove loading state
|
||||
// Answer callback immediately to prevent timeout
|
||||
await bot.AnswerCallbackQueryAsync(callbackQuery.Id);
|
||||
callbackAnswered = true;
|
||||
|
||||
var data = callbackQuery.Data.Split(':');
|
||||
var action = data[0];
|
||||
@@ -69,7 +76,14 @@ namespace TeleBot.Handlers
|
||||
break;
|
||||
|
||||
case "products":
|
||||
await HandleProductList(bot, callbackQuery.Message, session, data);
|
||||
if (data.Length > 1 && data[1] == "page")
|
||||
{
|
||||
await HandleProductsPage(bot, callbackQuery.Message, session, data);
|
||||
}
|
||||
else
|
||||
{
|
||||
await HandleProductList(bot, callbackQuery.Message, session, data);
|
||||
}
|
||||
break;
|
||||
|
||||
case "product":
|
||||
@@ -154,11 +168,24 @@ namespace TeleBot.Handlers
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error handling callback {Data}", callbackQuery.Data);
|
||||
await bot.AnswerCallbackQueryAsync(
|
||||
callbackQuery.Id,
|
||||
"An error occurred. Please try again.",
|
||||
showAlert: true
|
||||
);
|
||||
|
||||
// Only try to answer callback if not already answered
|
||||
if (!callbackAnswered)
|
||||
{
|
||||
try
|
||||
{
|
||||
await bot.AnswerCallbackQueryAsync(
|
||||
callbackQuery.Id,
|
||||
"An error occurred. Please try again.",
|
||||
showAlert: true
|
||||
);
|
||||
}
|
||||
catch (ApiRequestException apiEx) when (apiEx.Message.Contains("query is too old"))
|
||||
{
|
||||
// Callback already expired, ignore
|
||||
_logger.LogDebug("Callback query already expired");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,47 +279,22 @@ namespace TeleBot.Handlers
|
||||
categoryName = categories.FirstOrDefault(c => c.Id == categoryId)?.Name;
|
||||
}
|
||||
|
||||
// Edit the original message to show category header
|
||||
var headerText = !string.IsNullOrEmpty(categoryName)
|
||||
? $"**Products in {categoryName}**\n\nBrowse products below:"
|
||||
: "**All Products**\n\nBrowse products below:";
|
||||
|
||||
await bot.EditMessageTextAsync(
|
||||
message.Chat.Id,
|
||||
message.MessageId,
|
||||
headerText,
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: MenuBuilder.CategoryNavigationMenu(categoryId)
|
||||
);
|
||||
// Use carousel service to send products with images
|
||||
await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, categoryName, page);
|
||||
session.State = SessionState.BrowsingProducts;
|
||||
}
|
||||
|
||||
private async Task HandleProductsPage(ITelegramBotClient bot, Message message, UserSession session, string[] data)
|
||||
{
|
||||
// Format: products:page:pageNumber
|
||||
var page = int.Parse(data[2]);
|
||||
|
||||
// Send individual product bubbles
|
||||
if (products.Items.Any())
|
||||
{
|
||||
foreach (var product in products.Items)
|
||||
{
|
||||
await bot.SendTextMessageAsync(
|
||||
message.Chat.Id,
|
||||
MessageFormatter.FormatSingleProduct(product),
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: MenuBuilder.SingleProductMenu(product.Id)
|
||||
);
|
||||
}
|
||||
|
||||
// Send navigation buttons after all products
|
||||
await bot.SendTextMessageAsync(
|
||||
message.Chat.Id,
|
||||
".",
|
||||
replyMarkup: MenuBuilder.ProductNavigationMenu(categoryId)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
await bot.SendTextMessageAsync(
|
||||
message.Chat.Id,
|
||||
"No products available in this category.",
|
||||
replyMarkup: MenuBuilder.BackToCategoriesMenu()
|
||||
);
|
||||
}
|
||||
// Get products for all categories (no specific category filter)
|
||||
var products = await _shopService.GetProductsAsync(null, page);
|
||||
|
||||
// Use carousel service to send products with images
|
||||
await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, "All Categories", page);
|
||||
session.State = SessionState.BrowsingProducts;
|
||||
}
|
||||
|
||||
private async Task HandleProductDetail(ITelegramBotClient bot, Message message, UserSession session, Guid productId)
|
||||
@@ -308,13 +310,8 @@ namespace TeleBot.Handlers
|
||||
session.TempData["current_product"] = productId;
|
||||
session.TempData["current_quantity"] = 1;
|
||||
|
||||
await bot.EditMessageTextAsync(
|
||||
message.Chat.Id,
|
||||
message.MessageId,
|
||||
MessageFormatter.FormatProductDetail(product),
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: MenuBuilder.ProductDetailMenu(product, 1)
|
||||
);
|
||||
// Use carousel service to send product with image
|
||||
await _carouselService.SendSingleProductWithImageAsync(bot, message.Chat.Id, product);
|
||||
session.State = SessionState.ViewingProduct;
|
||||
}
|
||||
|
||||
@@ -482,24 +479,92 @@ namespace TeleBot.Handlers
|
||||
{
|
||||
if (!session.TempData.TryGetValue("current_order_id", out var orderIdObj) || orderIdObj is not Guid orderId)
|
||||
{
|
||||
await bot.AnswerCallbackQueryAsync("", "Order not found", showAlert: true);
|
||||
return;
|
||||
}
|
||||
|
||||
var payment = await _shopService.CreatePaymentAsync(orderId, currency);
|
||||
|
||||
if (payment == null)
|
||||
{
|
||||
await bot.EditMessageTextAsync(
|
||||
await SafeEditMessageAsync(
|
||||
bot,
|
||||
message.Chat.Id,
|
||||
message.MessageId,
|
||||
"❌ Failed to create payment. Please try again.",
|
||||
replyMarkup: MenuBuilder.MainMenu()
|
||||
"❌ Order not found. Please start a new order.",
|
||||
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
MenuBuilder.MainMenu()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var paymentText = MessageFormatter.FormatPayment(payment);
|
||||
// Show processing message
|
||||
await SafeEditMessageAsync(
|
||||
bot,
|
||||
message.Chat.Id,
|
||||
message.MessageId,
|
||||
$"🔄 Creating {currency} payment...\n\nPlease wait...",
|
||||
Telegram.Bot.Types.Enums.ParseMode.Markdown
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
var payment = await _shopService.CreatePaymentAsync(orderId, currency);
|
||||
|
||||
if (payment == null)
|
||||
{
|
||||
await SafeEditMessageAsync(
|
||||
bot,
|
||||
message.Chat.Id,
|
||||
message.MessageId,
|
||||
$"❌ *Payment Creation Failed*\n\n" +
|
||||
$"Unable to create {currency} payment.\n" +
|
||||
$"This might be due to:\n" +
|
||||
$"• Payment gateway temporarily unavailable\n" +
|
||||
$"• Network connectivity issues\n\n" +
|
||||
$"Please try again in a few minutes.",
|
||||
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
MenuBuilder.MainMenu()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Payment created successfully, continue with display
|
||||
var paymentText = MessageFormatter.FormatPayment(payment);
|
||||
|
||||
await DisplayPaymentInfo(bot, message, payment, paymentText);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create payment for order {OrderId} with currency {Currency}", orderId, currency);
|
||||
|
||||
await SafeEditMessageAsync(
|
||||
bot,
|
||||
message.Chat.Id,
|
||||
message.MessageId,
|
||||
$"❌ *Payment System Error*\n\n" +
|
||||
$"Sorry, there was a technical issue creating your {currency} payment.\n\n" +
|
||||
$"Our payment system may be undergoing maintenance.\n" +
|
||||
$"Please try again later or contact support.",
|
||||
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
MenuBuilder.MainMenu()
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely edit a message only if the content has changed
|
||||
/// </summary>
|
||||
private async Task SafeEditMessageAsync(ITelegramBotClient bot, ChatId chatId, int messageId, string newText,
|
||||
Telegram.Bot.Types.Enums.ParseMode parseMode = Telegram.Bot.Types.Enums.ParseMode.Html,
|
||||
InlineKeyboardMarkup? replyMarkup = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await bot.EditMessageTextAsync(chatId, messageId, newText, parseMode: parseMode, replyMarkup: replyMarkup);
|
||||
}
|
||||
catch (ApiRequestException apiEx) when (apiEx.Message.Contains("message is not modified"))
|
||||
{
|
||||
// Message content hasn't changed, this is fine
|
||||
_logger.LogDebug("Attempted to edit message with identical content");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DisplayPaymentInfo(ITelegramBotClient bot, Message message, dynamic payment, string paymentText)
|
||||
{
|
||||
|
||||
// Generate QR code if enabled
|
||||
if (_configuration.GetValue<bool>("Features:EnableQRCodes"))
|
||||
@@ -526,23 +591,25 @@ namespace TeleBot.Handlers
|
||||
{
|
||||
_logger.LogError(ex, "Failed to generate QR code");
|
||||
// Fall back to text-only
|
||||
await bot.EditMessageTextAsync(
|
||||
await SafeEditMessageAsync(
|
||||
bot,
|
||||
message.Chat.Id,
|
||||
message.MessageId,
|
||||
paymentText,
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: MenuBuilder.MainMenu()
|
||||
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
MenuBuilder.MainMenu()
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await bot.EditMessageTextAsync(
|
||||
await SafeEditMessageAsync(
|
||||
bot,
|
||||
message.Chat.Id,
|
||||
message.MessageId,
|
||||
paymentText,
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: MenuBuilder.MainMenu()
|
||||
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
MenuBuilder.MainMenu()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,17 +18,20 @@ namespace TeleBot.Handlers
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly ILittleShopService _shopService;
|
||||
private readonly IPrivacyService _privacyService;
|
||||
private readonly IProductCarouselService _carouselService;
|
||||
private readonly ILogger<CommandHandler> _logger;
|
||||
|
||||
public CommandHandler(
|
||||
ISessionManager sessionManager,
|
||||
ILittleShopService shopService,
|
||||
IPrivacyService privacyService,
|
||||
IProductCarouselService carouselService,
|
||||
ILogger<CommandHandler> logger)
|
||||
{
|
||||
_sessionManager = sessionManager;
|
||||
_shopService = shopService;
|
||||
_privacyService = privacyService;
|
||||
_carouselService = carouselService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -48,6 +51,10 @@ namespace TeleBot.Handlers
|
||||
await HandleBrowseCommand(bot, message, session);
|
||||
break;
|
||||
|
||||
case "/products":
|
||||
await HandleProductsCommand(bot, message, session, args);
|
||||
break;
|
||||
|
||||
case "/cart":
|
||||
await HandleCartCommand(bot, message, session);
|
||||
break;
|
||||
@@ -92,6 +99,10 @@ namespace TeleBot.Handlers
|
||||
await HandleCancelCommand(bot, message, session);
|
||||
break;
|
||||
|
||||
case "/review":
|
||||
await HandleReviewCommand(bot, message, session);
|
||||
break;
|
||||
|
||||
default:
|
||||
await bot.SendTextMessageAsync(
|
||||
message.Chat.Id,
|
||||
@@ -142,6 +153,55 @@ namespace TeleBot.Handlers
|
||||
session.State = Models.SessionState.BrowsingCategories;
|
||||
}
|
||||
|
||||
private async Task HandleProductsCommand(ITelegramBotClient bot, Message message, Models.UserSession session, string? args)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse category ID from args if provided
|
||||
Guid? categoryId = null;
|
||||
if (!string.IsNullOrEmpty(args) && Guid.TryParse(args, out var parsedCategoryId))
|
||||
{
|
||||
categoryId = parsedCategoryId;
|
||||
}
|
||||
|
||||
// Get products
|
||||
var products = await _shopService.GetProductsAsync(categoryId, 1);
|
||||
|
||||
if (!products.Items.Any())
|
||||
{
|
||||
await bot.SendTextMessageAsync(
|
||||
message.Chat.Id,
|
||||
"No products available in this category.",
|
||||
replyMarkup: MenuBuilder.CategoryNavigationMenu(categoryId)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get category name if categoryId is provided
|
||||
string? categoryName = null;
|
||||
if (categoryId.HasValue)
|
||||
{
|
||||
var categories = await _shopService.GetCategoriesAsync();
|
||||
var category = categories.FirstOrDefault(c => c.Id == categoryId.Value);
|
||||
categoryName = category?.Name;
|
||||
}
|
||||
|
||||
// Send products as carousel with images
|
||||
await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, categoryName, 1);
|
||||
|
||||
session.State = Models.SessionState.BrowsingProducts;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error handling products command");
|
||||
await bot.SendTextMessageAsync(
|
||||
message.Chat.Id,
|
||||
"An error occurred while loading products. Please try again.",
|
||||
replyMarkup: MenuBuilder.MainMenu()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCartCommand(ITelegramBotClient bot, Message message, Models.UserSession session)
|
||||
{
|
||||
var text = MessageFormatter.FormatCart(session.Cart);
|
||||
@@ -373,5 +433,103 @@ namespace TeleBot.Handlers
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleReviewCommand(ITelegramBotClient bot, Message message, Models.UserSession session)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get customer's shipped orders
|
||||
var orders = await _shopService.GetCustomerOrdersAsync(
|
||||
message.From!.Id,
|
||||
message.From.Username ?? "",
|
||||
message.From.FirstName + " " + message.From.LastName,
|
||||
message.From.FirstName ?? "",
|
||||
message.From.LastName ?? ""
|
||||
);
|
||||
|
||||
var shippedOrders = orders.Where(o => o.Status == 3).ToList(); // Status 3 = Shipped
|
||||
|
||||
if (!shippedOrders.Any())
|
||||
{
|
||||
await bot.SendTextMessageAsync(
|
||||
message.Chat.Id,
|
||||
"⭐ *Product Reviews*\n\n" +
|
||||
"You can only review products from orders that have been shipped.\n\n" +
|
||||
"Once you receive your order, come back here to share your experience!",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: MenuBuilder.MainMenu()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show reviewable products
|
||||
var reviewableProducts = new List<dynamic>();
|
||||
foreach (var order in shippedOrders)
|
||||
{
|
||||
foreach (var item in order.Items)
|
||||
{
|
||||
reviewableProducts.Add(new
|
||||
{
|
||||
ProductId = item.ProductId,
|
||||
ProductName = item.ProductName,
|
||||
OrderId = order.Id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!reviewableProducts.Any())
|
||||
{
|
||||
await bot.SendTextMessageAsync(
|
||||
message.Chat.Id,
|
||||
"⭐ *Product Reviews*\n\n" +
|
||||
"No reviewable products found in your shipped orders.",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: MenuBuilder.MainMenu()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build review selection menu
|
||||
var reviewText = "⭐ *Leave a Product Review*\n\n" +
|
||||
"Select a product from your shipped orders to review:\n\n";
|
||||
|
||||
var keyboard = new List<List<Telegram.Bot.Types.ReplyMarkups.InlineKeyboardButton>>();
|
||||
|
||||
foreach (var product in reviewableProducts.Take(10)) // Limit to 10 for UI
|
||||
{
|
||||
reviewText += $"• {product.ProductName}\n";
|
||||
|
||||
keyboard.Add(new List<Telegram.Bot.Types.ReplyMarkups.InlineKeyboardButton>
|
||||
{
|
||||
Telegram.Bot.Types.ReplyMarkups.InlineKeyboardButton.WithCallbackData(
|
||||
$"Review {product.ProductName}",
|
||||
$"review_{product.ProductId}_{product.OrderId}")
|
||||
});
|
||||
}
|
||||
|
||||
keyboard.Add(new List<Telegram.Bot.Types.ReplyMarkups.InlineKeyboardButton>
|
||||
{
|
||||
Telegram.Bot.Types.ReplyMarkups.InlineKeyboardButton.WithCallbackData("« Back to Menu", "menu_main")
|
||||
});
|
||||
|
||||
var replyMarkup = new Telegram.Bot.Types.ReplyMarkups.InlineKeyboardMarkup(keyboard);
|
||||
|
||||
await bot.SendTextMessageAsync(
|
||||
message.Chat.Id,
|
||||
reviewText,
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: replyMarkup
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error handling review command");
|
||||
await bot.SendTextMessageAsync(
|
||||
message.Chat.Id,
|
||||
"❌ Sorry, there was an error accessing your reviews. Please try again later.",
|
||||
replyMarkup: MenuBuilder.MainMenu()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,113 +1,119 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Hangfire;
|
||||
using Hangfire.LiteDB;
|
||||
using LittleShop.Client.Extensions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using TeleBot;
|
||||
using TeleBot.Handlers;
|
||||
using TeleBot.Services;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Configuration
|
||||
builder.Configuration
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
|
||||
.AddEnvironmentVariables();
|
||||
|
||||
// Serilog
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Information()
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console()
|
||||
.WriteTo.File("logs/telebot-.txt", rollingInterval: RollingInterval.Day)
|
||||
.CreateLogger();
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddSerilog();
|
||||
|
||||
// Services
|
||||
builder.Services.AddSingleton<IPrivacyService, PrivacyService>();
|
||||
builder.Services.AddSingleton<SessionManager>();
|
||||
builder.Services.AddSingleton<ISessionManager>(provider => provider.GetRequiredService<SessionManager>());
|
||||
builder.Services.AddHostedService<SessionManager>(provider => provider.GetRequiredService<SessionManager>());
|
||||
|
||||
// LittleShop Client
|
||||
builder.Services.AddLittleShopClient(options =>
|
||||
{
|
||||
var config = builder.Configuration;
|
||||
options.BaseUrl = config["LittleShop:ApiUrl"] ?? "https://localhost:5001";
|
||||
options.TimeoutSeconds = 30;
|
||||
options.MaxRetryAttempts = 3;
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<ILittleShopService, LittleShopService>();
|
||||
|
||||
// Redis (if enabled)
|
||||
if (builder.Configuration.GetValue<bool>("Redis:Enabled"))
|
||||
{
|
||||
builder.Services.AddStackExchangeRedisCache(options =>
|
||||
{
|
||||
options.Configuration = builder.Configuration["Redis:ConnectionString"] ?? "localhost:6379";
|
||||
options.InstanceName = builder.Configuration["Redis:InstanceName"] ?? "TeleBot";
|
||||
});
|
||||
}
|
||||
|
||||
// Hangfire (if enabled)
|
||||
if (builder.Configuration.GetValue<bool>("Hangfire:Enabled"))
|
||||
{
|
||||
var hangfireDb = builder.Configuration["Hangfire:DatabasePath"] ?? "hangfire.db";
|
||||
builder.Services.AddHangfire(config =>
|
||||
{
|
||||
config.UseLiteDbStorage(hangfireDb);
|
||||
});
|
||||
builder.Services.AddHangfireServer();
|
||||
}
|
||||
|
||||
// Bot Handlers
|
||||
builder.Services.AddSingleton<ICommandHandler, CommandHandler>();
|
||||
builder.Services.AddSingleton<ICallbackHandler, CallbackHandler>();
|
||||
builder.Services.AddSingleton<IMessageHandler, MessageHandler>();
|
||||
|
||||
// Bot Manager Service (for registration and metrics)
|
||||
builder.Services.AddHttpClient<BotManagerService>();
|
||||
builder.Services.AddSingleton<BotManagerService>();
|
||||
builder.Services.AddHostedService<BotManagerService>();
|
||||
|
||||
// Message Delivery Service - Single instance
|
||||
builder.Services.AddSingleton<MessageDeliveryService>();
|
||||
builder.Services.AddSingleton<IMessageDeliveryService>(sp => sp.GetRequiredService<MessageDeliveryService>());
|
||||
builder.Services.AddHostedService<MessageDeliveryService>(sp => sp.GetRequiredService<MessageDeliveryService>());
|
||||
|
||||
// Bot Service
|
||||
builder.Services.AddHostedService<TelegramBotService>();
|
||||
|
||||
// Build and run
|
||||
var host = builder.Build();
|
||||
|
||||
try
|
||||
{
|
||||
Log.Information("Starting TeleBot - Privacy-First E-Commerce Bot");
|
||||
Log.Information("Privacy Mode: {PrivacyMode}", builder.Configuration["Privacy:Mode"]);
|
||||
Log.Information("Ephemeral by Default: {Ephemeral}", builder.Configuration["Privacy:EphemeralByDefault"]);
|
||||
Log.Information("Tor Enabled: {Tor}", builder.Configuration["Privacy:EnableTor"]);
|
||||
|
||||
await host.RunAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Application start-up failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Hangfire;
|
||||
using Hangfire.LiteDB;
|
||||
using LittleShop.Client.Extensions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using TeleBot;
|
||||
using TeleBot.Handlers;
|
||||
using TeleBot.Services;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
public static string BrandName ?? "Little Shop";
|
||||
// Configuration
|
||||
builder.Configuration
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
|
||||
.AddEnvironmentVariables();
|
||||
|
||||
// Serilog
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Information()
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console()
|
||||
.WriteTo.File("logs/telebot-.txt", rollingInterval: RollingInterval.Day)
|
||||
.CreateLogger();
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddSerilog();
|
||||
|
||||
// Services
|
||||
builder.Services.AddSingleton<IPrivacyService, PrivacyService>();
|
||||
builder.Services.AddSingleton<SessionManager>();
|
||||
builder.Services.AddSingleton<ISessionManager>(provider => provider.GetRequiredService<SessionManager>());
|
||||
builder.Services.AddHostedService<SessionManager>(provider => provider.GetRequiredService<SessionManager>());
|
||||
|
||||
// LittleShop Client
|
||||
builder.Services.AddLittleShopClient(options =>
|
||||
{
|
||||
var config = builder.Configuration;
|
||||
options.BaseUrl = config["LittleShop:ApiUrl"] ?? "https://localhost:5001";
|
||||
options.TimeoutSeconds = 30;
|
||||
options.MaxRetryAttempts = 3;
|
||||
|
||||
BrandName = config["LittleShop.BrandName"] ?? "Little Shop";
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<ILittleShopService, LittleShopService>();
|
||||
|
||||
// Redis (if enabled)
|
||||
if (builder.Configuration.GetValue<bool>("Redis:Enabled"))
|
||||
{
|
||||
builder.Services.AddStackExchangeRedisCache(options =>
|
||||
{
|
||||
options.Configuration = builder.Configuration["Redis:ConnectionString"] ?? "localhost:6379";
|
||||
options.InstanceName = builder.Configuration["Redis:InstanceName"] ?? "TeleBot";
|
||||
});
|
||||
}
|
||||
|
||||
// Hangfire (if enabled)
|
||||
if (builder.Configuration.GetValue<bool>("Hangfire:Enabled"))
|
||||
{
|
||||
var hangfireDb = builder.Configuration["Hangfire:DatabasePath"] ?? "hangfire.db";
|
||||
builder.Services.AddHangfire(config =>
|
||||
{
|
||||
config.UseLiteDbStorage(hangfireDb);
|
||||
});
|
||||
builder.Services.AddHangfireServer();
|
||||
}
|
||||
|
||||
// Bot Handlers
|
||||
builder.Services.AddSingleton<ICommandHandler, CommandHandler>();
|
||||
builder.Services.AddSingleton<ICallbackHandler, CallbackHandler>();
|
||||
builder.Services.AddSingleton<IMessageHandler, MessageHandler>();
|
||||
|
||||
// Bot Manager Service (for registration and metrics)
|
||||
builder.Services.AddHttpClient<BotManagerService>();
|
||||
builder.Services.AddSingleton<BotManagerService>();
|
||||
builder.Services.AddHostedService<BotManagerService>();
|
||||
|
||||
// Message Delivery Service - Single instance
|
||||
builder.Services.AddSingleton<MessageDeliveryService>();
|
||||
builder.Services.AddSingleton<IMessageDeliveryService>(sp => sp.GetRequiredService<MessageDeliveryService>());
|
||||
builder.Services.AddHostedService<MessageDeliveryService>(sp => sp.GetRequiredService<MessageDeliveryService>());
|
||||
|
||||
// Product Carousel Service
|
||||
builder.Services.AddHttpClient<ProductCarouselService>();
|
||||
builder.Services.AddSingleton<IProductCarouselService, ProductCarouselService>();
|
||||
|
||||
// Bot Service
|
||||
builder.Services.AddHostedService<TelegramBotService>();
|
||||
|
||||
// Build and run
|
||||
var host = builder.Build();
|
||||
|
||||
try
|
||||
{
|
||||
Log.Information("Starting TeleBot - Privacy-First E-Commerce Bot");
|
||||
Log.Information("Privacy Mode: {PrivacyMode}", builder.Configuration["Privacy:Mode"]);
|
||||
Log.Information("Ephemeral by Default: {Ephemeral}", builder.Configuration["Privacy:EphemeralByDefault"]);
|
||||
Log.Information("Tor Enabled: {Tor}", builder.Configuration["Privacy:EnableTor"]);
|
||||
|
||||
await host.RunAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Application start-up failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
315
TeleBot/TeleBot/Services/ProductCarouselService.cs
Normal file
315
TeleBot/TeleBot/Services/ProductCarouselService.cs
Normal file
@@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using LittleShop.Client.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.InputFiles;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
|
||||
namespace TeleBot.Services
|
||||
{
|
||||
public interface IProductCarouselService
|
||||
{
|
||||
Task SendProductCarouselAsync(ITelegramBotClient botClient, long chatId, PagedResult<Product> products, string? categoryName = null, int currentPage = 1);
|
||||
Task SendSingleProductWithImageAsync(ITelegramBotClient botClient, long chatId, Product product);
|
||||
Task<InputOnlineFile?> GetProductImageAsync(Product product);
|
||||
Task<bool> IsImageUrlValidAsync(string imageUrl);
|
||||
}
|
||||
|
||||
public class ProductCarouselService : IProductCarouselService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<ProductCarouselService> _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _imageCachePath;
|
||||
|
||||
public ProductCarouselService(
|
||||
IConfiguration configuration,
|
||||
ILogger<ProductCarouselService> logger,
|
||||
HttpClient httpClient)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
_httpClient = httpClient;
|
||||
_imageCachePath = Path.Combine(Environment.CurrentDirectory, "image_cache");
|
||||
|
||||
// Ensure cache directory exists
|
||||
Directory.CreateDirectory(_imageCachePath);
|
||||
}
|
||||
|
||||
public async Task SendProductCarouselAsync(ITelegramBotClient botClient, long chatId, PagedResult<Product> products, string? categoryName = null, int currentPage = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!products.Items.Any())
|
||||
{
|
||||
await botClient.SendTextMessageAsync(
|
||||
chatId,
|
||||
"No products available in this category.",
|
||||
replyMarkup: MenuBuilder.CategoryNavigationMenu(null)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send products as media group (carousel) - max 10 items per group
|
||||
var productBatches = products.Items.Chunk(10);
|
||||
|
||||
foreach (var batch in productBatches)
|
||||
{
|
||||
var mediaGroup = new List<InputMediaPhoto>();
|
||||
var productButtons = new List<InlineKeyboardButton[]>();
|
||||
|
||||
foreach (var product in batch)
|
||||
{
|
||||
// Get product image
|
||||
var image = await GetProductImageAsync(product);
|
||||
|
||||
if (image != null)
|
||||
{
|
||||
// Create photo with caption
|
||||
var caption = FormatProductCaption(product);
|
||||
var photo = new InputMediaPhoto(image)
|
||||
{
|
||||
Caption = caption,
|
||||
ParseMode = Telegram.Bot.Types.Enums.ParseMode.Markdown
|
||||
};
|
||||
mediaGroup.Add(photo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no image, send as text message instead
|
||||
await botClient.SendTextMessageAsync(
|
||||
chatId,
|
||||
FormatProductCaption(product),
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: MenuBuilder.SingleProductMenu(product.Id)
|
||||
);
|
||||
}
|
||||
|
||||
// Add button for this product
|
||||
productButtons.Add(new[]
|
||||
{
|
||||
InlineKeyboardButton.WithCallbackData(
|
||||
$"🛒 {product.Name} - ${product.Price:F2}",
|
||||
$"product:{product.Id}"
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// Send media group if we have images
|
||||
if (mediaGroup.Any())
|
||||
{
|
||||
try
|
||||
{
|
||||
await botClient.SendMediaGroupAsync(chatId, mediaGroup);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to send media group, falling back to individual messages");
|
||||
|
||||
// Fallback: send individual messages
|
||||
foreach (var product in batch)
|
||||
{
|
||||
await SendSingleProductWithImageAsync(botClient, chatId, product);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send navigation buttons
|
||||
var navigationButtons = new List<InlineKeyboardButton[]>();
|
||||
|
||||
// Add product buttons
|
||||
navigationButtons.AddRange(productButtons);
|
||||
|
||||
// Add pagination if needed
|
||||
if (products.TotalPages > 1)
|
||||
{
|
||||
var paginationButtons = new List<InlineKeyboardButton>();
|
||||
|
||||
if (products.HasPreviousPage)
|
||||
{
|
||||
paginationButtons.Add(InlineKeyboardButton.WithCallbackData(
|
||||
"⬅️ Previous",
|
||||
$"products:page:{currentPage - 1}"
|
||||
));
|
||||
}
|
||||
|
||||
paginationButtons.Add(InlineKeyboardButton.WithCallbackData(
|
||||
$"Page {currentPage}/{products.TotalPages}",
|
||||
"noop"
|
||||
));
|
||||
|
||||
if (products.HasNextPage)
|
||||
{
|
||||
paginationButtons.Add(InlineKeyboardButton.WithCallbackData(
|
||||
"Next ➡️",
|
||||
$"products:page:{currentPage + 1}"
|
||||
));
|
||||
}
|
||||
|
||||
navigationButtons.Add(paginationButtons.ToArray());
|
||||
}
|
||||
|
||||
// Add main navigation
|
||||
navigationButtons.Add(new[]
|
||||
{
|
||||
InlineKeyboardButton.WithCallbackData("🛒 View Cart", "cart"),
|
||||
InlineKeyboardButton.WithCallbackData("⬅️ Back to Categories", "browse")
|
||||
});
|
||||
|
||||
navigationButtons.Add(new[]
|
||||
{
|
||||
InlineKeyboardButton.WithCallbackData("🏠 Main Menu", "menu")
|
||||
});
|
||||
|
||||
var replyMarkup = new InlineKeyboardMarkup(navigationButtons);
|
||||
|
||||
await botClient.SendTextMessageAsync(
|
||||
chatId,
|
||||
$"📦 *Products in {categoryName ?? "All Categories"}*\n\n" +
|
||||
$"Showing {batch.Count()} products (Page {currentPage} of {products.TotalPages})",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: replyMarkup
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error sending product carousel");
|
||||
|
||||
// Fallback to text-only product list
|
||||
await botClient.SendTextMessageAsync(
|
||||
chatId,
|
||||
MessageFormatter.FormatProductList(products, categoryName),
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: MenuBuilder.ProductListMenu(products, null, currentPage)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendSingleProductWithImageAsync(ITelegramBotClient botClient, long chatId, Product product)
|
||||
{
|
||||
try
|
||||
{
|
||||
var image = await GetProductImageAsync(product);
|
||||
|
||||
if (image != null)
|
||||
{
|
||||
await botClient.SendPhotoAsync(
|
||||
chatId,
|
||||
image,
|
||||
caption: FormatProductCaption(product),
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: MenuBuilder.ProductDetailMenu(product)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to text message
|
||||
await botClient.SendTextMessageAsync(
|
||||
chatId,
|
||||
MessageFormatter.FormatProductDetail(product),
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: MenuBuilder.ProductDetailMenu(product)
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error sending single product with image");
|
||||
|
||||
// Fallback to text message
|
||||
await botClient.SendTextMessageAsync(
|
||||
chatId,
|
||||
MessageFormatter.FormatProductDetail(product),
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||
replyMarkup: MenuBuilder.ProductDetailMenu(product)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<InputOnlineFile?> GetProductImageAsync(Product product)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!product.Photos.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the first photo
|
||||
var photo = product.Photos.First();
|
||||
var imageUrl = photo.Url;
|
||||
|
||||
if (string.IsNullOrEmpty(imageUrl) || !await IsImageUrlValidAsync(imageUrl))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if image is already cached
|
||||
var cacheKey = $"{product.Id}_{photo.Id}";
|
||||
var cachedPath = Path.Combine(_imageCachePath, $"{cacheKey}.jpg");
|
||||
|
||||
if (File.Exists(cachedPath))
|
||||
{
|
||||
return new InputOnlineFile(File.OpenRead(cachedPath), $"{product.Name}.jpg");
|
||||
}
|
||||
|
||||
// Download and cache the image
|
||||
var imageBytes = await _httpClient.GetByteArrayAsync(imageUrl);
|
||||
await File.WriteAllBytesAsync(cachedPath, imageBytes);
|
||||
|
||||
return new InputOnlineFile(File.OpenRead(cachedPath), $"{product.Name}.jpg");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to get product image for product {ProductId}", product.Id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsImageUrlValidAsync(string imageUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(imageUrl))
|
||||
return false;
|
||||
|
||||
var response = await _httpClient.HeadAsync(imageUrl);
|
||||
return response.IsSuccessStatusCode &&
|
||||
response.Content.Headers.ContentType?.MediaType?.StartsWith("image/") == true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatProductCaption(Product product)
|
||||
{
|
||||
var caption = $"🛍️ *{product.Name}*\n";
|
||||
caption += $"💰 *${product.Price:F2}*\n";
|
||||
|
||||
if (!string.IsNullOrEmpty(product.Description))
|
||||
{
|
||||
var desc = product.Description.Length > 200
|
||||
? product.Description.Substring(0, 197) + "..."
|
||||
: product.Description;
|
||||
caption += $"\n_{desc}_";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(product.CategoryName))
|
||||
{
|
||||
caption += $"\n\n📁 {product.CategoryName}";
|
||||
}
|
||||
|
||||
return caption;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,59 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Telegram Bot Framework -->
|
||||
<PackageReference Include="Telegram.Bot" Version="19.0.0" />
|
||||
<PackageReference Include="Telegram.Bot.Extensions.Polling" Version="1.0.2" />
|
||||
|
||||
<!-- Privacy & Security -->
|
||||
<!-- TorSharp alternative - use manual Tor configuration instead -->
|
||||
<PackageReference Include="PgpCore" Version="6.5.0" />
|
||||
|
||||
<!-- Data Storage -->
|
||||
<PackageReference Include="LiteDB" Version="5.0.21" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
|
||||
|
||||
<!-- Dependency Injection -->
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.0" />
|
||||
|
||||
<!-- Background Jobs -->
|
||||
<PackageReference Include="Hangfire" Version="1.8.17" />
|
||||
<PackageReference Include="Hangfire.LiteDB" Version="0.4.1" />
|
||||
|
||||
<!-- Utilities -->
|
||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
|
||||
<!-- Logging -->
|
||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\LittleShop.Client\LittleShop.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Telegram Bot Framework -->
|
||||
<PackageReference Include="Telegram.Bot" Version="19.0.0" />
|
||||
<PackageReference Include="Telegram.Bot.Extensions.Polling" Version="1.0.2" />
|
||||
|
||||
<!-- Privacy & Security -->
|
||||
<!-- TorSharp alternative - use manual Tor configuration instead -->
|
||||
<PackageReference Include="PgpCore" Version="6.5.0" />
|
||||
|
||||
<!-- Data Storage -->
|
||||
<PackageReference Include="LiteDB" Version="5.0.21" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
|
||||
|
||||
<!-- Dependency Injection -->
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.0" />
|
||||
|
||||
<!-- Background Jobs -->
|
||||
<PackageReference Include="Hangfire" Version="1.8.17" />
|
||||
<PackageReference Include="Hangfire.LiteDB" Version="0.4.1" />
|
||||
|
||||
<!-- Utilities -->
|
||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
|
||||
<!-- Logging -->
|
||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\LittleShop.Client\LittleShop.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,132 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Polling;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
using Telegram.Bot.Exceptions;
|
||||
|
||||
namespace TeleBot
|
||||
{
|
||||
using Telegram.Bot.Types.Enums;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
|
||||
public class TelgramBotService
|
||||
{
|
||||
private readonly string BotToken = "7880403661:AAGma1wAyoHsmG45iO6VvHCqzimhJX1pp14";
|
||||
|
||||
public BotScript Master { get; set; }
|
||||
public Dictionary<long, BotScript> Chats { get; set; } = new Dictionary<long, BotScript>();
|
||||
|
||||
public async Task Startup()
|
||||
{
|
||||
|
||||
if (Master == null)
|
||||
{
|
||||
Master = BotScript.CreateBotScript("Anonymous Feedback Survey for The Sweetshop \r\n\r\nWe’d love to hear your thoughts so we can improve your experience and keep the shop evolving in the best way possible.");
|
||||
Master.AddScaledQuestion("How would you rate communication at the shop (including updates, clarity, and friendliness)?\r\n(1 = Poor | 10 = Excellent)");
|
||||
Master.AddScaledQuestion("How would you rate the quality of the products?\r\n(1 = Poor | 10 = Excellent)");
|
||||
}
|
||||
|
||||
var botClient = new TelegramBotClient(BotToken);
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var receiverOptions = new ReceiverOptions
|
||||
{
|
||||
AllowedUpdates = Array.Empty<UpdateType>() // Receive all updates
|
||||
};
|
||||
|
||||
botClient.StartReceiving(
|
||||
HandleUpdateAsync,
|
||||
HandleErrorAsync,
|
||||
receiverOptions,
|
||||
cancellationToken: cts.Token
|
||||
);
|
||||
|
||||
var me = await botClient.GetMeAsync();
|
||||
Console.WriteLine($"Bot started: {me.Username}");
|
||||
Console.ReadLine();
|
||||
cts.Cancel();
|
||||
}
|
||||
|
||||
private async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
|
||||
{
|
||||
if (update.Message is { } message)
|
||||
{
|
||||
|
||||
var chatId = message.Chat.Id;
|
||||
if (!Chats.ContainsKey(chatId))
|
||||
{
|
||||
var s = Master;
|
||||
Chats.Add(chatId, s);
|
||||
}
|
||||
|
||||
//if (message.Text == "/start")
|
||||
//{
|
||||
await ProcessMessage(botClient, chatId, cancellationToken);
|
||||
//var responseText = $"Hello, {message.From?.FirstName}! You said: {message.Text}";
|
||||
//await botClient.SendMessage(chatId, responseText, cancellationToken: cancellationToken);
|
||||
}
|
||||
else if (update.CallbackQuery is { } callbackQuery)
|
||||
{
|
||||
var data = callbackQuery.Data?.Split(':');
|
||||
var chatId = callbackQuery.Message.Chat.Id;
|
||||
var aID = Guid.Parse(data[0]);
|
||||
Chats[chatId].Answers.Add(aID, data[1]);
|
||||
|
||||
var response = $"Thank for choosing: {data[1]} in response to '{Chats[chatId].Questions.First(x => x.Key == aID).Value.Text}'";
|
||||
await botClient.SendTextMessageAsync(callbackQuery.Message.Chat.Id, response, cancellationToken: cancellationToken);
|
||||
Chats[chatId].Stage++;
|
||||
if (Chats[chatId].Stage > Chats[chatId].Questions.Count)
|
||||
{
|
||||
await botClient.SendTextMessageAsync(chatId, "Thank you for completing our questions, we appreciete your feedback!", cancellationToken: cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ProcessMessage(botClient, chatId, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessMessage(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Chats[chatId].Stage > Chats[chatId].Questions.Count)
|
||||
{
|
||||
await botClient.SendTextMessageAsync(chatId, "You have already completed the questionaire. Thank you for your feedback.", cancellationToken: cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (Chats[chatId].Stage)
|
||||
{
|
||||
case 0:
|
||||
await botClient.SendTextMessageAsync(chatId, Chats[chatId].WelcomeText, cancellationToken: cancellationToken);
|
||||
Chats[chatId].Stage++;
|
||||
break;
|
||||
default:
|
||||
var q = Chats[chatId].Questions.OrderBy(x => x.Value.Order).Skip(Chats[chatId].Stage - 1).Take(1).FirstOrDefault();
|
||||
var opts = new InlineKeyboardMarkup(new[]
|
||||
{
|
||||
q.Value.Options.Take(5).Select(x => InlineKeyboardButton.WithCallbackData(x, $"{q.Key}:{x}")),
|
||||
q.Value.Options.Skip(5).Select(x => InlineKeyboardButton.WithCallbackData(x, $"{q.Key}:{x}"))
|
||||
});
|
||||
await botClient.SendTextMessageAsync(chatId, q.Value.Text, replyMarkup: opts, cancellationToken: cancellationToken);
|
||||
|
||||
opts = new InlineKeyboardMarkup(q.Value.Options.Skip(5).Select(x => InlineKeyboardButton.WithCallbackData(x, $"{q.Key}:{x}")));
|
||||
await botClient.SendTextMessageAsync(chatId, "", replyMarkup: opts, cancellationToken: cancellationToken);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken)
|
||||
{
|
||||
Console.WriteLine($"Error: {exception.Message}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Polling;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
using Telegram.Bot.Exceptions;
|
||||
|
||||
namespace TeleBot
|
||||
{
|
||||
using Telegram.Bot.Types.Enums;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
|
||||
public class TelgramBotService
|
||||
{
|
||||
private readonly string BotToken = "7880403661:AAGma1wAyoHsmG45iO6VvHCqzimhJX1pp14";
|
||||
|
||||
public BotScript Master { get; set; }
|
||||
public Dictionary<long, BotScript> Chats { get; set; } = new Dictionary<long, BotScript>();
|
||||
|
||||
public async Task Startup()
|
||||
{
|
||||
|
||||
if (Master == null)
|
||||
{
|
||||
Master = BotScript.CreateBotScript("Anonymous Feedback Survey for The Sweetshop \r\n\r\nWe’d love to hear your thoughts so we can improve your experience and keep the shop evolving in the best way possible.");
|
||||
Master.AddScaledQuestion("How would you rate communication at the shop (including updates, clarity, and friendliness)?\r\n(1 = Poor | 10 = Excellent)");
|
||||
Master.AddScaledQuestion("How would you rate the quality of the products?\r\n(1 = Poor | 10 = Excellent)");
|
||||
}
|
||||
|
||||
var botClient = new TelegramBotClient(BotToken);
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var receiverOptions = new ReceiverOptions
|
||||
{
|
||||
AllowedUpdates = Array.Empty<UpdateType>() // Receive all updates
|
||||
};
|
||||
|
||||
botClient.StartReceiving(
|
||||
HandleUpdateAsync,
|
||||
HandleErrorAsync,
|
||||
receiverOptions,
|
||||
cancellationToken: cts.Token
|
||||
);
|
||||
|
||||
var me = await botClient.GetMeAsync();
|
||||
Console.WriteLine($"Bot started: {me.Username}");
|
||||
Console.ReadLine();
|
||||
cts.Cancel();
|
||||
}
|
||||
|
||||
private async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
|
||||
{
|
||||
if (update.Message is { } message)
|
||||
{
|
||||
|
||||
var chatId = message.Chat.Id;
|
||||
if (!Chats.ContainsKey(chatId))
|
||||
{
|
||||
var s = Master;
|
||||
Chats.Add(chatId, s);
|
||||
}
|
||||
|
||||
//if (message.Text == "/start")
|
||||
//{
|
||||
await ProcessMessage(botClient, chatId, cancellationToken);
|
||||
//var responseText = $"Hello, {message.From?.FirstName}! You said: {message.Text}";
|
||||
//await botClient.SendMessage(chatId, responseText, cancellationToken: cancellationToken);
|
||||
}
|
||||
else if (update.CallbackQuery is { } callbackQuery)
|
||||
{
|
||||
var data = callbackQuery.Data?.Split(':');
|
||||
var chatId = callbackQuery.Message.Chat.Id;
|
||||
var aID = Guid.Parse(data[0]);
|
||||
Chats[chatId].Answers.Add(aID, data[1]);
|
||||
|
||||
var response = $"Thank for choosing: {data[1]} in response to '{Chats[chatId].Questions.First(x => x.Key == aID).Value.Text}'";
|
||||
await botClient.SendTextMessageAsync(callbackQuery.Message.Chat.Id, response, cancellationToken: cancellationToken);
|
||||
Chats[chatId].Stage++;
|
||||
if (Chats[chatId].Stage > Chats[chatId].Questions.Count)
|
||||
{
|
||||
await botClient.SendTextMessageAsync(chatId, "Thank you for completing our questions, we appreciete your feedback!", cancellationToken: cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ProcessMessage(botClient, chatId, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessMessage(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Chats[chatId].Stage > Chats[chatId].Questions.Count)
|
||||
{
|
||||
await botClient.SendTextMessageAsync(chatId, "You have already completed the questionaire. Thank you for your feedback.", cancellationToken: cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (Chats[chatId].Stage)
|
||||
{
|
||||
case 0:
|
||||
await botClient.SendTextMessageAsync(chatId, Chats[chatId].WelcomeText, cancellationToken: cancellationToken);
|
||||
Chats[chatId].Stage++;
|
||||
break;
|
||||
default:
|
||||
var q = Chats[chatId].Questions.OrderBy(x => x.Value.Order).Skip(Chats[chatId].Stage - 1).Take(1).FirstOrDefault();
|
||||
var opts = new InlineKeyboardMarkup(new[]
|
||||
{
|
||||
q.Value.Options.Take(5).Select(x => InlineKeyboardButton.WithCallbackData(x, $"{q.Key}:{x}")),
|
||||
q.Value.Options.Skip(5).Select(x => InlineKeyboardButton.WithCallbackData(x, $"{q.Key}:{x}"))
|
||||
});
|
||||
await botClient.SendTextMessageAsync(chatId, q.Value.Text, replyMarkup: opts, cancellationToken: cancellationToken);
|
||||
|
||||
opts = new InlineKeyboardMarkup(q.Value.Options.Skip(5).Select(x => InlineKeyboardButton.WithCallbackData(x, $"{q.Key}:{x}")));
|
||||
await botClient.SendTextMessageAsync(chatId, "", replyMarkup: opts, cancellationToken: cancellationToken);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken)
|
||||
{
|
||||
Console.WriteLine($"Error: {exception.Message}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
72
TeleBot/TeleBot/TestCarousel.cs
Normal file
72
TeleBot/TeleBot/TestCarousel.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using LittleShop.Client.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using TeleBot.Services;
|
||||
|
||||
namespace TeleBot
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple test class to verify carousel functionality
|
||||
/// This can be used for testing the ProductCarouselService
|
||||
/// </summary>
|
||||
public class TestCarousel
|
||||
{
|
||||
public static async Task TestImageValidation()
|
||||
{
|
||||
var config = new ConfigurationBuilder().Build();
|
||||
var logger = NullLogger<ProductCarouselService>.Instance;
|
||||
var httpClient = new System.Net.Http.HttpClient();
|
||||
|
||||
var carouselService = new ProductCarouselService(config, logger, httpClient);
|
||||
|
||||
// Test image URL validation
|
||||
var validUrls = new[]
|
||||
{
|
||||
"https://via.placeholder.com/300x200.jpg",
|
||||
"https://picsum.photos/300/200",
|
||||
"https://httpbin.org/image/jpeg"
|
||||
};
|
||||
|
||||
foreach (var url in validUrls)
|
||||
{
|
||||
var isValid = await carouselService.IsImageUrlValidAsync(url);
|
||||
Console.WriteLine($"URL {url} is valid: {isValid}");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task TestProductImage()
|
||||
{
|
||||
var config = new ConfigurationBuilder().Build();
|
||||
var logger = NullLogger<ProductCarouselService>.Instance;
|
||||
var httpClient = new System.Net.Http.HttpClient();
|
||||
|
||||
var carouselService = new ProductCarouselService(config, logger, httpClient);
|
||||
|
||||
// Create a test product with image
|
||||
var testProduct = new Product
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Test Product",
|
||||
Price = 29.99m,
|
||||
Description = "A test product for carousel functionality",
|
||||
Photos = new List<ProductPhoto>
|
||||
{
|
||||
new ProductPhoto
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Url = "https://via.placeholder.com/300x200.jpg",
|
||||
IsMain = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Test getting product image
|
||||
var image = await carouselService.GetProductImageAsync(testProduct);
|
||||
Console.WriteLine($"Product image retrieved: {image != null}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,12 @@ namespace TeleBot.UI
|
||||
{
|
||||
return new InlineKeyboardMarkup(new[]
|
||||
{
|
||||
new[] { InlineKeyboardButton.WithCallbackData("🛍️ Browse Products", "browse") },
|
||||
new[] { InlineKeyboardButton.WithCallbackData("🛍️ Browse Categories", "browse") },
|
||||
new[] { InlineKeyboardButton.WithCallbackData("🖼️ View Products with Images", "products") },
|
||||
new[] { InlineKeyboardButton.WithCallbackData("🛒 View Cart", "cart") },
|
||||
new[] { InlineKeyboardButton.WithCallbackData("📦 My Orders", "orders") },
|
||||
new[] { InlineKeyboardButton.WithCallbackData("💬 Messages", "support") },
|
||||
new[] { InlineKeyboardButton.WithCallbackData("🔒 Privacy Settings", "privacy") },
|
||||
//new[] { InlineKeyboardButton.WithCallbackData("🔒 Privacy Settings", "privacy") },
|
||||
new[] { InlineKeyboardButton.WithCallbackData("❓ Help", "help") }
|
||||
});
|
||||
}
|
||||
@@ -328,7 +329,7 @@ namespace TeleBot.UI
|
||||
return new InlineKeyboardMarkup(new[]
|
||||
{
|
||||
new[] {
|
||||
InlineKeyboardButton.WithCallbackData("🛒 Quick Buy", $"add:{productId}:1"),
|
||||
InlineKeyboardButton.WithCallbackData("🛒 Buy Now", $"add:{productId}:1"),
|
||||
InlineKeyboardButton.WithCallbackData("📄 Details", $"product:{productId}")
|
||||
}
|
||||
});
|
||||
@@ -383,14 +384,14 @@ namespace TeleBot.UI
|
||||
_ => "📦"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(description))
|
||||
{
|
||||
// Truncate description for button
|
||||
var shortDesc = description.Length > 40
|
||||
? description.Substring(0, 37) + "..."
|
||||
: description;
|
||||
return $"{emoji} {categoryName}\n{shortDesc}";
|
||||
}
|
||||
// if (!string.IsNullOrEmpty(description))
|
||||
// {
|
||||
// // Truncate description for button
|
||||
// var shortDesc = description.Length > 40
|
||||
// ? description.Substring(0, 37) + "..."
|
||||
// : description;
|
||||
// return $"{emoji} {categoryName}\n\n\n{shortDesc}";
|
||||
// }
|
||||
|
||||
return $"{emoji} {categoryName}";
|
||||
}
|
||||
|
||||
@@ -12,18 +12,19 @@ namespace TeleBot.UI
|
||||
{
|
||||
if (isReturning)
|
||||
{
|
||||
return "🔒 *Welcome back to LittleShop*\n\n" +
|
||||
return $"🔒 *Welcome back to {Program.BrandName}*\n\n" +
|
||||
"Your privacy is our priority. All sessions are ephemeral by default.\n\n" +
|
||||
"🖼️ *New Feature:* Browse products with beautiful image carousels!\n\n" +
|
||||
"How can I help you today?";
|
||||
}
|
||||
|
||||
return "🔒 *Welcome to LittleShop - Privacy First E-Commerce*\n\n" +
|
||||
return $"🔒 *Welcome to {Program.BrandName}*\n\n" +
|
||||
"🛡️ *Your Privacy Matters:*\n" +
|
||||
"• No account required\n" +
|
||||
"• Ephemeral sessions by default\n" +
|
||||
"• Optional PGP encryption for shipping\n" +
|
||||
"• Cryptocurrency payments only\n" +
|
||||
"• Tor support available\n\n" +
|
||||
"• Cryptocurrency payments only\n\n" +
|
||||
"🖼️ *New Feature:* Browse products with beautiful image carousels!\n\n" +
|
||||
"Use /help for available commands or choose from the menu below:";
|
||||
}
|
||||
|
||||
@@ -262,9 +263,11 @@ namespace TeleBot.UI
|
||||
{
|
||||
return "*Available Commands:*\n\n" +
|
||||
"/start - Start shopping\n" +
|
||||
"/browse - Browse products\n" +
|
||||
"/browse - Browse categories\n" +
|
||||
"/products - View products with images\n" +
|
||||
"/cart - View shopping cart\n" +
|
||||
"/orders - View your orders\n" +
|
||||
"/review - Review shipped products\n" +
|
||||
"/support - View messages and chat\n" +
|
||||
"/privacy - Privacy settings\n" +
|
||||
"/pgpkey - Set PGP public key\n" +
|
||||
@@ -272,13 +275,7 @@ namespace TeleBot.UI
|
||||
"/cancel - Cancel current operation\n" +
|
||||
"/delete - Delete all your data\n" +
|
||||
"/tor - Get Tor onion address\n" +
|
||||
"/help - Show this help message\n\n" +
|
||||
"*Privacy Features:*\n" +
|
||||
"• All data is ephemeral by default\n" +
|
||||
"• Optional PGP encryption for shipping\n" +
|
||||
"• No personal data stored\n" +
|
||||
"• Anonymous order references\n" +
|
||||
"• Cryptocurrency payments only";
|
||||
"/help - Show this help message\n\n"
|
||||
}
|
||||
|
||||
public static string FormatPrivacyPolicy()
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"UseWebhook": false
|
||||
},
|
||||
"LittleShop": {
|
||||
"ApiUrl": "http://localhost:5000",
|
||||
"ApiUrl": "https://localhost:5001",
|
||||
"OnionUrl": "",
|
||||
"Username": "admin",
|
||||
"Password": "admin",
|
||||
|
||||
@@ -1,308 +1,325 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bogus;
|
||||
using LittleShop.Client;
|
||||
using LittleShop.Client.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace TeleBotClient
|
||||
{
|
||||
public class BotSimulator
|
||||
{
|
||||
private readonly ILittleShopClient _client;
|
||||
private readonly ILogger<BotSimulator> _logger;
|
||||
private readonly Random _random;
|
||||
private readonly Faker _faker;
|
||||
private List<Category> _categories = new();
|
||||
private List<Product> _products = new();
|
||||
|
||||
public BotSimulator(ILittleShopClient client, ILogger<BotSimulator> logger)
|
||||
{
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
_random = new Random();
|
||||
_faker = new Faker();
|
||||
}
|
||||
|
||||
public async Task<SimulationResult> SimulateUserSession()
|
||||
{
|
||||
var result = new SimulationResult
|
||||
{
|
||||
SessionId = Guid.NewGuid().ToString(),
|
||||
StartTime = DateTime.UtcNow
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("🤖 Starting bot simulation session {SessionId}", result.SessionId);
|
||||
|
||||
// Step 1: Authenticate
|
||||
_logger.LogInformation("📝 Authenticating with API...");
|
||||
if (!await Authenticate())
|
||||
{
|
||||
result.Success = false;
|
||||
result.ErrorMessage = "Authentication failed";
|
||||
return result;
|
||||
}
|
||||
result.Steps.Add("✅ Authentication successful");
|
||||
|
||||
// Step 2: Browse categories
|
||||
_logger.LogInformation("📁 Browsing categories...");
|
||||
await BrowseCategories();
|
||||
result.Steps.Add($"✅ Found {_categories.Count} categories");
|
||||
|
||||
// Step 3: Select random category and browse products
|
||||
if (_categories.Any())
|
||||
{
|
||||
var selectedCategory = _categories[_random.Next(_categories.Count)];
|
||||
_logger.LogInformation("🔍 Selected category: {Category}", selectedCategory.Name);
|
||||
result.Steps.Add($"✅ Selected category: {selectedCategory.Name}");
|
||||
|
||||
await BrowseProducts(selectedCategory.Id);
|
||||
result.Steps.Add($"✅ Found {_products.Count} products in category");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Browse all products if no categories
|
||||
await BrowseProducts(null);
|
||||
result.Steps.Add($"✅ Found {_products.Count} total products");
|
||||
}
|
||||
|
||||
// Step 4: Build shopping cart
|
||||
_logger.LogInformation("🛒 Building shopping cart...");
|
||||
var cart = BuildRandomCart();
|
||||
result.Cart = cart;
|
||||
result.Steps.Add($"✅ Added {cart.Items.Count} items to cart (Total: ${cart.TotalAmount:F2})");
|
||||
|
||||
// Step 5: Generate shipping information
|
||||
_logger.LogInformation("📦 Generating shipping information...");
|
||||
var shippingInfo = GenerateShippingInfo();
|
||||
result.ShippingInfo = shippingInfo;
|
||||
result.Steps.Add($"✅ Generated shipping to {shippingInfo.City}, {shippingInfo.Country}");
|
||||
|
||||
// Step 6: Create order
|
||||
_logger.LogInformation("📝 Creating order...");
|
||||
var order = await CreateOrder(cart, shippingInfo);
|
||||
if (order != null)
|
||||
{
|
||||
result.OrderId = order.Id;
|
||||
result.OrderTotal = order.TotalAmount;
|
||||
result.Steps.Add($"✅ Order created: {order.Id}");
|
||||
|
||||
// Step 7: Select payment method
|
||||
var currency = SelectRandomCurrency();
|
||||
_logger.LogInformation("💰 Selected payment method: {Currency}", currency);
|
||||
result.PaymentCurrency = currency;
|
||||
result.Steps.Add($"✅ Selected payment: {currency}");
|
||||
|
||||
// Step 8: Create payment
|
||||
var payment = await CreatePayment(order.Id, currency);
|
||||
if (payment != null)
|
||||
{
|
||||
result.PaymentId = payment.Id;
|
||||
result.PaymentAddress = payment.WalletAddress;
|
||||
result.PaymentAmount = payment.RequiredAmount;
|
||||
result.Steps.Add($"✅ Payment created: {payment.RequiredAmount} {currency}");
|
||||
}
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
result.EndTime = DateTime.UtcNow;
|
||||
result.Duration = result.EndTime - result.StartTime;
|
||||
|
||||
_logger.LogInformation("✅ Simulation completed successfully in {Duration}", result.Duration);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "❌ Simulation failed");
|
||||
result.Success = false;
|
||||
result.ErrorMessage = ex.Message;
|
||||
result.EndTime = DateTime.UtcNow;
|
||||
result.Duration = result.EndTime - result.StartTime;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<bool> Authenticate()
|
||||
{
|
||||
var result = await _client.Authentication.LoginAsync("admin", "admin");
|
||||
if (result.IsSuccess && result.Data != null && !string.IsNullOrEmpty(result.Data.Token))
|
||||
{
|
||||
_client.Authentication.SetToken(result.Data.Token);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task BrowseCategories()
|
||||
{
|
||||
var result = await _client.Catalog.GetCategoriesAsync();
|
||||
if (result.IsSuccess && result.Data != null)
|
||||
{
|
||||
_categories = result.Data;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BrowseProducts(Guid? categoryId)
|
||||
{
|
||||
var result = await _client.Catalog.GetProductsAsync(
|
||||
pageNumber: 1,
|
||||
pageSize: 50,
|
||||
categoryId: categoryId
|
||||
);
|
||||
|
||||
if (result.IsSuccess && result.Data != null)
|
||||
{
|
||||
_products = result.Data.Items;
|
||||
}
|
||||
}
|
||||
|
||||
private ShoppingCart BuildRandomCart()
|
||||
{
|
||||
var cart = new ShoppingCart();
|
||||
|
||||
if (!_products.Any())
|
||||
return cart;
|
||||
|
||||
// Random number of items (1-5)
|
||||
var itemCount = _random.Next(1, Math.Min(6, _products.Count + 1));
|
||||
var selectedProducts = _products.OrderBy(x => _random.Next()).Take(itemCount).ToList();
|
||||
|
||||
foreach (var product in selectedProducts)
|
||||
{
|
||||
var quantity = _random.Next(1, 4); // 1-3 items
|
||||
cart.AddItem(product.Id, product.Name, product.Price, quantity);
|
||||
_logger.LogDebug("Added {Quantity}x {Product} @ ${Price}",
|
||||
quantity, product.Name, product.Price);
|
||||
}
|
||||
|
||||
return cart;
|
||||
}
|
||||
|
||||
private ShippingInfo GenerateShippingInfo()
|
||||
{
|
||||
return new ShippingInfo
|
||||
{
|
||||
IdentityReference = $"SIM-{Guid.NewGuid().ToString().Substring(0, 8).ToUpper()}",
|
||||
Name = _faker.Name.FullName(),
|
||||
Address = _faker.Address.StreetAddress(),
|
||||
City = _faker.Address.City(),
|
||||
PostCode = _faker.Address.ZipCode(),
|
||||
Country = _faker.PickRandom(new[] { "United Kingdom", "Ireland", "France", "Germany", "Netherlands" }),
|
||||
Notes = _faker.Lorem.Sentence()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Order?> CreateOrder(ShoppingCart cart, ShippingInfo shipping)
|
||||
{
|
||||
var request = new CreateOrderRequest
|
||||
{
|
||||
IdentityReference = shipping.IdentityReference,
|
||||
ShippingName = shipping.Name,
|
||||
ShippingAddress = shipping.Address,
|
||||
ShippingCity = shipping.City,
|
||||
ShippingPostCode = shipping.PostCode,
|
||||
ShippingCountry = shipping.Country,
|
||||
Notes = shipping.Notes,
|
||||
Items = cart.Items.Select(i => new CreateOrderItem
|
||||
{
|
||||
ProductId = i.ProductId,
|
||||
Quantity = i.Quantity
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var result = await _client.Orders.CreateOrderAsync(request);
|
||||
return result.IsSuccess ? result.Data : null;
|
||||
}
|
||||
|
||||
private string SelectRandomCurrency()
|
||||
{
|
||||
var currencies = new[] { "BTC", "XMR", "USDT", "LTC", "ETH", "ZEC", "DASH", "DOGE" };
|
||||
|
||||
// Weight towards BTC and XMR
|
||||
var weights = new[] { 30, 25, 15, 10, 10, 5, 3, 2 };
|
||||
var totalWeight = weights.Sum();
|
||||
var randomValue = _random.Next(totalWeight);
|
||||
|
||||
var currentWeight = 0;
|
||||
for (int i = 0; i < currencies.Length; i++)
|
||||
{
|
||||
currentWeight += weights[i];
|
||||
if (randomValue < currentWeight)
|
||||
return currencies[i];
|
||||
}
|
||||
|
||||
return "BTC";
|
||||
}
|
||||
|
||||
private async Task<CryptoPayment?> CreatePayment(Guid orderId, string currency)
|
||||
{
|
||||
var result = await _client.Orders.CreatePaymentAsync(orderId, currency);
|
||||
return result.IsSuccess ? result.Data : null;
|
||||
}
|
||||
}
|
||||
|
||||
public class SimulationResult
|
||||
{
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime EndTime { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
public List<string> Steps { get; set; } = new();
|
||||
|
||||
// Order details
|
||||
public Guid? OrderId { get; set; }
|
||||
public decimal OrderTotal { get; set; }
|
||||
public ShoppingCart? Cart { get; set; }
|
||||
public ShippingInfo? ShippingInfo { get; set; }
|
||||
|
||||
// Payment details
|
||||
public Guid? PaymentId { get; set; }
|
||||
public string? PaymentCurrency { get; set; }
|
||||
public string? PaymentAddress { get; set; }
|
||||
public decimal PaymentAmount { get; set; }
|
||||
}
|
||||
|
||||
public class ShoppingCart
|
||||
{
|
||||
public List<CartItem> Items { get; set; } = new();
|
||||
public decimal TotalAmount => Items.Sum(i => i.TotalPrice);
|
||||
|
||||
public void AddItem(Guid productId, string name, decimal price, int quantity)
|
||||
{
|
||||
Items.Add(new CartItem
|
||||
{
|
||||
ProductId = productId,
|
||||
ProductName = name,
|
||||
UnitPrice = price,
|
||||
Quantity = quantity,
|
||||
TotalPrice = price * quantity
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class CartItem
|
||||
{
|
||||
public Guid ProductId { get; set; }
|
||||
public string ProductName { get; set; } = string.Empty;
|
||||
public decimal UnitPrice { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
public decimal TotalPrice { get; set; }
|
||||
}
|
||||
|
||||
public class ShippingInfo
|
||||
{
|
||||
public string IdentityReference { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Address { get; set; } = string.Empty;
|
||||
public string City { get; set; } = string.Empty;
|
||||
public string PostCode { get; set; } = string.Empty;
|
||||
public string Country { get; set; } = string.Empty;
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bogus;
|
||||
using LittleShop.Client;
|
||||
using LittleShop.Client.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace TeleBotClient
|
||||
{
|
||||
public class BotSimulator
|
||||
{
|
||||
private readonly ILittleShopClient _client;
|
||||
private readonly ILogger<BotSimulator> _logger;
|
||||
private readonly Random _random;
|
||||
private readonly Faker _faker;
|
||||
private List<Category> _categories = new();
|
||||
private List<Product> _products = new();
|
||||
|
||||
public BotSimulator(ILittleShopClient client, ILogger<BotSimulator> logger)
|
||||
{
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
_random = new Random();
|
||||
_faker = new Faker();
|
||||
}
|
||||
|
||||
public async Task<SimulationResult> SimulateUserSession()
|
||||
{
|
||||
var result = new SimulationResult
|
||||
{
|
||||
SessionId = Guid.NewGuid().ToString(),
|
||||
StartTime = DateTime.UtcNow
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("🤖 Starting bot simulation session {SessionId}", result.SessionId);
|
||||
|
||||
// Step 1: Authenticate
|
||||
_logger.LogInformation("📝 Authenticating with API...");
|
||||
if (!await Authenticate())
|
||||
{
|
||||
result.Success = false;
|
||||
result.ErrorMessage = "Authentication failed";
|
||||
return result;
|
||||
}
|
||||
result.Steps.Add("✅ Authentication successful");
|
||||
|
||||
// Step 2: Browse categories
|
||||
_logger.LogInformation("📁 Browsing categories...");
|
||||
await BrowseCategories();
|
||||
result.Steps.Add($"✅ Found {_categories.Count} categories");
|
||||
|
||||
// Step 3: Select random category and browse products
|
||||
if (_categories.Any())
|
||||
{
|
||||
var selectedCategory = _categories[_random.Next(_categories.Count)];
|
||||
_logger.LogInformation("🔍 Selected category: {Category}", selectedCategory.Name);
|
||||
result.Steps.Add($"✅ Selected category: {selectedCategory.Name}");
|
||||
|
||||
await BrowseProducts(selectedCategory.Id);
|
||||
result.Steps.Add($"✅ Found {_products.Count} products in category");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Browse all products if no categories
|
||||
await BrowseProducts(null);
|
||||
result.Steps.Add($"✅ Found {_products.Count} total products");
|
||||
}
|
||||
|
||||
// Step 4: Build shopping cart
|
||||
_logger.LogInformation("🛒 Building shopping cart...");
|
||||
var cart = BuildRandomCart();
|
||||
result.Cart = cart;
|
||||
result.Steps.Add($"✅ Added {cart.Items.Count} items to cart (Total: ${cart.TotalAmount:F2})");
|
||||
|
||||
// Step 5: Generate shipping information
|
||||
_logger.LogInformation("📦 Generating shipping information...");
|
||||
var shippingInfo = GenerateShippingInfo();
|
||||
result.ShippingInfo = shippingInfo;
|
||||
result.Steps.Add($"✅ Generated shipping to {shippingInfo.City}, {shippingInfo.Country}");
|
||||
|
||||
// Step 6: Create order
|
||||
_logger.LogInformation("📝 Creating order...");
|
||||
var order = await CreateOrder(cart, shippingInfo);
|
||||
if (order != null)
|
||||
{
|
||||
result.OrderId = order.Id;
|
||||
result.OrderTotal = order.TotalAmount;
|
||||
result.Steps.Add($"✅ Order created: {order.Id}");
|
||||
|
||||
// Step 7: Select payment method
|
||||
var currency = SelectRandomCurrency();
|
||||
_logger.LogInformation("💰 Selected payment method: {Currency}", currency);
|
||||
result.PaymentCurrency = currency;
|
||||
result.Steps.Add($"✅ Selected payment: {currency}");
|
||||
|
||||
// Step 8: Create payment
|
||||
var payment = await CreatePayment(order.Id, currency);
|
||||
if (payment != null)
|
||||
{
|
||||
result.PaymentId = payment.Id;
|
||||
result.PaymentAddress = payment.WalletAddress;
|
||||
result.PaymentAmount = payment.RequiredAmount;
|
||||
result.Steps.Add($"✅ Payment created: {payment.RequiredAmount} {currency}");
|
||||
}
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
result.EndTime = DateTime.UtcNow;
|
||||
result.Duration = result.EndTime - result.StartTime;
|
||||
|
||||
_logger.LogInformation("✅ Simulation completed successfully in {Duration}", result.Duration);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "❌ Simulation failed");
|
||||
result.Success = false;
|
||||
result.ErrorMessage = ex.Message;
|
||||
result.EndTime = DateTime.UtcNow;
|
||||
result.Duration = result.EndTime - result.StartTime;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<bool> Authenticate()
|
||||
{
|
||||
var result = await _client.Authentication.LoginAsync("admin", "admin");
|
||||
if (result.IsSuccess && result.Data != null && !string.IsNullOrEmpty(result.Data.Token))
|
||||
{
|
||||
_client.Authentication.SetToken(result.Data.Token);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task BrowseCategories()
|
||||
{
|
||||
var result = await _client.Catalog.GetCategoriesAsync();
|
||||
if (result.IsSuccess && result.Data != null)
|
||||
{
|
||||
_categories = result.Data;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BrowseProducts(Guid? categoryId)
|
||||
{
|
||||
var result = await _client.Catalog.GetProductsAsync(
|
||||
pageNumber: 1,
|
||||
pageSize: 50,
|
||||
categoryId: categoryId
|
||||
);
|
||||
|
||||
if (result.IsSuccess && result.Data != null)
|
||||
{
|
||||
_products = result.Data.Items;
|
||||
}
|
||||
}
|
||||
|
||||
private ShoppingCart BuildRandomCart()
|
||||
{
|
||||
var cart = new ShoppingCart();
|
||||
|
||||
if (!_products.Any())
|
||||
return cart;
|
||||
|
||||
// Random number of items (1-5)
|
||||
var itemCount = _random.Next(1, Math.Min(6, _products.Count + 1));
|
||||
var selectedProducts = _products.OrderBy(x => _random.Next()).Take(itemCount).ToList();
|
||||
|
||||
foreach (var product in selectedProducts)
|
||||
{
|
||||
var quantity = _random.Next(1, 4); // 1-3 items
|
||||
cart.AddItem(product.Id, product.Name, product.Price, quantity);
|
||||
_logger.LogDebug("Added {Quantity}x {Product} @ ${Price}",
|
||||
quantity, product.Name, product.Price);
|
||||
}
|
||||
|
||||
return cart;
|
||||
}
|
||||
|
||||
private ShippingInfo GenerateShippingInfo()
|
||||
{
|
||||
return new ShippingInfo
|
||||
{
|
||||
IdentityReference = $"SIM-{Guid.NewGuid().ToString().Substring(0, 8).ToUpper()}",
|
||||
Name = _faker.Name.FullName(),
|
||||
Address = _faker.Address.StreetAddress(),
|
||||
City = _faker.Address.City(),
|
||||
PostCode = _faker.Address.ZipCode(),
|
||||
Country = _faker.PickRandom(new[] { "United Kingdom", "Ireland", "France", "Germany", "Netherlands" }),
|
||||
Notes = _faker.Lorem.Sentence()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Order?> CreateOrder(ShoppingCart cart, ShippingInfo shipping)
|
||||
{
|
||||
var request = new CreateOrderRequest
|
||||
{
|
||||
IdentityReference = shipping.IdentityReference,
|
||||
ShippingName = shipping.Name,
|
||||
ShippingAddress = shipping.Address,
|
||||
ShippingCity = shipping.City,
|
||||
ShippingPostCode = shipping.PostCode,
|
||||
ShippingCountry = shipping.Country,
|
||||
Notes = shipping.Notes,
|
||||
Items = cart.Items.Select(i => new CreateOrderItem
|
||||
{
|
||||
ProductId = i.ProductId,
|
||||
Quantity = i.Quantity
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var result = await _client.Orders.CreateOrderAsync(request);
|
||||
return result.IsSuccess ? result.Data : null;
|
||||
}
|
||||
|
||||
private string SelectRandomCurrency()
|
||||
{
|
||||
var currencies = new[] { "BTC", "XMR", "USDT", "LTC", "ETH", "ZEC", "DASH", "DOGE" };
|
||||
|
||||
// Weight towards BTC and XMR
|
||||
var weights = new[] { 30, 25, 15, 10, 10, 5, 3, 2 };
|
||||
var totalWeight = weights.Sum();
|
||||
var randomValue = _random.Next(totalWeight);
|
||||
|
||||
var currentWeight = 0;
|
||||
for (int i = 0; i < currencies.Length; i++)
|
||||
{
|
||||
currentWeight += weights[i];
|
||||
if (randomValue < currentWeight)
|
||||
return currencies[i];
|
||||
}
|
||||
|
||||
return "BTC";
|
||||
}
|
||||
|
||||
private async Task<CryptoPayment?> CreatePayment(Guid orderId, string currency)
|
||||
{
|
||||
var currencyInt = ConvertCurrencyToEnum(currency);
|
||||
var result = await _client.Orders.CreatePaymentAsync(orderId, currencyInt);
|
||||
return result.IsSuccess ? result.Data : null;
|
||||
}
|
||||
|
||||
private static int ConvertCurrencyToEnum(string currency)
|
||||
{
|
||||
return currency.ToUpper() switch
|
||||
{
|
||||
"BTC" => 0,
|
||||
"XMR" => 1,
|
||||
"USDT" => 2,
|
||||
"LTC" => 3,
|
||||
"ETH" => 4,
|
||||
"ZEC" => 5,
|
||||
"DASH" => 6,
|
||||
"DOGE" => 7,
|
||||
_ => 0 // Default to BTC
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class SimulationResult
|
||||
{
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime EndTime { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
public List<string> Steps { get; set; } = new();
|
||||
|
||||
// Order details
|
||||
public Guid? OrderId { get; set; }
|
||||
public decimal OrderTotal { get; set; }
|
||||
public ShoppingCart? Cart { get; set; }
|
||||
public ShippingInfo? ShippingInfo { get; set; }
|
||||
|
||||
// Payment details
|
||||
public Guid? PaymentId { get; set; }
|
||||
public string? PaymentCurrency { get; set; }
|
||||
public string? PaymentAddress { get; set; }
|
||||
public decimal PaymentAmount { get; set; }
|
||||
}
|
||||
|
||||
public class ShoppingCart
|
||||
{
|
||||
public List<CartItem> Items { get; set; } = new();
|
||||
public decimal TotalAmount => Items.Sum(i => i.TotalPrice);
|
||||
|
||||
public void AddItem(Guid productId, string name, decimal price, int quantity)
|
||||
{
|
||||
Items.Add(new CartItem
|
||||
{
|
||||
ProductId = productId,
|
||||
ProductName = name,
|
||||
UnitPrice = price,
|
||||
Quantity = quantity,
|
||||
TotalPrice = price * quantity
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class CartItem
|
||||
{
|
||||
public Guid ProductId { get; set; }
|
||||
public string ProductName { get; set; } = string.Empty;
|
||||
public decimal UnitPrice { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
public decimal TotalPrice { get; set; }
|
||||
}
|
||||
|
||||
public class ShippingInfo
|
||||
{
|
||||
public string IdentityReference { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Address { get; set; } = string.Empty;
|
||||
public string City { get; set; } = string.Empty;
|
||||
public string PostCode { get; set; } = string.Empty;
|
||||
public string Country { get; set; } = string.Empty;
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,96 +1,96 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
class TelegramClient
|
||||
{
|
||||
private static readonly string BotToken = "7330819864:AAHx9GEL-G-WeH2ON5-ncdsbbhV6YaOqZzg";
|
||||
private static readonly string ApiUrl = $"https://api.telegram.org/bot{BotToken}/";
|
||||
|
||||
static async Task Main()
|
||||
{
|
||||
Console.WriteLine("Telegram Bot Client Started...");
|
||||
|
||||
while (true)
|
||||
{
|
||||
await ReceiveMessagesAsync();
|
||||
await Task.Delay(5000); // Polling delay
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SendMessageAsync(string chatId, string message)
|
||||
{
|
||||
using HttpClient client = new HttpClient();
|
||||
var payload = new
|
||||
{
|
||||
chat_id = chatId,
|
||||
text = message
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
try
|
||||
{
|
||||
var response = await client.PostAsync(ApiUrl + "sendMessage", content);
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Console.WriteLine($"Response: {responseText}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error sending message: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReceiveMessagesAsync()
|
||||
{
|
||||
using HttpClient client = new HttpClient();
|
||||
try
|
||||
{
|
||||
var response = await client.GetAsync(ApiUrl + "getUpdates");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
var updates = JsonSerializer.Deserialize<TelegramUpdateResponse>(responseText);
|
||||
|
||||
if (updates?.Result != null)
|
||||
{
|
||||
foreach (var update in updates.Result)
|
||||
{
|
||||
Console.WriteLine($"Received message from {update.Message.Chat.Id}: {update.Message.Text}");
|
||||
await SendMessageAsync(update.Message.Chat.Id.ToString(), "Message received!");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
Console.WriteLine($"HTTP error: {httpEx.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Unexpected error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TelegramUpdateResponse
|
||||
{
|
||||
public TelegramUpdate[] Result { get; set; }
|
||||
}
|
||||
|
||||
class TelegramUpdate
|
||||
{
|
||||
public TelegramMessage Message { get; set; }
|
||||
}
|
||||
|
||||
class TelegramMessage
|
||||
{
|
||||
public TelegramChat Chat { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
||||
|
||||
class TelegramChat
|
||||
{
|
||||
public long Id { get; set; }
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
class TelegramClient
|
||||
{
|
||||
private static readonly string BotToken = "7330819864:AAHx9GEL-G-WeH2ON5-ncdsbbhV6YaOqZzg";
|
||||
private static readonly string ApiUrl = $"https://api.telegram.org/bot{BotToken}/";
|
||||
|
||||
static async Task Main()
|
||||
{
|
||||
Console.WriteLine("Telegram Bot Client Started...");
|
||||
|
||||
while (true)
|
||||
{
|
||||
await ReceiveMessagesAsync();
|
||||
await Task.Delay(5000); // Polling delay
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SendMessageAsync(string chatId, string message)
|
||||
{
|
||||
using HttpClient client = new HttpClient();
|
||||
var payload = new
|
||||
{
|
||||
chat_id = chatId,
|
||||
text = message
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
try
|
||||
{
|
||||
var response = await client.PostAsync(ApiUrl + "sendMessage", content);
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Console.WriteLine($"Response: {responseText}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error sending message: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReceiveMessagesAsync()
|
||||
{
|
||||
using HttpClient client = new HttpClient();
|
||||
try
|
||||
{
|
||||
var response = await client.GetAsync(ApiUrl + "getUpdates");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
var updates = JsonSerializer.Deserialize<TelegramUpdateResponse>(responseText);
|
||||
|
||||
if (updates?.Result != null)
|
||||
{
|
||||
foreach (var update in updates.Result)
|
||||
{
|
||||
Console.WriteLine($"Received message from {update.Message.Chat.Id}: {update.Message.Text}");
|
||||
await SendMessageAsync(update.Message.Chat.Id.ToString(), "Message received!");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
Console.WriteLine($"HTTP error: {httpEx.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Unexpected error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TelegramUpdateResponse
|
||||
{
|
||||
public TelegramUpdate[] Result { get; set; }
|
||||
}
|
||||
|
||||
class TelegramUpdate
|
||||
{
|
||||
public TelegramMessage Message { get; set; }
|
||||
}
|
||||
|
||||
class TelegramMessage
|
||||
{
|
||||
public TelegramChat Chat { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
||||
|
||||
class TelegramChat
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
@@ -1,367 +1,367 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LittleShop.Client.Extensions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TeleBotClient;
|
||||
|
||||
namespace TeleBotClient
|
||||
{
|
||||
public class SimulatorProgram
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// Configure Serilog
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.WriteTo.File("logs/simulator-.txt", rollingInterval: RollingInterval.Day)
|
||||
.CreateLogger();
|
||||
|
||||
try
|
||||
{
|
||||
// Build configuration
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: false)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
// Configure services
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Add logging
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
builder.ClearProviders();
|
||||
builder.AddSerilog();
|
||||
});
|
||||
|
||||
// Add LittleShop client
|
||||
services.AddLittleShopClient(options =>
|
||||
{
|
||||
options.BaseUrl = configuration["LittleShop:ApiUrl"] ?? "https://localhost:5001";
|
||||
options.TimeoutSeconds = 30;
|
||||
options.MaxRetryAttempts = 3;
|
||||
});
|
||||
|
||||
// Add simulator
|
||||
services.AddTransient<BotSimulator>();
|
||||
services.AddTransient<TestRunner>();
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Run tests
|
||||
var runner = serviceProvider.GetRequiredService<TestRunner>();
|
||||
await runner.RunAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Application terminated unexpectedly");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TestRunner
|
||||
{
|
||||
private readonly BotSimulator _simulator;
|
||||
private readonly ILogger<TestRunner> _logger;
|
||||
private readonly List<SimulationResult> _allResults = new();
|
||||
|
||||
public TestRunner(BotSimulator simulator, ILogger<TestRunner> logger)
|
||||
{
|
||||
_simulator = simulator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
_logger.LogInformation("===========================================");
|
||||
_logger.LogInformation("🚀 TeleBot Client Simulator");
|
||||
_logger.LogInformation("===========================================");
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.WriteLine("\n📋 Select an option:");
|
||||
Console.WriteLine("1. Run single simulation");
|
||||
Console.WriteLine("2. Run multiple simulations");
|
||||
Console.WriteLine("3. Run stress test");
|
||||
Console.WriteLine("4. View statistics");
|
||||
Console.WriteLine("5. Exit");
|
||||
Console.Write("\nChoice: ");
|
||||
|
||||
var choice = Console.ReadLine();
|
||||
|
||||
switch (choice)
|
||||
{
|
||||
case "1":
|
||||
await RunSingleSimulation();
|
||||
break;
|
||||
|
||||
case "2":
|
||||
await RunMultipleSimulations();
|
||||
break;
|
||||
|
||||
case "3":
|
||||
await RunStressTest();
|
||||
break;
|
||||
|
||||
case "4":
|
||||
DisplayStatistics();
|
||||
break;
|
||||
|
||||
case "5":
|
||||
_logger.LogInformation("Exiting simulator...");
|
||||
return;
|
||||
|
||||
default:
|
||||
Console.WriteLine("Invalid choice. Please try again.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunSingleSimulation()
|
||||
{
|
||||
_logger.LogInformation("\n🎯 Running single simulation...\n");
|
||||
|
||||
var result = await _simulator.SimulateUserSession();
|
||||
_allResults.Add(result);
|
||||
|
||||
DisplaySimulationResult(result);
|
||||
}
|
||||
|
||||
private async Task RunMultipleSimulations()
|
||||
{
|
||||
Console.Write("\nHow many simulations to run? ");
|
||||
if (!int.TryParse(Console.ReadLine(), out var count) || count <= 0)
|
||||
{
|
||||
Console.WriteLine("Invalid number.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("\n🎯 Running {Count} simulations...\n", count);
|
||||
|
||||
var results = new List<SimulationResult>();
|
||||
var successful = 0;
|
||||
var failed = 0;
|
||||
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
_logger.LogInformation("▶️ Simulation {Number}/{Total}", i, count);
|
||||
|
||||
var result = await _simulator.SimulateUserSession();
|
||||
results.Add(result);
|
||||
_allResults.Add(result);
|
||||
|
||||
if (result.Success)
|
||||
successful++;
|
||||
else
|
||||
failed++;
|
||||
|
||||
// Brief pause between simulations
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
DisplayBatchSummary(results, successful, failed);
|
||||
}
|
||||
|
||||
private async Task RunStressTest()
|
||||
{
|
||||
Console.Write("\nNumber of concurrent simulations: ");
|
||||
if (!int.TryParse(Console.ReadLine(), out var concurrent) || concurrent <= 0)
|
||||
{
|
||||
Console.WriteLine("Invalid number.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Write("Total simulations to run: ");
|
||||
if (!int.TryParse(Console.ReadLine(), out var total) || total <= 0)
|
||||
{
|
||||
Console.WriteLine("Invalid number.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("\n⚡ Starting stress test: {Concurrent} concurrent, {Total} total\n",
|
||||
concurrent, total);
|
||||
|
||||
var semaphore = new SemaphoreSlim(concurrent);
|
||||
var tasks = new List<Task<SimulationResult>>();
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
await semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
return await _simulator.SimulateUserSession();
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
});
|
||||
tasks.Add(task);
|
||||
}
|
||||
|
||||
var allResults = await Task.WhenAll(tasks);
|
||||
var results = allResults.ToList();
|
||||
_allResults.AddRange(results);
|
||||
|
||||
var duration = DateTime.UtcNow - startTime;
|
||||
|
||||
DisplayStressTestResults(results, duration, concurrent, total);
|
||||
}
|
||||
|
||||
private void DisplayStatistics()
|
||||
{
|
||||
if (!_allResults.Any())
|
||||
{
|
||||
Console.WriteLine("\n📊 No simulation data available yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("\n📊 Session Statistics:");
|
||||
Console.WriteLine($" Total Simulations: {_allResults.Count}");
|
||||
Console.WriteLine($" Successful: {_allResults.Count(r => r.Success)}");
|
||||
Console.WriteLine($" Failed: {_allResults.Count(r => !r.Success)}");
|
||||
Console.WriteLine($" Success Rate: {(_allResults.Count(r => r.Success) * 100.0 / _allResults.Count):F1}%");
|
||||
|
||||
var successful = _allResults.Where(r => r.Success).ToList();
|
||||
if (successful.Any())
|
||||
{
|
||||
Console.WriteLine($"\n💰 Order Statistics:");
|
||||
Console.WriteLine($" Total Orders: {successful.Count}");
|
||||
Console.WriteLine($" Total Revenue: ${successful.Sum(r => r.OrderTotal):F2}");
|
||||
Console.WriteLine($" Average Order: ${successful.Average(r => r.OrderTotal):F2}");
|
||||
Console.WriteLine($" Min Order: ${successful.Min(r => r.OrderTotal):F2}");
|
||||
Console.WriteLine($" Max Order: ${successful.Max(r => r.OrderTotal):F2}");
|
||||
|
||||
// Payment distribution
|
||||
var payments = successful
|
||||
.Where(r => !string.IsNullOrEmpty(r.PaymentCurrency))
|
||||
.GroupBy(r => r.PaymentCurrency)
|
||||
.Select(g => new { Currency = g.Key, Count = g.Count() })
|
||||
.OrderByDescending(x => x.Count);
|
||||
|
||||
Console.WriteLine($"\n💳 Payment Methods:");
|
||||
foreach (var p in payments)
|
||||
{
|
||||
Console.WriteLine($" {p.Currency}: {p.Count} ({p.Count * 100.0 / successful.Count:F1}%)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisplaySimulationResult(SimulationResult result)
|
||||
{
|
||||
Console.WriteLine("\n========================================");
|
||||
Console.WriteLine($"📋 Simulation Result: {result.SessionId}");
|
||||
Console.WriteLine("========================================");
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
Console.WriteLine("✅ Status: SUCCESS");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"❌ Status: FAILED - {result.ErrorMessage}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"⏱️ Duration: {result.Duration.TotalSeconds:F2}s");
|
||||
|
||||
if (result.Steps.Any())
|
||||
{
|
||||
Console.WriteLine("\n📝 Steps Completed:");
|
||||
foreach (var step in result.Steps)
|
||||
{
|
||||
Console.WriteLine($" {step}");
|
||||
}
|
||||
}
|
||||
|
||||
if (result.Cart != null && result.Cart.Items.Any())
|
||||
{
|
||||
Console.WriteLine($"\n🛒 Shopping Cart ({result.Cart.Items.Count} items):");
|
||||
foreach (var item in result.Cart.Items)
|
||||
{
|
||||
Console.WriteLine($" - {item.Quantity}x {item.ProductName} @ ${item.UnitPrice:F2} = ${item.TotalPrice:F2}");
|
||||
}
|
||||
Console.WriteLine($" Total: ${result.Cart.TotalAmount:F2}");
|
||||
}
|
||||
|
||||
if (result.OrderId.HasValue)
|
||||
{
|
||||
Console.WriteLine($"\n📝 Order Details:");
|
||||
Console.WriteLine($" Order ID: {result.OrderId}");
|
||||
Console.WriteLine($" Total: ${result.OrderTotal:F2}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result.PaymentCurrency))
|
||||
{
|
||||
Console.WriteLine($"\n💰 Payment Details:");
|
||||
Console.WriteLine($" Currency: {result.PaymentCurrency}");
|
||||
Console.WriteLine($" Amount: {result.PaymentAmount}");
|
||||
}
|
||||
|
||||
Console.WriteLine("\n========================================");
|
||||
}
|
||||
|
||||
private void DisplayBatchSummary(List<SimulationResult> results, int successful, int failed)
|
||||
{
|
||||
Console.WriteLine("\n📊 Batch Summary:");
|
||||
Console.WriteLine($"✅ Successful: {successful}");
|
||||
Console.WriteLine($"❌ Failed: {failed}");
|
||||
Console.WriteLine($"📈 Success Rate: {(successful * 100.0 / results.Count):F1}%");
|
||||
|
||||
if (results.Any(r => r.Success))
|
||||
{
|
||||
var successfulResults = results.Where(r => r.Success).ToList();
|
||||
Console.WriteLine($"\n💰 Order Statistics:");
|
||||
Console.WriteLine($" Average Order: ${successfulResults.Average(r => r.OrderTotal):F2}");
|
||||
Console.WriteLine($" Total Revenue: ${successfulResults.Sum(r => r.OrderTotal):F2}");
|
||||
Console.WriteLine($" Average Duration: {successfulResults.Average(r => r.Duration.TotalSeconds):F1}s");
|
||||
}
|
||||
}
|
||||
|
||||
private void DisplayStressTestResults(List<SimulationResult> results, TimeSpan duration, int concurrent, int total)
|
||||
{
|
||||
var successful = results.Count(r => r.Success);
|
||||
var failed = results.Count(r => !r.Success);
|
||||
|
||||
Console.WriteLine("\n📊 Stress Test Results:");
|
||||
Console.WriteLine($"⏱️ Total Duration: {duration.TotalSeconds:F1}s");
|
||||
Console.WriteLine($"✅ Successful: {successful}");
|
||||
Console.WriteLine($"❌ Failed: {failed}");
|
||||
Console.WriteLine($"📈 Success Rate: {(successful * 100.0 / total):F1}%");
|
||||
Console.WriteLine($"⚡ Throughput: {(total / duration.TotalSeconds):F2} simulations/second");
|
||||
Console.WriteLine($"🔄 Concurrency: {concurrent} simultaneous connections");
|
||||
|
||||
if (failed > 0)
|
||||
{
|
||||
Console.WriteLine("\n❌ Failure Analysis:");
|
||||
var errors = results
|
||||
.Where(r => !r.Success && !string.IsNullOrEmpty(r.ErrorMessage))
|
||||
.GroupBy(r => r.ErrorMessage)
|
||||
.Select(g => new { Error = g.Key, Count = g.Count() })
|
||||
.OrderByDescending(x => x.Count)
|
||||
.Take(5);
|
||||
|
||||
foreach (var error in errors)
|
||||
{
|
||||
Console.WriteLine($" {error.Error}: {error.Count}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LittleShop.Client.Extensions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TeleBotClient;
|
||||
|
||||
namespace TeleBotClient
|
||||
{
|
||||
public class SimulatorProgram
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// Configure Serilog
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.WriteTo.File("logs/simulator-.txt", rollingInterval: RollingInterval.Day)
|
||||
.CreateLogger();
|
||||
|
||||
try
|
||||
{
|
||||
// Build configuration
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: false)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
// Configure services
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Add logging
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
builder.ClearProviders();
|
||||
builder.AddSerilog();
|
||||
});
|
||||
|
||||
// Add LittleShop client
|
||||
services.AddLittleShopClient(options =>
|
||||
{
|
||||
options.BaseUrl = configuration["LittleShop:ApiUrl"] ?? "https://localhost:5001";
|
||||
options.TimeoutSeconds = 30;
|
||||
options.MaxRetryAttempts = 3;
|
||||
});
|
||||
|
||||
// Add simulator
|
||||
services.AddTransient<BotSimulator>();
|
||||
services.AddTransient<TestRunner>();
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Run tests
|
||||
var runner = serviceProvider.GetRequiredService<TestRunner>();
|
||||
await runner.RunAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Application terminated unexpectedly");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TestRunner
|
||||
{
|
||||
private readonly BotSimulator _simulator;
|
||||
private readonly ILogger<TestRunner> _logger;
|
||||
private readonly List<SimulationResult> _allResults = new();
|
||||
|
||||
public TestRunner(BotSimulator simulator, ILogger<TestRunner> logger)
|
||||
{
|
||||
_simulator = simulator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
_logger.LogInformation("===========================================");
|
||||
_logger.LogInformation("🚀 TeleBot Client Simulator");
|
||||
_logger.LogInformation("===========================================");
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.WriteLine("\n📋 Select an option:");
|
||||
Console.WriteLine("1. Run single simulation");
|
||||
Console.WriteLine("2. Run multiple simulations");
|
||||
Console.WriteLine("3. Run stress test");
|
||||
Console.WriteLine("4. View statistics");
|
||||
Console.WriteLine("5. Exit");
|
||||
Console.Write("\nChoice: ");
|
||||
|
||||
var choice = Console.ReadLine();
|
||||
|
||||
switch (choice)
|
||||
{
|
||||
case "1":
|
||||
await RunSingleSimulation();
|
||||
break;
|
||||
|
||||
case "2":
|
||||
await RunMultipleSimulations();
|
||||
break;
|
||||
|
||||
case "3":
|
||||
await RunStressTest();
|
||||
break;
|
||||
|
||||
case "4":
|
||||
DisplayStatistics();
|
||||
break;
|
||||
|
||||
case "5":
|
||||
_logger.LogInformation("Exiting simulator...");
|
||||
return;
|
||||
|
||||
default:
|
||||
Console.WriteLine("Invalid choice. Please try again.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunSingleSimulation()
|
||||
{
|
||||
_logger.LogInformation("\n🎯 Running single simulation...\n");
|
||||
|
||||
var result = await _simulator.SimulateUserSession();
|
||||
_allResults.Add(result);
|
||||
|
||||
DisplaySimulationResult(result);
|
||||
}
|
||||
|
||||
private async Task RunMultipleSimulations()
|
||||
{
|
||||
Console.Write("\nHow many simulations to run? ");
|
||||
if (!int.TryParse(Console.ReadLine(), out var count) || count <= 0)
|
||||
{
|
||||
Console.WriteLine("Invalid number.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("\n🎯 Running {Count} simulations...\n", count);
|
||||
|
||||
var results = new List<SimulationResult>();
|
||||
var successful = 0;
|
||||
var failed = 0;
|
||||
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
_logger.LogInformation("▶️ Simulation {Number}/{Total}", i, count);
|
||||
|
||||
var result = await _simulator.SimulateUserSession();
|
||||
results.Add(result);
|
||||
_allResults.Add(result);
|
||||
|
||||
if (result.Success)
|
||||
successful++;
|
||||
else
|
||||
failed++;
|
||||
|
||||
// Brief pause between simulations
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
DisplayBatchSummary(results, successful, failed);
|
||||
}
|
||||
|
||||
private async Task RunStressTest()
|
||||
{
|
||||
Console.Write("\nNumber of concurrent simulations: ");
|
||||
if (!int.TryParse(Console.ReadLine(), out var concurrent) || concurrent <= 0)
|
||||
{
|
||||
Console.WriteLine("Invalid number.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Write("Total simulations to run: ");
|
||||
if (!int.TryParse(Console.ReadLine(), out var total) || total <= 0)
|
||||
{
|
||||
Console.WriteLine("Invalid number.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("\n⚡ Starting stress test: {Concurrent} concurrent, {Total} total\n",
|
||||
concurrent, total);
|
||||
|
||||
var semaphore = new SemaphoreSlim(concurrent);
|
||||
var tasks = new List<Task<SimulationResult>>();
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
await semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
return await _simulator.SimulateUserSession();
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
});
|
||||
tasks.Add(task);
|
||||
}
|
||||
|
||||
var allResults = await Task.WhenAll(tasks);
|
||||
var results = allResults.ToList();
|
||||
_allResults.AddRange(results);
|
||||
|
||||
var duration = DateTime.UtcNow - startTime;
|
||||
|
||||
DisplayStressTestResults(results, duration, concurrent, total);
|
||||
}
|
||||
|
||||
private void DisplayStatistics()
|
||||
{
|
||||
if (!_allResults.Any())
|
||||
{
|
||||
Console.WriteLine("\n📊 No simulation data available yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("\n📊 Session Statistics:");
|
||||
Console.WriteLine($" Total Simulations: {_allResults.Count}");
|
||||
Console.WriteLine($" Successful: {_allResults.Count(r => r.Success)}");
|
||||
Console.WriteLine($" Failed: {_allResults.Count(r => !r.Success)}");
|
||||
Console.WriteLine($" Success Rate: {(_allResults.Count(r => r.Success) * 100.0 / _allResults.Count):F1}%");
|
||||
|
||||
var successful = _allResults.Where(r => r.Success).ToList();
|
||||
if (successful.Any())
|
||||
{
|
||||
Console.WriteLine($"\n💰 Order Statistics:");
|
||||
Console.WriteLine($" Total Orders: {successful.Count}");
|
||||
Console.WriteLine($" Total Revenue: ${successful.Sum(r => r.OrderTotal):F2}");
|
||||
Console.WriteLine($" Average Order: ${successful.Average(r => r.OrderTotal):F2}");
|
||||
Console.WriteLine($" Min Order: ${successful.Min(r => r.OrderTotal):F2}");
|
||||
Console.WriteLine($" Max Order: ${successful.Max(r => r.OrderTotal):F2}");
|
||||
|
||||
// Payment distribution
|
||||
var payments = successful
|
||||
.Where(r => !string.IsNullOrEmpty(r.PaymentCurrency))
|
||||
.GroupBy(r => r.PaymentCurrency)
|
||||
.Select(g => new { Currency = g.Key, Count = g.Count() })
|
||||
.OrderByDescending(x => x.Count);
|
||||
|
||||
Console.WriteLine($"\n💳 Payment Methods:");
|
||||
foreach (var p in payments)
|
||||
{
|
||||
Console.WriteLine($" {p.Currency}: {p.Count} ({p.Count * 100.0 / successful.Count:F1}%)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisplaySimulationResult(SimulationResult result)
|
||||
{
|
||||
Console.WriteLine("\n========================================");
|
||||
Console.WriteLine($"📋 Simulation Result: {result.SessionId}");
|
||||
Console.WriteLine("========================================");
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
Console.WriteLine("✅ Status: SUCCESS");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"❌ Status: FAILED - {result.ErrorMessage}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"⏱️ Duration: {result.Duration.TotalSeconds:F2}s");
|
||||
|
||||
if (result.Steps.Any())
|
||||
{
|
||||
Console.WriteLine("\n📝 Steps Completed:");
|
||||
foreach (var step in result.Steps)
|
||||
{
|
||||
Console.WriteLine($" {step}");
|
||||
}
|
||||
}
|
||||
|
||||
if (result.Cart != null && result.Cart.Items.Any())
|
||||
{
|
||||
Console.WriteLine($"\n🛒 Shopping Cart ({result.Cart.Items.Count} items):");
|
||||
foreach (var item in result.Cart.Items)
|
||||
{
|
||||
Console.WriteLine($" - {item.Quantity}x {item.ProductName} @ ${item.UnitPrice:F2} = ${item.TotalPrice:F2}");
|
||||
}
|
||||
Console.WriteLine($" Total: ${result.Cart.TotalAmount:F2}");
|
||||
}
|
||||
|
||||
if (result.OrderId.HasValue)
|
||||
{
|
||||
Console.WriteLine($"\n📝 Order Details:");
|
||||
Console.WriteLine($" Order ID: {result.OrderId}");
|
||||
Console.WriteLine($" Total: ${result.OrderTotal:F2}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result.PaymentCurrency))
|
||||
{
|
||||
Console.WriteLine($"\n💰 Payment Details:");
|
||||
Console.WriteLine($" Currency: {result.PaymentCurrency}");
|
||||
Console.WriteLine($" Amount: {result.PaymentAmount}");
|
||||
}
|
||||
|
||||
Console.WriteLine("\n========================================");
|
||||
}
|
||||
|
||||
private void DisplayBatchSummary(List<SimulationResult> results, int successful, int failed)
|
||||
{
|
||||
Console.WriteLine("\n📊 Batch Summary:");
|
||||
Console.WriteLine($"✅ Successful: {successful}");
|
||||
Console.WriteLine($"❌ Failed: {failed}");
|
||||
Console.WriteLine($"📈 Success Rate: {(successful * 100.0 / results.Count):F1}%");
|
||||
|
||||
if (results.Any(r => r.Success))
|
||||
{
|
||||
var successfulResults = results.Where(r => r.Success).ToList();
|
||||
Console.WriteLine($"\n💰 Order Statistics:");
|
||||
Console.WriteLine($" Average Order: ${successfulResults.Average(r => r.OrderTotal):F2}");
|
||||
Console.WriteLine($" Total Revenue: ${successfulResults.Sum(r => r.OrderTotal):F2}");
|
||||
Console.WriteLine($" Average Duration: {successfulResults.Average(r => r.Duration.TotalSeconds):F1}s");
|
||||
}
|
||||
}
|
||||
|
||||
private void DisplayStressTestResults(List<SimulationResult> results, TimeSpan duration, int concurrent, int total)
|
||||
{
|
||||
var successful = results.Count(r => r.Success);
|
||||
var failed = results.Count(r => !r.Success);
|
||||
|
||||
Console.WriteLine("\n📊 Stress Test Results:");
|
||||
Console.WriteLine($"⏱️ Total Duration: {duration.TotalSeconds:F1}s");
|
||||
Console.WriteLine($"✅ Successful: {successful}");
|
||||
Console.WriteLine($"❌ Failed: {failed}");
|
||||
Console.WriteLine($"📈 Success Rate: {(successful * 100.0 / total):F1}%");
|
||||
Console.WriteLine($"⚡ Throughput: {(total / duration.TotalSeconds):F2} simulations/second");
|
||||
Console.WriteLine($"🔄 Concurrency: {concurrent} simultaneous connections");
|
||||
|
||||
if (failed > 0)
|
||||
{
|
||||
Console.WriteLine("\n❌ Failure Analysis:");
|
||||
var errors = results
|
||||
.Where(r => !r.Success && !string.IsNullOrEmpty(r.ErrorMessage))
|
||||
.GroupBy(r => r.ErrorMessage)
|
||||
.Select(g => new { Error = g.Key, Count = g.Count() })
|
||||
.OrderByDescending(x => x.Count)
|
||||
.Take(5);
|
||||
|
||||
foreach (var error in errors)
|
||||
{
|
||||
Console.WriteLine($" {error.Error}: {error.Count}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Telegram.Bot" Version="22.5.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
|
||||
<PackageReference Include="Bogus" Version="35.6.1" />
|
||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\LittleShop.Client\LittleShop.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Program.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Telegram.Bot" Version="22.5.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
|
||||
<PackageReference Include="Bogus" Version="35.6.1" />
|
||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\LittleShop.Client\LittleShop.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Program.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"LittleShop": {
|
||||
"ApiUrl": "https://localhost:5001",
|
||||
"Username": "admin",
|
||||
"Password": "admin"
|
||||
},
|
||||
"Simulator": {
|
||||
"MinItemsPerOrder": 1,
|
||||
"MaxItemsPerOrder": 5,
|
||||
"MinQuantityPerItem": 1,
|
||||
"MaxQuantityPerItem": 3,
|
||||
"DelayBetweenSimulations": 1000,
|
||||
"EnableDetailedLogging": true
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"System": "Warning"
|
||||
}
|
||||
}
|
||||
{
|
||||
"LittleShop": {
|
||||
"ApiUrl": "http://localhost:5000",
|
||||
"Username": "admin",
|
||||
"Password": "admin"
|
||||
},
|
||||
"Simulator": {
|
||||
"MinItemsPerOrder": 1,
|
||||
"MaxItemsPerOrder": 5,
|
||||
"MinQuantityPerItem": 1,
|
||||
"MaxQuantityPerItem": 3,
|
||||
"DelayBetweenSimulations": 1000,
|
||||
"EnableDetailedLogging": true
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"System": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
109
TeleBot/docker-compose.yml
Normal file
109
TeleBot/docker-compose.yml
Normal file
@@ -0,0 +1,109 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
telebot:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: TeleBot/TeleBot/Dockerfile
|
||||
container_name: littleshop-telebot
|
||||
restart: unless-stopped
|
||||
|
||||
environment:
|
||||
- DOTNET_ENVIRONMENT=Production
|
||||
- TZ=UTC
|
||||
|
||||
# Telegram Bot Configuration
|
||||
- Telegram__BotToken=${TELEGRAM_BOT_TOKEN}
|
||||
- Telegram__AdminChatId=${TELEGRAM_ADMIN_CHAT_ID}
|
||||
- Telegram__UseWebhook=false
|
||||
|
||||
# LittleShop API Configuration
|
||||
- LittleShop__ApiUrl=${LITTLESHOP_API_URL:-https://host.docker.internal:5001}
|
||||
- LittleShop__Username=${LITTLESHOP_USERNAME:-admin}
|
||||
- LittleShop__Password=${LITTLESHOP_PASSWORD:-admin}
|
||||
- LittleShop__UseTor=false
|
||||
|
||||
# Privacy Settings
|
||||
- Privacy__Mode=strict
|
||||
- Privacy__DataRetentionHours=24
|
||||
- Privacy__SessionTimeoutMinutes=30
|
||||
- Privacy__EnableAnalytics=false
|
||||
- Privacy__EphemeralByDefault=true
|
||||
- Privacy__EnableTor=false
|
||||
|
||||
# Database Configuration
|
||||
- Database__ConnectionString=Filename=/app/data/telebot.db;Password=;
|
||||
- Database__EncryptionKey=${DATABASE_ENCRYPTION_KEY}
|
||||
|
||||
# Features
|
||||
- Features__EnableQRCodes=true
|
||||
- Features__EnablePGPEncryption=true
|
||||
- Features__EnableDisappearingMessages=true
|
||||
|
||||
# Redis (optional)
|
||||
- Redis__Enabled=${REDIS_ENABLED:-false}
|
||||
- Redis__ConnectionString=${REDIS_CONNECTION_STRING:-redis:6379}
|
||||
|
||||
# Hangfire (optional)
|
||||
- Hangfire__Enabled=${HANGFIRE_ENABLED:-false}
|
||||
|
||||
volumes:
|
||||
- telebot-data:/app/data
|
||||
- telebot-logs:/app/logs
|
||||
|
||||
networks:
|
||||
- littleshop-network
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "pgrep", "-f", "dotnet.*TeleBot"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: littleshop-redis
|
||||
restart: unless-stopped
|
||||
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
networks:
|
||||
- littleshop-network
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
telebot-data:
|
||||
name: littleshop-telebot-data
|
||||
telebot-logs:
|
||||
name: littleshop-telebot-logs
|
||||
redis-data:
|
||||
name: littleshop-redis-data
|
||||
|
||||
networks:
|
||||
littleshop-network:
|
||||
name: littleshop-network
|
||||
driver: bridge
|
||||
Reference in New Issue
Block a user