From 0e5c418a0e0c36185a10ee221456a253d593f0a1 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Wed, 22 Oct 2025 03:28:48 +0300 Subject: [PATCH] fixes --- .env.example | 13 ++ .../Interfaces/ISessionStorageTests.cs | 10 +- ChatBot/.dockerignore | 28 +++ ChatBot/Dockerfile | 42 ++++ .../Commands/TelegramCommandProcessor.cs | 161 +++++++++------ DOCKER_README.md | 189 ++++++++++++++++++ SECRETS_SETUP.md | 55 +++++ docker-compose.yml | 58 ++++++ 8 files changed, 492 insertions(+), 64 deletions(-) create mode 100644 .env.example create mode 100644 ChatBot/.dockerignore create mode 100644 ChatBot/Dockerfile create mode 100644 DOCKER_README.md create mode 100644 SECRETS_SETUP.md create mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bfc6530 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Database Configuration +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=chatbot +DB_USER=postgres +DB_PASSWORD=postgres + +# Telegram Bot Configuration +TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here + +# Ollama Configuration +OLLAMA_URL=https://ai.api.home/ +OLLAMA_DEFAULT_MODEL=gemma3:4b diff --git a/ChatBot.Tests/Services/Interfaces/ISessionStorageTests.cs b/ChatBot.Tests/Services/Interfaces/ISessionStorageTests.cs index f2b8586..ccce0da 100644 --- a/ChatBot.Tests/Services/Interfaces/ISessionStorageTests.cs +++ b/ChatBot.Tests/Services/Interfaces/ISessionStorageTests.cs @@ -22,7 +22,7 @@ public class ISessionStorageTests : UnitTestBase // GetOrCreateAsync method var getOrCreateMethod = methods.FirstOrDefault(m => m.Name == "GetOrCreateAsync"); getOrCreateMethod.Should().NotBeNull(); - getOrCreateMethod!.ReturnType.Should().Be(typeof(Task)); + getOrCreateMethod!.ReturnType.Should().Be>(); getOrCreateMethod.GetParameters().Should().HaveCount(3); getOrCreateMethod.GetParameters()[0].ParameterType.Should().Be(); getOrCreateMethod.GetParameters()[1].ParameterType.Should().Be(); @@ -31,14 +31,14 @@ public class ISessionStorageTests : UnitTestBase // GetAsync method var getMethod = methods.FirstOrDefault(m => m.Name == "GetAsync"); getMethod.Should().NotBeNull(); - getMethod!.ReturnType.Should().Be(typeof(Task)); + getMethod!.ReturnType.Should().Be>(); getMethod.GetParameters().Should().HaveCount(1); getMethod.GetParameters()[0].ParameterType.Should().Be(); // RemoveAsync method var removeMethod = methods.FirstOrDefault(m => m.Name == "RemoveAsync"); removeMethod.Should().NotBeNull(); - removeMethod!.ReturnType.Should().Be(typeof(Task)); + removeMethod!.ReturnType.Should().Be>(); removeMethod.GetParameters().Should().HaveCount(1); removeMethod.GetParameters()[0].ParameterType.Should().Be(); @@ -47,13 +47,13 @@ public class ISessionStorageTests : UnitTestBase m.Name == "GetActiveSessionsCountAsync" ); getActiveSessionsCountMethod.Should().NotBeNull(); - getActiveSessionsCountMethod!.ReturnType.Should().Be(typeof(Task)); + getActiveSessionsCountMethod!.ReturnType.Should().Be>(); getActiveSessionsCountMethod.GetParameters().Should().BeEmpty(); // CleanupOldSessionsAsync method var cleanupOldSessionsMethod = methods.FirstOrDefault(m => m.Name == "CleanupOldSessionsAsync"); cleanupOldSessionsMethod.Should().NotBeNull(); - cleanupOldSessionsMethod!.ReturnType.Should().Be(typeof(Task)); + cleanupOldSessionsMethod!.ReturnType.Should().Be>(); cleanupOldSessionsMethod.GetParameters().Should().HaveCount(1); cleanupOldSessionsMethod.GetParameters()[0].ParameterType.Should().Be(); diff --git a/ChatBot/.dockerignore b/ChatBot/.dockerignore new file mode 100644 index 0000000..852b0f1 --- /dev/null +++ b/ChatBot/.dockerignore @@ -0,0 +1,28 @@ +# Build artifacts +bin/ +obj/ +out/ +publish/ + +# IDE and editor files +.vs/ +.vscode/ +.idea/ +*.suo +*.user +*.userosscache +*.sln.docstates + +# Environment files (will be injected via secrets) +.env +.env.* +!.env.example + +# Logs +logs/ +*.log + +# Test results +TestResults/ +*.trx +*.coverage diff --git a/ChatBot/Dockerfile b/ChatBot/Dockerfile new file mode 100644 index 0000000..2d20874 --- /dev/null +++ b/ChatBot/Dockerfile @@ -0,0 +1,42 @@ +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Copy project file +COPY ChatBot.csproj ./ + +# Restore dependencies +RUN dotnet restore + +# Copy all source files +COPY . . + +# Build the application +RUN dotnet build -c Release -o /app/build + +# Publish stage +FROM build AS publish +RUN dotnet publish -c Release -o /app/publish /p:UseAppHost=false + +# Runtime stage +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final +WORKDIR /app + +# Install PostgreSQL client for healthcheck (optional) +RUN apt-get update && apt-get install -y postgresql-client && rm -rf /var/lib/apt/lists/* + +# Copy published application +COPY --from=publish /app/publish . + +# Create directory for logs +RUN mkdir -p /app/logs && chmod 777 /app/logs + +# Expose ports (if needed for health checks or metrics) +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD dotnet ChatBot.dll --health-check || exit 1 + +# Run the application +ENTRYPOINT ["dotnet", "ChatBot.dll"] diff --git a/ChatBot/Services/Telegram/Commands/TelegramCommandProcessor.cs b/ChatBot/Services/Telegram/Commands/TelegramCommandProcessor.cs index 6dbdd99..dff4bf3 100644 --- a/ChatBot/Services/Telegram/Commands/TelegramCommandProcessor.cs +++ b/ChatBot/Services/Telegram/Commands/TelegramCommandProcessor.cs @@ -1,5 +1,6 @@ using ChatBot.Services.Telegram.Interfaces; using ChatBot.Services.Telegram.Services; +using Telegram.Bot.Types; namespace ChatBot.Services.Telegram.Commands { @@ -58,51 +59,13 @@ namespace ChatBot.Services.Telegram.Commands try { - // Получаем информацию о боте var botInfo = await _botInfoService.GetBotInfoAsync(cancellationToken); - // Проверяем, нужно ли отвечать на реплай - if (replyInfo != null) + if (!ShouldProcessMessage(messageText, chatId, replyInfo, botInfo)) { - _logger.LogInformation( - "Reply detected: ReplyToUserId={ReplyToUserId}, BotId={BotId}, ChatId={ChatId}", - replyInfo.UserId, - botInfo?.Id, - chatId - ); - - if (botInfo != null && replyInfo.UserId != botInfo.Id) - { - _logger.LogInformation( - "Ignoring reply to user {ReplyToUserId} (not bot {BotId}) in chat {ChatId}", - replyInfo.UserId, - botInfo.Id, - chatId - ); - return string.Empty; // Не отвечаем на реплаи другим пользователям - } - } - else - { - // Если это не реплай, проверяем, обращаются ли к боту или нет упоминаний других пользователей - if (botInfo != null) - { - bool hasBotMention = messageText.Contains($"@{botInfo.Username}"); - bool hasOtherMentions = messageText.Contains('@') && !hasBotMention; - - if (!hasBotMention && hasOtherMentions) - { - _logger.LogInformation( - "Ignoring message with other user mentions in chat {ChatId}: {MessageText}", - chatId, - messageText - ); - return string.Empty; // Не отвечаем на сообщения с упоминанием других пользователей - } - } + return string.Empty; } - // Создаем контекст команды var context = TelegramCommandContext.Create( chatId, username, @@ -112,27 +75,11 @@ namespace ChatBot.Services.Telegram.Commands replyInfo ); - // Ищем команду, которая может обработать сообщение - var command = _commandRegistry.FindCommandForMessage(messageText); - if (command != null) - { - _logger.LogDebug( - "Executing command {CommandName} for chat {ChatId}", - command.CommandName, - chatId - ); - return await command.ExecuteAsync(context, cancellationToken); - } - - // Если команда не найдена, обрабатываем как обычное сообщение - _logger.LogDebug( - "No command found, processing as regular message for chat {ChatId}", - chatId - ); - return await _chatService.ProcessMessageAsync( + return await ExecuteCommandOrProcessMessageAsync( + context, + messageText, chatId, username, - messageText, chatType, chatTitle, cancellationToken @@ -159,5 +106,101 @@ namespace ChatBot.Services.Telegram.Commands return "Произошла непредвиденная ошибка. Попробуйте еще раз."; } } + + private bool ShouldProcessMessage( + string messageText, + long chatId, + ReplyInfo? replyInfo, + User? botInfo + ) + { + if (replyInfo != null) + { + return ShouldProcessReply(replyInfo, botInfo, chatId); + } + + return ShouldProcessNonReplyMessage(messageText, botInfo, chatId); + } + + private bool ShouldProcessReply(ReplyInfo replyInfo, User? botInfo, long chatId) + { + _logger.LogInformation( + "Reply detected: ReplyToUserId={ReplyToUserId}, BotId={BotId}, ChatId={ChatId}", + replyInfo.UserId, + botInfo?.Id, + chatId + ); + + if (botInfo != null && replyInfo.UserId != botInfo.Id) + { + _logger.LogInformation( + "Ignoring reply to user {ReplyToUserId} (not bot {BotId}) in chat {ChatId}", + replyInfo.UserId, + botInfo.Id, + chatId + ); + return false; + } + + return true; + } + + private bool ShouldProcessNonReplyMessage(string messageText, User? botInfo, long chatId) + { + if (botInfo == null) + { + return true; + } + + bool hasBotMention = messageText.Contains($"@{botInfo.Username}"); + bool hasOtherMentions = messageText.Contains('@') && !hasBotMention; + + if (!hasBotMention && hasOtherMentions) + { + _logger.LogInformation( + "Ignoring message with other user mentions in chat {ChatId}: {MessageText}", + chatId, + messageText + ); + return false; + } + + return true; + } + + private async Task ExecuteCommandOrProcessMessageAsync( + TelegramCommandContext context, + string messageText, + long chatId, + string username, + string chatType, + string chatTitle, + CancellationToken cancellationToken + ) + { + var command = _commandRegistry.FindCommandForMessage(messageText); + if (command != null) + { + _logger.LogDebug( + "Executing command {CommandName} for chat {ChatId}", + command.CommandName, + chatId + ); + return await command.ExecuteAsync(context, cancellationToken); + } + + _logger.LogDebug( + "No command found, processing as regular message for chat {ChatId}", + chatId + ); + return await _chatService.ProcessMessageAsync( + chatId, + username, + messageText, + chatType, + chatTitle, + cancellationToken + ); + } } } diff --git a/DOCKER_README.md b/DOCKER_README.md new file mode 100644 index 0000000..684d1a5 --- /dev/null +++ b/DOCKER_README.md @@ -0,0 +1,189 @@ +# Docker Deployment Guide + +## Структура файлов + +``` +ChatBot/ +├── ChatBot/ +│ ├── Dockerfile # Dockerfile для сборки приложения +│ └── .dockerignore # Исключения для Docker build +├── docker-compose.yml # Композиция для локального запуска +├── .env.example # Пример переменных окружения +└── .gitea/workflows/ + └── deploy.yml # CI/CD pipeline для автоматического развертывания +``` + +## Локальный запуск с Docker Compose + +### 1. Подготовка + +Скопируйте `.env.example` в `.env` и заполните необходимые значения: + +```bash +cp .env.example .env +``` + +Отредактируйте `.env`: +```env +TELEGRAM_BOT_TOKEN=your_actual_bot_token +OLLAMA_URL=https://your-ollama-instance/ +OLLAMA_DEFAULT_MODEL=gemma3:4b +``` + +### 2. Запуск + +```bash +# Запуск всех сервисов (PostgreSQL + ChatBot) +docker-compose up -d + +# Просмотр логов +docker-compose logs -f chatbot + +# Остановка +docker-compose down + +# Остановка с удалением volumes +docker-compose down -v +``` + +### 3. Проверка статуса + +```bash +# Статус контейнеров +docker-compose ps + +# Логи приложения +docker-compose logs chatbot + +# Логи базы данных +docker-compose logs postgres +``` + +## Ручная сборка и запуск + +### Сборка образа + +```bash +cd ChatBot +docker build -t chatbot:latest . +``` + +### Запуск контейнера + +```bash +docker run -d \ + --name chatbot-app \ + -e DB_HOST=your_db_host \ + -e DB_PORT=5432 \ + -e DB_NAME=chatbot \ + -e DB_USER=postgres \ + -e DB_PASSWORD=your_password \ + -e TELEGRAM_BOT_TOKEN=your_token \ + -e OLLAMA_URL=https://your-ollama/ \ + -e OLLAMA_DEFAULT_MODEL=gemma3:4b \ + chatbot:latest +``` + +## CI/CD Pipeline + +### Настройка секретов в Gitea + +Для работы CI/CD pipeline необходимо настроить следующие секреты в Gitea (Settings → Secrets): + +| Секрет | Описание | Пример | +|--------|----------|--------| +| `CHATBOT_DB_HOST` | Хост базы данных | `postgres` или `your-db-host` | +| `CHATBOT_DB_PORT` | Порт базы данных | `5432` | +| `CHATBOT_DB_NAME` | Имя базы данных | `chatbot` | +| `CHATBOT_DB_USER` | Пользователь БД | `postgres` | +| `CHATBOT_DB_PASSWORD` | Пароль БД | `your_secure_password` | +| `CHATBOT_TELEGRAM_BOT_TOKEN` | Токен Telegram бота | `123456:ABC-DEF...` | +| `CHATBOT_OLLAMA_URL` | URL Ollama API | `https://ai.api.home/` | +| `CHATBOT_OLLAMA_DEFAULT_MODEL` | Модель по умолчанию | `gemma3:4b` | + +### Workflow триггеры + +Pipeline запускается автоматически при: +- Push в ветки `master` или `develop` +- Создании Pull Request в ветку `master` + +### Этапы pipeline + +1. **Build Docker Image** - сборка Docker образа +2. **Stop existing container** - остановка существующего тестового контейнера +3. **Run test container** - запуск нового контейнера с секретами +4. **Health check** - проверка работоспособности приложения +5. **Cleanup** - очистка старых образов + +### Мониторинг deployment + +```bash +# Просмотр логов контейнера +docker logs chatbot-test -f + +# Проверка статуса +docker ps | grep chatbot-test + +# Health check +docker exec chatbot-test dotnet ChatBot.dll --health-check +``` + +## Troubleshooting + +### Контейнер не запускается + +```bash +# Проверьте логи +docker logs chatbot-app + +# Проверьте переменные окружения +docker inspect chatbot-app | grep -A 20 Env +``` + +### Проблемы с подключением к БД + +```bash +# Проверьте доступность PostgreSQL +docker exec chatbot-postgres pg_isready + +# Проверьте сетевое подключение +docker network inspect chatbot-network +``` + +### Очистка + +```bash +# Удалить все остановленные контейнеры +docker container prune + +# Удалить неиспользуемые образы +docker image prune -a + +# Удалить неиспользуемые volumes +docker volume prune +``` + +## Production Deployment + +Для production рекомендуется: + +1. Использовать Docker registry (например, GitHub Container Registry) +2. Настроить мониторинг (Prometheus + Grafana) +3. Использовать orchestration (Docker Swarm или Kubernetes) +4. Настроить backup базы данных +5. Использовать secrets management (Docker Secrets, Vault) +6. Настроить reverse proxy (Nginx, Traefik) + +### Пример с Docker Swarm + +```bash +# Инициализация swarm +docker swarm init + +# Создание секретов +echo "your_token" | docker secret create telegram_token - +echo "your_password" | docker secret create db_password - + +# Deploy stack +docker stack deploy -c docker-compose.yml chatbot +``` diff --git a/SECRETS_SETUP.md b/SECRETS_SETUP.md new file mode 100644 index 0000000..8ea391f --- /dev/null +++ b/SECRETS_SETUP.md @@ -0,0 +1,55 @@ +# Настройка секретов для CI/CD + +## Gitea Secrets + +Перейдите в настройки репозитория: **Settings → Secrets → Actions** + +### Обязательные секреты с префиксом CHATBOT_ + +| Имя секрета | Значение | Описание | +|-------------|----------|----------| +| `CHATBOT_DB_HOST` | `postgres` | Хост PostgreSQL | +| `CHATBOT_DB_PORT` | `5432` | Порт PostgreSQL | +| `CHATBOT_DB_NAME` | `chatbot` | Имя базы данных | +| `CHATBOT_DB_USER` | `postgres` | Пользователь БД | +| `CHATBOT_DB_PASSWORD` | `your_secure_password` | Пароль БД | +| `CHATBOT_TELEGRAM_BOT_TOKEN` | `123456:ABC-DEF...` | Токен Telegram бота | +| `CHATBOT_OLLAMA_URL` | `https://ai.api.home/` | URL Ollama API | +| `CHATBOT_OLLAMA_DEFAULT_MODEL` | `gemma3:4b` | Модель по умолчанию | + +## Как добавить секрет в Gitea + +1. Откройте репозиторий в Gitea +2. Перейдите в **Settings** (⚙️) +3. Выберите **Secrets** → **Actions** +4. Нажмите **Add Secret** +5. Введите: + - **Name**: имя секрета (например, `CHATBOT_DB_HOST`) + - **Value**: значение секрета +6. Нажмите **Add Secret** + +## Проверка секретов + +После добавления всех секретов, workflow `.gitea/workflows/deploy.yml` будет использовать их автоматически при каждом push в ветки `master` или `develop`. + +## Локальная разработка + +Для локальной разработки используйте файл `.env`: + +```bash +# Скопируйте пример +cp .env.example .env + +# Отредактируйте значения +nano .env # или любой другой редактор +``` + +**Важно**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий! + +## Безопасность + +- ✅ Никогда не коммитьте файл `.env` с реальными данными +- ✅ Используйте сложные пароли для production +- ✅ Регулярно ротируйте токены и пароли +- ✅ Ограничьте доступ к секретам в Gitea +- ✅ Используйте разные токены для dev/test/prod окружений diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9a720d8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +version: '3.8' + +services: + postgres: + image: postgres:16-alpine + container_name: chatbot-postgres + environment: + POSTGRES_DB: ${DB_NAME:-chatbot} + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + ports: + - "${DB_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - chatbot-network + + chatbot: + build: + context: ./ChatBot + dockerfile: Dockerfile + container_name: chatbot-app + depends_on: + postgres: + condition: service_healthy + environment: + - DB_HOST=postgres + - DB_PORT=5432 + - DB_NAME=${DB_NAME:-chatbot} + - DB_USER=${DB_USER:-postgres} + - DB_PASSWORD=${DB_PASSWORD:-postgres} + - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} + - OLLAMA_URL=${OLLAMA_URL} + - OLLAMA_DEFAULT_MODEL=${OLLAMA_DEFAULT_MODEL:-gemma3:4b} + volumes: + - ./ChatBot/logs:/app/logs + restart: unless-stopped + networks: + - chatbot-network + healthcheck: + test: ["CMD", "dotnet", "ChatBot.dll", "--health-check"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + postgres_data: + driver: local + +networks: + chatbot-network: + driver: bridge