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:
2025-09-17 15:07:38 +01:00
parent bcca00ab39
commit e1b377a042
140 changed files with 32166 additions and 21089 deletions

22
TeleBot/.env.example Normal file
View 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
View 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

View File

@@ -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; }
}
}

View 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.

View 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"]

View File

@@ -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()
);
}
}

View File

@@ -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()
);
}
}
}
}

View File

@@ -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();
}

View 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;
}
}
}

View File

@@ -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>

View File

@@ -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\nWed 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\nWed 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;
}
}
}

View 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}");
}
}
}

View File

@@ -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}";
}

View File

@@ -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()

View File

@@ -15,7 +15,7 @@
"UseWebhook": false
},
"LittleShop": {
"ApiUrl": "http://localhost:5000",
"ApiUrl": "https://localhost:5001",
"OnionUrl": "",
"Username": "admin",
"Password": "admin",

View File

@@ -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; }
}
}

View File

@@ -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; }
}

View File

@@ -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}");
}
}
}
}
}

View File

@@ -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>

View File

@@ -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
View 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