fixes
All checks were successful
SonarQube / Build and analyze (push) Successful in 2m57s

This commit is contained in:
Leonid Pershin
2025-10-22 03:28:48 +03:00
parent 1996fec14f
commit 0e5c418a0e
8 changed files with 492 additions and 64 deletions

13
.env.example Normal file
View File

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

View File

@@ -22,7 +22,7 @@ public class ISessionStorageTests : UnitTestBase
// GetOrCreateAsync method // GetOrCreateAsync method
var getOrCreateMethod = methods.FirstOrDefault(m => m.Name == "GetOrCreateAsync"); var getOrCreateMethod = methods.FirstOrDefault(m => m.Name == "GetOrCreateAsync");
getOrCreateMethod.Should().NotBeNull(); getOrCreateMethod.Should().NotBeNull();
getOrCreateMethod!.ReturnType.Should().Be(typeof(Task<ChatSession>)); getOrCreateMethod!.ReturnType.Should().Be<Task<ChatSession>>();
getOrCreateMethod.GetParameters().Should().HaveCount(3); getOrCreateMethod.GetParameters().Should().HaveCount(3);
getOrCreateMethod.GetParameters()[0].ParameterType.Should().Be<long>(); getOrCreateMethod.GetParameters()[0].ParameterType.Should().Be<long>();
getOrCreateMethod.GetParameters()[1].ParameterType.Should().Be<string>(); getOrCreateMethod.GetParameters()[1].ParameterType.Should().Be<string>();
@@ -31,14 +31,14 @@ public class ISessionStorageTests : UnitTestBase
// GetAsync method // GetAsync method
var getMethod = methods.FirstOrDefault(m => m.Name == "GetAsync"); var getMethod = methods.FirstOrDefault(m => m.Name == "GetAsync");
getMethod.Should().NotBeNull(); getMethod.Should().NotBeNull();
getMethod!.ReturnType.Should().Be(typeof(Task<ChatSession?>)); getMethod!.ReturnType.Should().Be<Task<ChatSession?>>();
getMethod.GetParameters().Should().HaveCount(1); getMethod.GetParameters().Should().HaveCount(1);
getMethod.GetParameters()[0].ParameterType.Should().Be<long>(); getMethod.GetParameters()[0].ParameterType.Should().Be<long>();
// RemoveAsync method // RemoveAsync method
var removeMethod = methods.FirstOrDefault(m => m.Name == "RemoveAsync"); var removeMethod = methods.FirstOrDefault(m => m.Name == "RemoveAsync");
removeMethod.Should().NotBeNull(); removeMethod.Should().NotBeNull();
removeMethod!.ReturnType.Should().Be(typeof(Task<bool>)); removeMethod!.ReturnType.Should().Be<Task<bool>>();
removeMethod.GetParameters().Should().HaveCount(1); removeMethod.GetParameters().Should().HaveCount(1);
removeMethod.GetParameters()[0].ParameterType.Should().Be<long>(); removeMethod.GetParameters()[0].ParameterType.Should().Be<long>();
@@ -47,13 +47,13 @@ public class ISessionStorageTests : UnitTestBase
m.Name == "GetActiveSessionsCountAsync" m.Name == "GetActiveSessionsCountAsync"
); );
getActiveSessionsCountMethod.Should().NotBeNull(); getActiveSessionsCountMethod.Should().NotBeNull();
getActiveSessionsCountMethod!.ReturnType.Should().Be(typeof(Task<int>)); getActiveSessionsCountMethod!.ReturnType.Should().Be<Task<int>>();
getActiveSessionsCountMethod.GetParameters().Should().BeEmpty(); getActiveSessionsCountMethod.GetParameters().Should().BeEmpty();
// CleanupOldSessionsAsync method // CleanupOldSessionsAsync method
var cleanupOldSessionsMethod = methods.FirstOrDefault(m => m.Name == "CleanupOldSessionsAsync"); var cleanupOldSessionsMethod = methods.FirstOrDefault(m => m.Name == "CleanupOldSessionsAsync");
cleanupOldSessionsMethod.Should().NotBeNull(); cleanupOldSessionsMethod.Should().NotBeNull();
cleanupOldSessionsMethod!.ReturnType.Should().Be(typeof(Task<int>)); cleanupOldSessionsMethod!.ReturnType.Should().Be<Task<int>>();
cleanupOldSessionsMethod.GetParameters().Should().HaveCount(1); cleanupOldSessionsMethod.GetParameters().Should().HaveCount(1);
cleanupOldSessionsMethod.GetParameters()[0].ParameterType.Should().Be<int>(); cleanupOldSessionsMethod.GetParameters()[0].ParameterType.Should().Be<int>();

28
ChatBot/.dockerignore Normal file
View File

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

42
ChatBot/Dockerfile Normal file
View File

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

View File

@@ -1,5 +1,6 @@
using ChatBot.Services.Telegram.Interfaces; using ChatBot.Services.Telegram.Interfaces;
using ChatBot.Services.Telegram.Services; using ChatBot.Services.Telegram.Services;
using Telegram.Bot.Types;
namespace ChatBot.Services.Telegram.Commands namespace ChatBot.Services.Telegram.Commands
{ {
@@ -58,51 +59,13 @@ namespace ChatBot.Services.Telegram.Commands
try try
{ {
// Получаем информацию о боте
var botInfo = await _botInfoService.GetBotInfoAsync(cancellationToken); var botInfo = await _botInfoService.GetBotInfoAsync(cancellationToken);
// Проверяем, нужно ли отвечать на реплай if (!ShouldProcessMessage(messageText, chatId, replyInfo, botInfo))
if (replyInfo != null)
{ {
_logger.LogInformation( return string.Empty;
"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; // Не отвечаем на сообщения с упоминанием других пользователей
}
}
} }
// Создаем контекст команды
var context = TelegramCommandContext.Create( var context = TelegramCommandContext.Create(
chatId, chatId,
username, username,
@@ -112,27 +75,11 @@ namespace ChatBot.Services.Telegram.Commands
replyInfo replyInfo
); );
// Ищем команду, которая может обработать сообщение return await ExecuteCommandOrProcessMessageAsync(
var command = _commandRegistry.FindCommandForMessage(messageText); context,
if (command != null) messageText,
{
_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, chatId,
username, username,
messageText,
chatType, chatType,
chatTitle, chatTitle,
cancellationToken cancellationToken
@@ -159,5 +106,101 @@ namespace ChatBot.Services.Telegram.Commands
return "Произошла непредвиденная ошибка. Попробуйте еще раз."; 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<string> 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
);
}
} }
} }

189
DOCKER_README.md Normal file
View File

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

55
SECRETS_SETUP.md Normal file
View File

@@ -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 окружений

58
docker-compose.yml Normal file
View File

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