Compare commits
10 Commits
3474fe5f96
...
e553b9584c
| Author | SHA1 | Date | |
|---|---|---|---|
| e553b9584c | |||
| df0eb4536a | |||
| 3e1a090fc8 | |||
| c0a8cf1334 | |||
| 98ba1cb3c1 | |||
| a25c39e9cd | |||
| f43580e1b0 | |||
| f2fe946244 | |||
| fe0aaefbec | |||
| 5f3d478adb |
91
.dockerignore
Normal file
91
.dockerignore
Normal file
@ -0,0 +1,91 @@
|
||||
# Версионный контроль
|
||||
.git/
|
||||
.gitignore
|
||||
.gitattributes
|
||||
.gitmodules
|
||||
|
||||
# Окружение Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.env
|
||||
.venv
|
||||
|
||||
# IDE и редакторы
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*.sublime-*
|
||||
*.code-workspace
|
||||
|
||||
# Операционная система
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Логи и временные файлы
|
||||
logs/
|
||||
*.log
|
||||
*.tmp
|
||||
*.temp
|
||||
temp/
|
||||
tmp/
|
||||
|
||||
# Тестирование
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
.tox/
|
||||
.cache
|
||||
coverage.xml
|
||||
htmlcov/
|
||||
.nyc_output
|
||||
|
||||
# Документация
|
||||
docs/
|
||||
*.md
|
||||
*.rst
|
||||
*.txt
|
||||
!requirements.txt
|
||||
|
||||
# Docker файлы
|
||||
Dockerfile.*
|
||||
docker-compose*.yml
|
||||
!docker-compose.yml
|
||||
|
||||
# Конфигурация
|
||||
env.example
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Базы данных (если есть локальные)
|
||||
*.db
|
||||
*.sqlite3
|
||||
data/
|
||||
|
||||
# Бекапы и архивы
|
||||
*.bak
|
||||
*.backup
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# Локальные изображения для разработки
|
||||
images/*
|
||||
!images/.gitkeep
|
||||
|
||||
# Специфичные для проекта
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
68
Dockerfile
Normal file
68
Dockerfile
Normal file
@ -0,0 +1,68 @@
|
||||
# Используем официальный Python образ
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Устанавливаем системные зависимости
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
libaio1 \
|
||||
libaio-dev \
|
||||
unzip \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Устанавливаем Oracle Instant Client
|
||||
RUN mkdir -p /opt/oracle && \
|
||||
cd /opt/oracle && \
|
||||
wget https://download.oracle.com/otn_software/linux/instantclient/1921000/instantclient-basic-linux.x64-19.21.0.0.0dbru.zip && \
|
||||
unzip instantclient-basic-linux.x64-19.21.0.0.0dbru.zip && \
|
||||
rm instantclient-basic-linux.x64-19.21.0.0.0dbru.zip && \
|
||||
echo /opt/oracle/instantclient_19_21 > /etc/ld.so.conf.d/oracle-instantclient.conf && \
|
||||
ldconfig
|
||||
|
||||
# Устанавливаем переменные окружения для Oracle
|
||||
ENV ORACLE_HOME=/opt/oracle/instantclient_19_21
|
||||
ENV LD_LIBRARY_PATH=/opt/oracle/instantclient_19_21${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
|
||||
ENV PATH=/opt/oracle/instantclient_19_21:$PATH
|
||||
|
||||
# Создаем пользователя для безопасности с UID 1027 и добавляем в группу users
|
||||
RUN useradd --create-home --shell /bin/bash --uid 1027 --gid 100 salvagebot
|
||||
|
||||
# Создаем рабочую директорию и устанавливаем права
|
||||
RUN mkdir -p /home/salvagebot/app && \
|
||||
chown -R salvagebot:users /home/salvagebot/app
|
||||
|
||||
USER salvagebot
|
||||
WORKDIR /home/salvagebot/app
|
||||
|
||||
# Копируем файл зависимостей
|
||||
COPY --chown=salvagebot:users requirements.txt .
|
||||
|
||||
# Устанавливаем Python зависимости
|
||||
RUN pip install --user --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем entrypoint скрипт
|
||||
COPY --chown=salvagebot:users docker-entrypoint.sh /usr/local/bin/
|
||||
USER root
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
USER salvagebot
|
||||
|
||||
# Копируем код приложения
|
||||
COPY --chown=salvagebot:users . .
|
||||
|
||||
# Создаем необходимые директории с правильными правами
|
||||
RUN mkdir -p logs images data && \
|
||||
chmod 755 logs images data
|
||||
|
||||
# Устанавливаем переменные окружения Python
|
||||
ENV PYTHONPATH=/home/salvagebot/app
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Проверяем здоровье контейнера
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD python -c "import sys; sys.exit(0)"
|
||||
|
||||
# Используем entrypoint для инициализации
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
|
||||
# Команда запуска
|
||||
CMD ["python", "main.py"]
|
||||
159
Makefile
Normal file
159
Makefile
Normal file
@ -0,0 +1,159 @@
|
||||
# Makefile для SalvageDB Telegram Bot
|
||||
.PHONY: help build up down restart logs shell clean test health
|
||||
|
||||
# Переменные
|
||||
COMPOSE_FILE = docker-compose.yml
|
||||
SERVICE_NAME = salvagedb-bot
|
||||
CONTAINER_NAME = salvagedb-telegram-bot
|
||||
|
||||
# Помощь
|
||||
help: ## Показать доступные команды
|
||||
@echo "SalvageDB Telegram Bot - Docker Commands"
|
||||
@echo "========================================"
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
# Основные команды
|
||||
build: ## Собрать Docker образ
|
||||
@echo "🔨 Building Docker image..."
|
||||
docker-compose build --no-cache
|
||||
|
||||
up: ## Запустить контейнер
|
||||
@echo "🚀 Starting container..."
|
||||
docker-compose up -d
|
||||
|
||||
down: ## Остановить контейнер
|
||||
@echo "🛑 Stopping container..."
|
||||
docker-compose down
|
||||
|
||||
restart: ## Перезапустить контейнер
|
||||
@echo "🔄 Restarting container..."
|
||||
docker-compose restart
|
||||
|
||||
# Логи и мониторинг
|
||||
logs: ## Показать логи в реальном времени
|
||||
@echo "📋 Following logs..."
|
||||
docker-compose logs -f $(SERVICE_NAME)
|
||||
|
||||
logs-tail: ## Показать последние 100 строк логов
|
||||
@echo "📋 Last 100 log lines..."
|
||||
docker-compose logs --tail=100 $(SERVICE_NAME)
|
||||
|
||||
health: ## Проверить здоровье контейнера
|
||||
@echo "🏥 Checking container health..."
|
||||
docker inspect --format='{{.State.Health.Status}}' $(CONTAINER_NAME) || echo "Health check not available"
|
||||
|
||||
status: ## Показать статус контейнера
|
||||
@echo "📊 Container status:"
|
||||
docker-compose ps
|
||||
|
||||
stats: ## Показать статистику ресурсов
|
||||
@echo "📈 Resource usage:"
|
||||
docker stats $(CONTAINER_NAME) --no-stream
|
||||
|
||||
# Разработка
|
||||
shell: ## Подключиться к контейнеру
|
||||
@echo "🐚 Opening shell in container..."
|
||||
docker-compose exec $(SERVICE_NAME) bash
|
||||
|
||||
shell-root: ## Подключиться как root
|
||||
@echo "🔑 Opening root shell in container..."
|
||||
docker-compose exec --user root $(SERVICE_NAME) bash
|
||||
|
||||
# Тестирование
|
||||
test-env: ## Проверить переменные окружения
|
||||
@echo "🔍 Checking environment variables..."
|
||||
docker-compose exec $(SERVICE_NAME) python -c "import os; [print(f'{k}={v[:20]}{"..." if len(v) > 20 else ""}') for k, v in sorted(os.environ.items()) if any(x in k.upper() for x in ['BOT', 'DB', 'PRICE', 'ADMIN', 'DEBUG'])]"
|
||||
|
||||
test-db: ## Тестировать подключение к БД
|
||||
@echo "🗄️ Testing database connection..."
|
||||
docker-compose exec $(SERVICE_NAME) python -c "import oracledb, os; conn = oracledb.connect(user=os.getenv('db_user'), password=os.getenv('db_password'), dsn=os.getenv('db_dsn')); print('✅ Database connection successful!'); conn.close()"
|
||||
|
||||
test-bot: ## Проверить импорты бота
|
||||
@echo "🤖 Testing bot imports..."
|
||||
docker-compose exec $(SERVICE_NAME) python -c "import aiogram, oracledb, asyncio, logging; print('✅ All imports successful!')"
|
||||
|
||||
test-all: test-env test-db test-bot ## Запустить все тесты
|
||||
|
||||
# Обслуживание
|
||||
clean: ## Удалить контейнеры и volumes
|
||||
@echo "🧹 Cleaning up..."
|
||||
docker-compose down -v
|
||||
docker system prune -f
|
||||
|
||||
clean-images: ## Удалить неиспользуемые образы
|
||||
@echo "🗑️ Removing unused images..."
|
||||
docker image prune -f
|
||||
|
||||
clean-all: clean clean-images ## Полная очистка
|
||||
|
||||
# Бэкап и восстановление
|
||||
backup-logs: ## Создать архив логов
|
||||
@echo "💾 Creating logs backup..."
|
||||
tar -czf logs_backup_$(shell date +%Y%m%d_%H%M%S).tar.gz logs/
|
||||
|
||||
backup-config: ## Создать бэкап конфигурации
|
||||
@echo "🔐 Creating configuration backup..."
|
||||
cp docker-compose.yml docker-compose.yml.backup.$(shell date +%Y%m%d_%H%M%S)
|
||||
|
||||
# Деплой
|
||||
deploy: build up ## Полный деплой (сборка + запуск)
|
||||
@echo "🚀 Deployment complete!"
|
||||
@make health
|
||||
|
||||
redeploy: down build up ## Переделой (остановка + сборка + запуск)
|
||||
@echo "🔄 Redeployment complete!"
|
||||
@make health
|
||||
|
||||
# Мониторинг
|
||||
monitor: ## Мониторинг в реальном времени
|
||||
@echo "📊 Starting monitoring (Ctrl+C to stop)..."
|
||||
@while true; do \
|
||||
clear; \
|
||||
echo "=== SalvageDB Bot Monitoring ==="; \
|
||||
echo "Time: $$(date)"; \
|
||||
echo ""; \
|
||||
echo "Container Status:"; \
|
||||
docker-compose ps; \
|
||||
echo ""; \
|
||||
echo "Resource Usage:"; \
|
||||
docker stats $(CONTAINER_NAME) --no-stream 2>/dev/null || echo "Container not running"; \
|
||||
echo ""; \
|
||||
echo "Last 5 log lines:"; \
|
||||
docker-compose logs --tail=5 $(SERVICE_NAME) 2>/dev/null || echo "No logs available"; \
|
||||
sleep 5; \
|
||||
done
|
||||
|
||||
# Обновление
|
||||
update: ## Обновить код и перезапустить
|
||||
@echo "📥 Updating application..."
|
||||
git pull
|
||||
@make redeploy
|
||||
|
||||
# Конфигурация
|
||||
setup: ## Первоначальная настройка
|
||||
@echo "⚙️ Initial setup..."
|
||||
@echo "📁 Creating directories..."
|
||||
mkdir -p logs images data
|
||||
chmod 755 logs images data
|
||||
@echo "✅ Setup complete! Edit docker-compose.yml with your configuration and run 'make deploy'"
|
||||
|
||||
config-check: ## Проверить конфигурацию docker-compose.yml
|
||||
@echo "🔍 Checking docker-compose.yml configuration..."
|
||||
@if grep -q "your_bot_token_here" docker-compose.yml; then \
|
||||
echo "❌ BOT_TOKEN not configured"; \
|
||||
else \
|
||||
echo "✅ BOT_TOKEN configured"; \
|
||||
fi
|
||||
@if grep -q "your_db_user" docker-compose.yml; then \
|
||||
echo "❌ Database credentials not configured"; \
|
||||
else \
|
||||
echo "✅ Database credentials configured"; \
|
||||
fi
|
||||
@if grep -q "123456789" docker-compose.yml; then \
|
||||
echo "⚠️ ADMIN_USER_ID using default value"; \
|
||||
else \
|
||||
echo "✅ ADMIN_USER_ID configured"; \
|
||||
fi
|
||||
|
||||
# По умолчанию
|
||||
.DEFAULT_GOAL := help
|
||||
248
README_DOCKER.md
Normal file
248
README_DOCKER.md
Normal file
@ -0,0 +1,248 @@
|
||||
# SalvageDB Telegram Bot - Docker Deployment
|
||||
|
||||
Полная инструкция по развертыванию SalvageDB Telegram Bot в Docker контейнере.
|
||||
|
||||
## 📋 Требования
|
||||
|
||||
- Docker Engine 20.10+
|
||||
- Docker Compose 2.0+
|
||||
- Минимум 512 MB RAM
|
||||
- Доступ к Oracle Database
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### 1. Настройка конфигурации
|
||||
|
||||
Отредактируйте файл `docker-compose.yml` и замените значения по умолчанию на ваши:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
# Telegram Bot настройки
|
||||
- BOT_TOKEN=your_actual_bot_token_here # Замените на токен от @BotFather
|
||||
- BOT_NAME=SalvageDB Bot
|
||||
- ADMIN_USER_ID=your_telegram_user_id # Замените на ваш Telegram ID
|
||||
|
||||
# База данных Oracle
|
||||
- db_user=your_actual_db_user # Замените на пользователя БД
|
||||
- db_password=your_actual_db_password # Замените на пароль БД
|
||||
- db_dsn=your_db_host:1521/service_name # Замените на DSN вашей БД
|
||||
```
|
||||
|
||||
**Обязательно замените:**
|
||||
- `BOT_TOKEN` - токен от @BotFather
|
||||
- `ADMIN_USER_ID` - ваш Telegram ID для админ-функций
|
||||
- `db_user`, `db_password`, `db_dsn` - настройки Oracle DB
|
||||
|
||||
### 2. Создание необходимых директорий
|
||||
|
||||
```bash
|
||||
# Создаем директории для данных
|
||||
mkdir -p logs images data
|
||||
|
||||
# Устанавливаем права доступа (Linux/macOS)
|
||||
chmod 755 logs images data
|
||||
```
|
||||
|
||||
### 3. Запуск контейнера
|
||||
|
||||
```bash
|
||||
# Сборка и запуск
|
||||
docker-compose up --build -d
|
||||
|
||||
# Проверка статуса
|
||||
docker-compose ps
|
||||
|
||||
# Просмотр логов
|
||||
docker-compose logs -f salvagedb-bot
|
||||
```
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
```
|
||||
salvagedb_bot/
|
||||
├── Dockerfile # Конфигурация Docker образа
|
||||
├── docker-compose.yml # Orchestration конфигурация (содержит все настройки)
|
||||
├── docker-entrypoint.sh # Скрипт инициализации
|
||||
├── .dockerignore # Исключения для Docker
|
||||
├── env.example # Пример переменных (справочно)
|
||||
├── requirements.txt # Python зависимости
|
||||
├── main.py # Основной код бота
|
||||
├── db.py # Модуль работы с БД
|
||||
├── middlewares/ # Middleware компоненты
|
||||
├── logs/ # Логи приложения (volume)
|
||||
├── images/ # Изображения автомобилей (volume)
|
||||
└── data/ # Дополнительные данные (volume)
|
||||
```
|
||||
|
||||
## 🔧 Конфигурация
|
||||
|
||||
### Переменные окружения в docker-compose.yml
|
||||
|
||||
Все настройки находятся в файле `docker-compose.yml` в секции `environment`:
|
||||
|
||||
| Переменная | Описание | Значение по умолчанию |
|
||||
|-----------|----------|----------------------|
|
||||
| `BOT_TOKEN` | Токен Telegram бота | `your_bot_token_here` ⚠️ |
|
||||
| `BOT_NAME` | Имя бота | `SalvageDB Bot` |
|
||||
| `ADMIN_USER_ID` | ID администратора | `123456789` ⚠️ |
|
||||
| `DECODE_PRICE` | Цена декодирования VIN | `1` |
|
||||
| `CHECK_PRICE` | Цена проверки повреждений | `10` |
|
||||
| `IMG_PRICE` | Цена доступа к фото | `100` |
|
||||
| `db_user` | Пользователь Oracle DB | `your_db_user` ⚠️ |
|
||||
| `db_password` | Пароль Oracle DB | `your_db_password` ⚠️ |
|
||||
| `db_dsn` | DSN строка Oracle DB | `localhost:1521/XEPDB1` ⚠️ |
|
||||
| `DEBUG` | Режим отладки | `0` |
|
||||
| `TZ` | Часовой пояс | `UTC` |
|
||||
|
||||
**⚠️ Обязательно замените** отмеченные значения на реальные!
|
||||
|
||||
### Volumes
|
||||
|
||||
- `./logs:/home/salvagebot/app/logs` - Логи приложения
|
||||
- `./images:/home/salvagebot/app/images:ro` - Изображения (read-only)
|
||||
- `./data:/home/salvagebot/app/data` - Дополнительные данные
|
||||
|
||||
## 🛠️ Управление контейнером
|
||||
|
||||
### Основные команды
|
||||
|
||||
```bash
|
||||
# Запуск
|
||||
docker-compose up -d
|
||||
|
||||
# Остановка
|
||||
docker-compose down
|
||||
|
||||
# Перезапуск
|
||||
docker-compose restart
|
||||
|
||||
# Пересборка и запуск
|
||||
docker-compose up --build -d
|
||||
|
||||
# Просмотр логов в реальном времени
|
||||
docker-compose logs -f salvagedb-bot
|
||||
|
||||
# Подключение к контейнеру
|
||||
docker-compose exec salvagedb-bot bash
|
||||
|
||||
# Просмотр статуса
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### Мониторинг
|
||||
|
||||
```bash
|
||||
# Проверка здоровья контейнера
|
||||
docker-compose exec salvagedb-bot python -c "print('Bot is healthy!')"
|
||||
|
||||
# Статистика ресурсов
|
||||
docker stats salvagedb-telegram-bot
|
||||
|
||||
# Информация о контейнере
|
||||
docker inspect salvagedb-telegram-bot
|
||||
```
|
||||
|
||||
## 📊 Логирование
|
||||
|
||||
### Структура логов
|
||||
|
||||
Логи сохраняются в директории `./logs/`:
|
||||
- `salvagedb_bot.log` - текущий лог файл
|
||||
- `salvagedb_bot.log.YYYY-MM-DD` - архивные логи
|
||||
|
||||
### Конфигурация логирования
|
||||
|
||||
- **Ротация:** ежедневно в полночь
|
||||
- **Хранение:** 30 дней
|
||||
- **Формат:** `YYYY-MM-DD HH:MM:SS - module - LEVEL - message`
|
||||
- **Уровни:** INFO, WARNING, ERROR
|
||||
|
||||
## 🔒 Безопасность
|
||||
|
||||
### Рекомендации
|
||||
|
||||
1. **Переменные окружения:**
|
||||
- Замените все placeholder значения в docker-compose.yml
|
||||
- Используйте сильные пароли для БД
|
||||
- Ограничьте доступ к токену бота
|
||||
|
||||
2. **Сетевая безопасность:**
|
||||
- Контейнер работает в изолированной сети
|
||||
- Нет открытых портов (только исходящие соединения)
|
||||
|
||||
3. **Файловая система:**
|
||||
- Приложение работает под непривилегированным пользователем
|
||||
- Read-only монтирование для изображений
|
||||
|
||||
## 🐛 Устранение неполадок
|
||||
|
||||
### Проблемы с Oracle
|
||||
|
||||
```bash
|
||||
# Проверка Oracle client в контейнере
|
||||
docker-compose exec salvagedb-bot python -c "import oracledb; print(oracledb.version)"
|
||||
|
||||
# Тест подключения к БД
|
||||
docker-compose exec salvagedb-bot python -c "
|
||||
import oracledb
|
||||
import os
|
||||
conn = oracledb.connect(user=os.getenv('db_user'), password=os.getenv('db_password'), dsn=os.getenv('db_dsn'))
|
||||
print('Connection successful!')
|
||||
conn.close()
|
||||
"
|
||||
```
|
||||
|
||||
### Проблемы с ботом
|
||||
|
||||
```bash
|
||||
# Проверка токена бота
|
||||
docker-compose exec salvagedb-bot python -c "
|
||||
import os
|
||||
print('Bot token length:', len(os.getenv('BOT_TOKEN', '')))
|
||||
"
|
||||
|
||||
# Проверка aiogram
|
||||
docker-compose exec salvagedb-bot python -c "import aiogram; print('aiogram version:', aiogram.__version__)"
|
||||
```
|
||||
|
||||
### Логи отладки
|
||||
|
||||
```bash
|
||||
# Включить DEBUG режим (отредактировать docker-compose.yml)
|
||||
# Изменить: DEBUG=1
|
||||
docker-compose restart
|
||||
|
||||
# Просмотр подробных логов
|
||||
docker-compose logs -f --tail=100 salvagedb-bot
|
||||
```
|
||||
|
||||
## 🔄 Обновление
|
||||
|
||||
```bash
|
||||
# Остановить контейнер
|
||||
docker-compose down
|
||||
|
||||
# Обновить код
|
||||
git pull
|
||||
|
||||
# Пересобрать и запустить
|
||||
docker-compose up --build -d
|
||||
|
||||
# Проверить статус
|
||||
docker-compose logs -f salvagedb-bot
|
||||
```
|
||||
|
||||
## 📞 Поддержка
|
||||
|
||||
При возникновении проблем:
|
||||
|
||||
1. Проверьте логи: `docker-compose logs salvagedb-bot`
|
||||
2. Убедитесь в правильности переменных в docker-compose.yml
|
||||
3. Проверьте подключение к Oracle DB
|
||||
4. Проверьте валидность токена бота
|
||||
|
||||
## 🏷️ Теги версий
|
||||
|
||||
- `latest` - последняя стабильная версия
|
||||
- `dev` - версия для разработки
|
||||
- `v1.x.x` - конкретные релизы
|
||||
10
build.cmd
Normal file
10
build.cmd
Normal file
@ -0,0 +1,10 @@
|
||||
@echo off
|
||||
for /f "tokens=*" %%i in ('powershell -Command "Get-Date -Format 'yyyyMMdd'"') do set TAG=%%i
|
||||
echo Building with tag: %TAG%
|
||||
del salvagebot
|
||||
docker rmi vladsimachkov/salvagebot:%TAG% 2>nul
|
||||
docker buildx build --no-cache --progress=plain -t vladsimachkov/salvagebot:%TAG% .
|
||||
docker save vladsimachkov/salvagebot:%TAG% >salvagebot
|
||||
|
||||
#docker tag vladsimachkov/salvagebot:%TAG% reg.ddl.su/salvagedb/salvagenas_filecheker:%TAG%
|
||||
#docker push reg.ddl.su/salvagedb/salvagenas_filecheker:%TAG%
|
||||
94
db.py
94
db.py
@ -127,6 +127,19 @@ class OracleDatabase:
|
||||
import asyncio
|
||||
return await asyncio.to_thread(_query)
|
||||
|
||||
async def fetch_photo_paths(self, vin: str) -> list:
|
||||
"""
|
||||
Получает список путей к фотографиям для данного VIN
|
||||
"""
|
||||
def _query():
|
||||
with self._pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT ipath FROM salvagedb.salvage_images WHERE fn = 1 AND vin = :vin", {"vin": vin})
|
||||
results = cur.fetchall()
|
||||
return [row[0] for row in results if row[0]] if results else []
|
||||
import asyncio
|
||||
return await asyncio.to_thread(_query)
|
||||
|
||||
async def fetch_detailed_vin_info(self, vin: str) -> dict:
|
||||
# Manual async wrapper since oracledb is synchronous (threaded)
|
||||
def _query():
|
||||
@ -378,3 +391,84 @@ class OracleDatabase:
|
||||
except Exception as e:
|
||||
print(f"Error getting users summary: {e}")
|
||||
return {}
|
||||
|
||||
async def count_photo_records(self, vin: str) -> int:
|
||||
"""
|
||||
Подсчитывает количество фотографий для данного VIN
|
||||
"""
|
||||
def _query():
|
||||
with self._pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT COUNT(*) FROM salvagedb.salvage_images WHERE vin = :vin AND fn = 1", {"vin": vin})
|
||||
result = cur.fetchone()
|
||||
return result[0] if result else 0
|
||||
import asyncio
|
||||
return await asyncio.to_thread(_query)
|
||||
|
||||
async def save_payment_log(self, user: User, service_type: str, vin: str, payment_data: dict, service_result: dict = None) -> bool:
|
||||
"""
|
||||
Логирует операцию оплаты с полной информацией о пользователе, услуге и результате
|
||||
|
||||
Args:
|
||||
user: Объект пользователя Telegram
|
||||
service_type: Тип услуги ('decode_vin', 'check_salvage', 'get_photos')
|
||||
vin: VIN номер автомобиля
|
||||
payment_data: Данные о платеже (сумма, transaction_id, статус)
|
||||
service_result: Результат предоставления услуги (количество данных, статус)
|
||||
"""
|
||||
def _save_log():
|
||||
with self._pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
insert_query = """
|
||||
INSERT INTO payment_logs (
|
||||
log_id, user_id, user_first_name, user_last_name, user_username,
|
||||
user_language_code, user_is_premium, service_type, vin_number,
|
||||
payment_amount, transaction_id, payment_status, payment_currency,
|
||||
service_status, data_found_count, refund_status, refund_reason,
|
||||
vehicle_make, vehicle_model, vehicle_year, error_message,
|
||||
created_date, ip_address
|
||||
) VALUES (
|
||||
payment_logs_seq.NEXTVAL, :user_id, :user_first_name, :user_last_name, :user_username,
|
||||
:user_language_code, :user_is_premium, :service_type, :vin_number,
|
||||
:payment_amount, :transaction_id, :payment_status, :payment_currency,
|
||||
:service_status, :data_found_count, :refund_status, :refund_reason,
|
||||
:vehicle_make, :vehicle_model, :vehicle_year, :error_message,
|
||||
SYSDATE, :ip_address
|
||||
)
|
||||
"""
|
||||
|
||||
params = {
|
||||
"user_id": user.id,
|
||||
"user_first_name": user.first_name,
|
||||
"user_last_name": user.last_name,
|
||||
"user_username": user.username,
|
||||
"user_language_code": user.language_code,
|
||||
"user_is_premium": 1 if user.is_premium else 0,
|
||||
"service_type": service_type,
|
||||
"vin_number": vin,
|
||||
"payment_amount": payment_data.get('amount', 0),
|
||||
"transaction_id": payment_data.get('transaction_id'),
|
||||
"payment_status": payment_data.get('status', 'completed'),
|
||||
"payment_currency": payment_data.get('currency', 'XTR'),
|
||||
"service_status": service_result.get('status', 'success') if service_result else 'pending',
|
||||
"data_found_count": service_result.get('data_count', 0) if service_result else 0,
|
||||
"refund_status": payment_data.get('refund_status', 'no_refund'),
|
||||
"refund_reason": payment_data.get('refund_reason'),
|
||||
"vehicle_make": service_result.get('vehicle_make') if service_result else None,
|
||||
"vehicle_model": service_result.get('vehicle_model') if service_result else None,
|
||||
"vehicle_year": service_result.get('vehicle_year') if service_result else None,
|
||||
"error_message": service_result.get('error') if service_result else None,
|
||||
"ip_address": None # Telegram не предоставляет IP адреса
|
||||
}
|
||||
|
||||
cur.execute(insert_query, params)
|
||||
conn.commit()
|
||||
return True
|
||||
|
||||
try:
|
||||
import asyncio
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(None, _save_log)
|
||||
except Exception as e:
|
||||
print(f"Error saving payment log for user {user.id}: {e}")
|
||||
return False
|
||||
|
||||
100
db_sql/log_payment.sql
Normal file
100
db_sql/log_payment.sql
Normal file
@ -0,0 +1,100 @@
|
||||
-- Ñîçäàíèå òàáëèöû äëÿ ëîãèðîâàíèÿ âñåõ ïëàòåæíûõ îïåðàöèé
|
||||
-- Ñîäåðæèò èñ÷åðïûâàþùóþ èíôîðìàöèþ î êëèåíòå, óñëóãå, VIN è ðåçóëüòàòå
|
||||
|
||||
-- Ñîçäàíèå ïîñëåäîâàòåëüíîñòè äëÿ ID çàïèñåé
|
||||
CREATE SEQUENCE payment_logs_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
CACHE 100;
|
||||
|
||||
-- Ñîçäàíèå îñíîâíîé òàáëèöû ëîãîâ
|
||||
CREATE TABLE payment_logs (
|
||||
log_id NUMBER PRIMARY KEY,
|
||||
|
||||
-- Èíôîðìàöèÿ î ïîëüçîâàòåëå
|
||||
user_id NUMBER NOT NULL,
|
||||
user_first_name VARCHAR2(255),
|
||||
user_last_name VARCHAR2(255),
|
||||
user_username VARCHAR2(255),
|
||||
user_language_code VARCHAR2(10),
|
||||
user_is_premium NUMBER(1) DEFAULT 0,
|
||||
|
||||
-- Èíôîðìàöèÿ îá óñëóãå
|
||||
service_type VARCHAR2(50) NOT NULL, -- 'decode_vin', 'check_salvage', 'get_photos'
|
||||
vin_number VARCHAR2(17) NOT NULL,
|
||||
|
||||
-- Èíôîðìàöèÿ î ïëàòåæå
|
||||
payment_amount NUMBER(10,2) NOT NULL,
|
||||
transaction_id VARCHAR2(255),
|
||||
payment_status VARCHAR2(50) DEFAULT 'completed', -- 'completed', 'pending', 'failed'
|
||||
payment_currency VARCHAR2(10) DEFAULT 'XTR',
|
||||
|
||||
-- Ðåçóëüòàò ïðåäîñòàâëåíèÿ óñëóãè
|
||||
service_status VARCHAR2(50), -- 'success', 'failed', 'no_data', 'error'
|
||||
data_found_count NUMBER DEFAULT 0, -- êîëè÷åñòâî íàéäåííûõ çàïèñåé/ôîòî
|
||||
refund_status VARCHAR2(50) DEFAULT 'no_refund', -- 'no_refund', 'auto_refund', 'manual_refund', 'admin_refund'
|
||||
refund_reason VARCHAR2(500),
|
||||
|
||||
-- Èíôîðìàöèÿ îá àâòîìîáèëå
|
||||
vehicle_make VARCHAR2(100),
|
||||
vehicle_model VARCHAR2(100),
|
||||
vehicle_year VARCHAR2(10),
|
||||
|
||||
-- Òåõíè÷åñêàÿ èíôîðìàöèÿ
|
||||
error_message CLOB,
|
||||
created_date DATE DEFAULT SYSDATE,
|
||||
ip_address VARCHAR2(45), -- äëÿ áóäóùåãî èñïîëüçîâàíèÿ
|
||||
|
||||
-- Èíäåêñû äëÿ áûñòðîãî ïîèñêà
|
||||
CONSTRAINT chk_service_type CHECK (service_type IN ('decode_vin', 'check_salvage', 'get_photos')),
|
||||
CONSTRAINT chk_payment_status CHECK (payment_status IN ('completed', 'pending', 'failed')),
|
||||
CONSTRAINT chk_service_status CHECK (service_status IN ('success', 'failed', 'no_data', 'error', 'pending')),
|
||||
CONSTRAINT chk_refund_status CHECK (refund_status IN ('no_refund', 'auto_refund', 'manual_refund', 'admin_refund'))
|
||||
);
|
||||
|
||||
-- Ñîçäàíèå èíäåêñîâ äëÿ îïòèìèçàöèè çàïðîñîâ
|
||||
CREATE INDEX idx_payment_logs_user_id ON payment_logs(user_id);
|
||||
CREATE INDEX idx_payment_logs_vin ON payment_logs(vin_number);
|
||||
CREATE INDEX idx_payment_logs_service_type ON payment_logs(service_type);
|
||||
CREATE INDEX idx_payment_logs_date ON payment_logs(created_date);
|
||||
CREATE INDEX idx_payment_logs_transaction_id ON payment_logs(transaction_id);
|
||||
CREATE INDEX idx_payment_logs_status ON payment_logs(payment_status, service_status);
|
||||
|
||||
-- Êîììåíòàðèè ê òàáëèöå è ïîëÿì
|
||||
COMMENT ON TABLE payment_logs IS 'Ëîãèðîâàíèå âñåõ ïëàòåæíûõ îïåðàöèé áîòà ñ äåòàëüíîé èíôîðìàöèåé';
|
||||
COMMENT ON COLUMN payment_logs.log_id IS 'Óíèêàëüíûé èäåíòèôèêàòîð çàïèñè';
|
||||
COMMENT ON COLUMN payment_logs.user_id IS 'ID ïîëüçîâàòåëÿ Telegram';
|
||||
COMMENT ON COLUMN payment_logs.service_type IS 'Òèï óñëóãè: decode_vin, check_salvage, get_photos';
|
||||
COMMENT ON COLUMN payment_logs.vin_number IS 'VIN íîìåð àâòîìîáèëÿ';
|
||||
COMMENT ON COLUMN payment_logs.payment_amount IS 'Ñóììà ïëàòåæà â Telegram Stars';
|
||||
COMMENT ON COLUMN payment_logs.transaction_id IS 'ID òðàíçàêöèè Telegram';
|
||||
COMMENT ON COLUMN payment_logs.data_found_count IS 'Êîëè÷åñòâî íàéäåííûõ äàííûõ (çàïèñåé, ôîòî è ò.ä.)';
|
||||
COMMENT ON COLUMN payment_logs.refund_status IS 'Ñòàòóñ âîçâðàòà ñðåäñòâ';
|
||||
COMMENT ON COLUMN payment_logs.created_date IS 'Äàòà è âðåìÿ ñîçäàíèÿ çàïèñè';
|
||||
|
||||
-- Ñîçäàíèå ïðåäñòàâëåíèÿ äëÿ àíàëèòèêè
|
||||
CREATE OR REPLACE VIEW payment_analytics AS
|
||||
SELECT
|
||||
service_type,
|
||||
COUNT(*) as total_transactions,
|
||||
SUM(payment_amount) as total_revenue,
|
||||
AVG(payment_amount) as avg_payment,
|
||||
COUNT(CASE WHEN refund_status != 'no_refund' THEN 1 END) as refunds_count,
|
||||
COUNT(CASE WHEN service_status = 'success' THEN 1 END) as successful_services,
|
||||
ROUND(COUNT(CASE WHEN service_status = 'success' THEN 1 END) * 100.0 / COUNT(*), 2) as success_rate,
|
||||
TRUNC(created_date) as date_created
|
||||
FROM payment_logs
|
||||
GROUP BY service_type, TRUNC(created_date)
|
||||
ORDER BY date_created DESC, service_type;
|
||||
|
||||
|
||||
-- Ñîçäàíèå òðèããåðà äëÿ àâòîìàòè÷åñêîãî çàïîëíåíèÿ created_date
|
||||
CREATE OR REPLACE TRIGGER trg_payment_logs_created_date
|
||||
BEFORE INSERT ON payment_logs
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF :NEW.created_date IS NULL THEN
|
||||
:NEW.created_date := SYSDATE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
77
db_sql/salvage.sql
Normal file
77
db_sql/salvage.sql
Normal file
@ -0,0 +1,77 @@
|
||||
-- Create table
|
||||
create table SALVAGEDB
|
||||
(
|
||||
num NUMBER(20),
|
||||
vin VARCHAR2(20),
|
||||
svin VARCHAR2(15),
|
||||
odo NUMBER(11),
|
||||
odos VARCHAR2(50),
|
||||
title VARCHAR2(200),
|
||||
dem1 VARCHAR2(200),
|
||||
dem2 VARCHAR2(200),
|
||||
year NUMBER(4),
|
||||
month NUMBER(4),
|
||||
last_update DATE,
|
||||
auction VARCHAR2(1)
|
||||
)
|
||||
tablespace USERS
|
||||
pctfree 10
|
||||
initrans 1
|
||||
maxtrans 255
|
||||
storage
|
||||
(
|
||||
initial 3581M
|
||||
next 1M
|
||||
minextents 1
|
||||
maxextents unlimited
|
||||
)
|
||||
compress;
|
||||
-- Add comments to the columns
|
||||
comment on column SALVAGEDB.num
|
||||
is 'ID';
|
||||
comment on column SALVAGEDB.vin
|
||||
is 'vin';
|
||||
comment on column SALVAGEDB.odo
|
||||
is 'îäîìåòð';
|
||||
comment on column SALVAGEDB.odos
|
||||
is 'îäîìåòð ñòàòóñ';
|
||||
comment on column SALVAGEDB.title
|
||||
is 'äîêóìåíò';
|
||||
comment on column SALVAGEDB.dem1
|
||||
is 'ïîâðåäåëíèå 1';
|
||||
comment on column SALVAGEDB.dem2
|
||||
is 'ïîâðåæåäíèå 2';
|
||||
comment on column SALVAGEDB.year
|
||||
is 'ìåñÿö';
|
||||
comment on column SALVAGEDB.month
|
||||
is 'ãîä';
|
||||
comment on column SALVAGEDB.auction
|
||||
is 'Ñ êîïàðò I - IAAI';
|
||||
-- Create/Recreate indexes
|
||||
create index IDX_SALVAGEDB_NUM on SALVAGEDB (NUM)
|
||||
tablespace USERS
|
||||
pctfree 10
|
||||
initrans 2
|
||||
maxtrans 255
|
||||
storage
|
||||
(
|
||||
initial 64K
|
||||
next 1M
|
||||
minextents 1
|
||||
maxextents unlimited
|
||||
)
|
||||
compress nologging;
|
||||
create index IDX_SALVAGEDB_SVIN_VIN on SALVAGEDB (SVIN, VIN)
|
||||
tablespace USERS
|
||||
pctfree 10
|
||||
initrans 2
|
||||
maxtrans 255
|
||||
storage
|
||||
(
|
||||
initial 64K
|
||||
next 1M
|
||||
minextents 1
|
||||
maxextents unlimited
|
||||
);
|
||||
-- Grant/Revoke object privileges
|
||||
grant read on SALVAGEDB to SALVAGEBOT;
|
||||
36
db_sql/salvage_images.sql
Normal file
36
db_sql/salvage_images.sql
Normal file
@ -0,0 +1,36 @@
|
||||
-- Create table
|
||||
create table SALVAGE_IMAGES
|
||||
(
|
||||
vin VARCHAR2(20) not null,
|
||||
dateadd TIMESTAMP(6) not null,
|
||||
ipath VARCHAR2(500) not null,
|
||||
fn NUMBER(1) default 0 not null
|
||||
)
|
||||
tablespace USERS
|
||||
pctfree 0
|
||||
initrans 1
|
||||
maxtrans 255
|
||||
storage
|
||||
(
|
||||
initial 64K
|
||||
next 1M
|
||||
minextents 1
|
||||
maxextents unlimited
|
||||
)
|
||||
compress;
|
||||
-- Create/Recreate indexes
|
||||
create unique index VIB_DATEADD_IDX on SALVAGE_IMAGES (VIN, DATEADD)
|
||||
tablespace USERS
|
||||
pctfree 10
|
||||
initrans 2
|
||||
maxtrans 255
|
||||
storage
|
||||
(
|
||||
initial 64K
|
||||
next 1M
|
||||
minextents 1
|
||||
maxextents unlimited
|
||||
)
|
||||
compress 1;
|
||||
-- Grant/Revoke object privileges
|
||||
grant read on SALVAGE_IMAGES to SALVAGEBOT;
|
||||
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
@ -0,0 +1,35 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
salvagedb-bot:
|
||||
image: vladsimachkov/salvagebot:20250605
|
||||
container_name: salvagedb-telegram-bot
|
||||
restart: unless-stopped
|
||||
|
||||
# Переменные окружения
|
||||
environment:
|
||||
# Telegram Bot настройки
|
||||
- BOT_TOKEN=your_bot_token_here
|
||||
- BOT_NAME=SalvageDB Bot
|
||||
- ADMIN_USER_ID=123456789
|
||||
|
||||
# Цены на услуги (в Telegram Stars)
|
||||
- DECODE_PRICE=1
|
||||
- CHECK_PRICE=10
|
||||
- IMG_PRICE=100
|
||||
|
||||
# База данных Oracle
|
||||
- DB_USER=salvagebot
|
||||
- DB_PASSWORD=salvagebot
|
||||
- DB_DSN=localhost:1521/XEPDB1
|
||||
|
||||
# Настройки приложения
|
||||
- DEBUG=0
|
||||
- PYTHONUNBUFFERED=1
|
||||
- TZ=Europe/Moscow
|
||||
|
||||
# Монтируем volumes для логов и изображений
|
||||
volumes:
|
||||
- /volume2/salvagedb/salvage_bot/logs:/home/salvagebot/app/logs
|
||||
- /volume2/salvagedb/images:/images
|
||||
|
||||
98
docker-entrypoint.sh
Normal file
98
docker-entrypoint.sh
Normal file
@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт инициализации для Docker контейнера SalvageDB Bot
|
||||
set -e
|
||||
|
||||
echo "====================================================="
|
||||
echo "SalvageDB Telegram Bot - Docker Container Startup"
|
||||
echo "====================================================="
|
||||
|
||||
# Проверяем обязательные переменные окружения
|
||||
check_env_var() {
|
||||
if [ -z "${!1}" ]; then
|
||||
echo "❌ ERROR: Environment variable $1 is not set!"
|
||||
echo "Please check your .env file or docker-compose.yml"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ $1 is configured"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "🔍 Checking required environment variables..."
|
||||
check_env_var BOT_TOKEN
|
||||
check_env_var DB_USER
|
||||
check_env_var DB_PASSWORD
|
||||
check_env_var DB_DSN
|
||||
check_env_var ADMIN_USER_ID
|
||||
|
||||
echo ""
|
||||
echo "📋 Configuration Summary:"
|
||||
echo " • Bot Name: ${BOT_NAME:-'SalvageDB Bot'}"
|
||||
echo " • Debug Mode: ${DEBUG:-'0'}"
|
||||
echo " • Timezone: ${TZ:-'UTC'}"
|
||||
echo " • Decode Price: ${DECODE_PRICE:-'1'} ⭐"
|
||||
echo " • Check Price: ${CHECK_PRICE:-'10'} ⭐"
|
||||
echo " • Image Price: ${IMG_PRICE:-'100'} ⭐"
|
||||
echo " • Database DSN: ${DB_DSN}"
|
||||
echo " • Admin User ID: ${ADMIN_USER_ID}"
|
||||
|
||||
echo ""
|
||||
echo "📁 Creating directories..."
|
||||
mkdir -p /home/salvagebot/app/logs
|
||||
mkdir -p /home/salvagebot/app/images
|
||||
mkdir -p /home/salvagebot/app/data
|
||||
|
||||
echo "✅ Directory structure created"
|
||||
|
||||
echo ""
|
||||
echo "🐍 Testing Python environment..."
|
||||
python -c "
|
||||
import sys
|
||||
print(f'Python version: {sys.version}')
|
||||
print(f'Python path: {sys.executable}')
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "📦 Testing critical imports..."
|
||||
python -c "
|
||||
try:
|
||||
import aiogram
|
||||
print('✅ aiogram imported successfully')
|
||||
|
||||
import oracledb
|
||||
print('✅ oracledb imported successfully')
|
||||
|
||||
import asyncio
|
||||
print('✅ asyncio imported successfully')
|
||||
|
||||
import logging
|
||||
print('✅ logging imported successfully')
|
||||
|
||||
print('🎉 All critical dependencies are available')
|
||||
except ImportError as e:
|
||||
print(f'❌ Import error: {e}')
|
||||
exit(1)
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "🔗 Testing Oracle connectivity..."
|
||||
python -c "
|
||||
import oracledb
|
||||
import os
|
||||
|
||||
try:
|
||||
# Тестируем создание пула соединений (без подключения к БД)
|
||||
print('Testing Oracle client library...')
|
||||
print(f'Oracle client version: {oracledb.version}')
|
||||
print('✅ Oracle client is ready')
|
||||
except Exception as e:
|
||||
print(f'❌ Oracle client error: {e}')
|
||||
exit(1)
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "🚀 Starting SalvageDB Telegram Bot..."
|
||||
echo "====================================================="
|
||||
|
||||
# Запускаем основное приложение
|
||||
exec "$@"
|
||||
61
env.example
Normal file
61
env.example
Normal file
@ -0,0 +1,61 @@
|
||||
# ==================================================
|
||||
# SALVAGEDB TELEGRAM BOT - ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ
|
||||
# ==================================================
|
||||
|
||||
# --------------------------------------------------
|
||||
# TELEGRAM BOT НАСТРОЙКИ
|
||||
# --------------------------------------------------
|
||||
# Токен бота от @BotFather
|
||||
BOT_TOKEN=your_bot_token_here
|
||||
|
||||
# Имя бота (опционально)
|
||||
BOT_NAME=SalvageDB Bot
|
||||
|
||||
# ID администратора для возврата платежей и статистики
|
||||
ADMIN_USER_ID=123456789
|
||||
|
||||
# --------------------------------------------------
|
||||
# ЦЕНЫ НА УСЛУГИ (в Telegram Stars)
|
||||
# --------------------------------------------------
|
||||
# Цена за детальную информацию о VIN
|
||||
DECODE_PRICE=1
|
||||
|
||||
# Цена за детальную проверку повреждений
|
||||
CHECK_PRICE=10
|
||||
|
||||
# Цена за доступ к фотографиям
|
||||
IMG_PRICE=100
|
||||
|
||||
# --------------------------------------------------
|
||||
# БАЗА ДАННЫХ ORACLE
|
||||
# --------------------------------------------------
|
||||
# Пользователь базы данных
|
||||
DB_USER=your_db_user
|
||||
|
||||
# Пароль базы данных
|
||||
DB_PASSWORD=your_db_password
|
||||
|
||||
# DSN строка подключения к Oracle
|
||||
# Формат: host:port/service_name
|
||||
DB_DSN=localhost:1521/XEPDB1
|
||||
|
||||
# --------------------------------------------------
|
||||
# НАСТРОЙКИ ПРИЛОЖЕНИЯ
|
||||
# --------------------------------------------------
|
||||
# Режим отладки (0 = выключен, 1 = включен)
|
||||
DEBUG=0
|
||||
|
||||
# Часовой пояс
|
||||
TIMEZONE=UTC
|
||||
|
||||
# --------------------------------------------------
|
||||
# ДОПОЛНИТЕЛЬНЫЕ НАСТРОЙКИ (опционально)
|
||||
# --------------------------------------------------
|
||||
# Максимальный размер лог файла
|
||||
LOG_MAX_SIZE=10M
|
||||
|
||||
# Количество архивных лог файлов
|
||||
LOG_MAX_FILES=3
|
||||
|
||||
# Интервал очистки временных файлов (в часах)
|
||||
CLEANUP_INTERVAL=24
|
||||
781
main.py
781
main.py
@ -4,10 +4,11 @@ import logging
|
||||
from datetime import datetime
|
||||
import platform
|
||||
import os
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, LabeledPrice, PreCheckoutQuery
|
||||
from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, LabeledPrice, PreCheckoutQuery, InputMediaPhoto, FSInputFile
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.fsm.context import FSMContext
|
||||
@ -15,6 +16,80 @@ from db import OracleDatabase
|
||||
from middlewares.db import DbSessionMiddleware
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""
|
||||
Настройка системы логирования с ротацией файлов и выводом в консоль
|
||||
"""
|
||||
# Создаем каталог logs если он не существует
|
||||
if is_windows():
|
||||
logs_dir = "logs"
|
||||
else:
|
||||
logs_dir = "/logs/"
|
||||
if not os.path.exists(logs_dir):
|
||||
os.makedirs(logs_dir)
|
||||
print(f"Created logs directory: {logs_dir}")
|
||||
|
||||
# Определяем уровень логирования
|
||||
if getenv("DEBUG", '0') == '1':
|
||||
log_level = logging.INFO
|
||||
else:
|
||||
log_level = logging.WARNING
|
||||
|
||||
# Временно включаем детальное логирование для отладки
|
||||
log_level = logging.INFO
|
||||
|
||||
# Создаем основной логгер
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(log_level)
|
||||
|
||||
# Очищаем существующие обработчики
|
||||
logger.handlers.clear()
|
||||
|
||||
# Формат логов
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
# Настройка файлового логирования с ротацией
|
||||
file_handler = TimedRotatingFileHandler(
|
||||
filename=os.path.join(logs_dir, "salvagedb_bot.log"),
|
||||
when='midnight', # Ротация в полночь
|
||||
interval=1, # Каждый день
|
||||
backupCount=30, # Храним 30 дней
|
||||
encoding='utf-8',
|
||||
utc=False # Используем локальное время
|
||||
)
|
||||
file_handler.setLevel(log_level)
|
||||
file_handler.setFormatter(formatter)
|
||||
file_handler.suffix = "%Y-%m-%d" # Формат суффикса для файлов
|
||||
|
||||
# Настройка консольного логирования
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(log_level)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# Добавляем обработчики к логгеру
|
||||
logger.addHandler(file_handler)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# Логируем успешную настройку
|
||||
logging.info("=== LOGGING SYSTEM INITIALIZED ===")
|
||||
logging.info(f"Log level: {logging.getLevelName(log_level)}")
|
||||
logging.info(f"Logs directory: {os.path.abspath(logs_dir)}")
|
||||
logging.info(f"Log rotation: daily, keeping 30 days")
|
||||
|
||||
# Проверяем запуск в Docker
|
||||
if os.path.exists('/.dockerenv'):
|
||||
logging.info("Running inside Docker container")
|
||||
logging.info(f"Container timezone: {getenv('TZ', 'UTC')}")
|
||||
logging.info(f"Container user: {getenv('USER', 'unknown')}")
|
||||
else:
|
||||
logging.info("Running on host system")
|
||||
|
||||
logging.info("=== LOGGING SETUP COMPLETE ===")
|
||||
|
||||
|
||||
def get_us_state_name(state_code: str) -> str:
|
||||
"""
|
||||
Конвертирует двухбуквенный код штата США в полное название
|
||||
@ -148,13 +223,193 @@ def is_macos() -> bool:
|
||||
return get_operating_system() == 'macOS'
|
||||
|
||||
|
||||
if getenv("DEBUG",'0') == '1':
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
else:
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
def convert_photo_path(db_path: str) -> str:
|
||||
"""
|
||||
Конвертирует путь к фотографии в зависимости от операционной системы
|
||||
Args:
|
||||
db_path: путь из базы данных в Linux формате (с /)
|
||||
Returns:
|
||||
str: полный путь к файлу с учетом OS и базового пути
|
||||
"""
|
||||
if not db_path:
|
||||
return ""
|
||||
|
||||
# Временно включаем детальное логирование для отладки
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
# Убираем лишние пробелы из пути
|
||||
db_path = db_path.strip()
|
||||
|
||||
# Базовый путь из константы
|
||||
base_path = image_path
|
||||
|
||||
if is_windows():
|
||||
# Конвертируем Linux пути в Windows формат
|
||||
windows_path = db_path.replace('/', '\\')
|
||||
full_path = f"{base_path}\\{windows_path}"
|
||||
logging.info(f"Converted path for Windows: {db_path} -> {full_path}")
|
||||
return full_path
|
||||
else:
|
||||
# Для Linux/macOS оставляем как есть
|
||||
full_path = f"{base_path}/{db_path}"
|
||||
logging.info(f"Path for Linux/macOS: {db_path} -> {full_path}")
|
||||
return full_path
|
||||
|
||||
|
||||
def prepare_photo_paths(db_paths: list) -> list:
|
||||
"""
|
||||
Подготавливает список полных путей к фотографиям
|
||||
Args:
|
||||
db_paths: список путей из базы данных
|
||||
Returns:
|
||||
list: список полных путей к файлам
|
||||
"""
|
||||
if not db_paths:
|
||||
return []
|
||||
|
||||
full_paths = []
|
||||
for db_path in db_paths:
|
||||
full_path = convert_photo_path(db_path)
|
||||
if full_path:
|
||||
full_paths.append(full_path)
|
||||
|
||||
logging.info(f"Prepared {len(full_paths)} photo paths from {len(db_paths)} database paths")
|
||||
return full_paths
|
||||
|
||||
|
||||
async def send_vehicle_photos(message: Message, vin: str, photo_paths: list, make: str, model: str, year: str):
|
||||
"""
|
||||
Отправляет фотографии автомобиля пользователю
|
||||
Args:
|
||||
message: сообщение пользователя
|
||||
vin: VIN автомобиля
|
||||
photo_paths: список полных путей к фотографиям
|
||||
make, model, year: информация об автомобиле
|
||||
"""
|
||||
if not photo_paths:
|
||||
await message.answer("❌ No photos found to send.")
|
||||
return
|
||||
|
||||
try:
|
||||
# Дебаг информация о текущем пользователе
|
||||
import pwd
|
||||
import grp
|
||||
current_user = pwd.getpwuid(os.getuid())
|
||||
current_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()]
|
||||
logging.info(f"DEBUG: Running as user: {current_user.pw_name}({os.getuid()}), groups: {current_groups}")
|
||||
|
||||
# Telegram позволяет максимум 10 фотографий в media group
|
||||
photos_per_group = 10
|
||||
total_photos = len(photo_paths)
|
||||
|
||||
logging.info(f"Attempting to send {total_photos} photos for VIN: {vin}")
|
||||
|
||||
# Разбиваем фотографии на группы по 10
|
||||
photo_groups = [photo_paths[i:i + photos_per_group] for i in range(0, len(photo_paths), photos_per_group)]
|
||||
total_groups = len(photo_groups)
|
||||
|
||||
logging.info(f"Split {total_photos} photos into {total_groups} groups")
|
||||
|
||||
sent_count = 0
|
||||
|
||||
for group_num, photo_group in enumerate(photo_groups, 1):
|
||||
try:
|
||||
logging.info(f"Processing group {group_num}/{total_groups} with {len(photo_group)} photos")
|
||||
|
||||
# Создаем media group для текущей группы
|
||||
media_group = []
|
||||
|
||||
for i, photo_path in enumerate(photo_group):
|
||||
try:
|
||||
# Проверяем существование файла
|
||||
# Дебаг информация
|
||||
import stat
|
||||
import pwd
|
||||
import grp
|
||||
try:
|
||||
stat_info = os.stat(photo_path)
|
||||
file_owner = pwd.getpwuid(stat_info.st_uid).pw_name
|
||||
file_group = grp.getgrgid(stat_info.st_gid).gr_name
|
||||
file_perms = oct(stat_info.st_mode)[-3:]
|
||||
logging.info(f"DEBUG: File {photo_path} - owner: {file_owner}({stat_info.st_uid}), group: {file_group}({stat_info.st_gid}), perms: {file_perms}")
|
||||
except Exception as debug_e:
|
||||
logging.warning(f"DEBUG: Cannot get file info for {photo_path}: {debug_e}")
|
||||
|
||||
if os.path.exists(photo_path):
|
||||
# Создаем InputMediaPhoto
|
||||
if i == 0 and group_num == 1:
|
||||
# Первая фотография первой группы с полным описанием
|
||||
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
|
||||
caption = f"📸 Vehicle damage photos\n📋 VIN: {vin}\n📊 Total photos: {total_photos}\n🗂️ Group {group_num}/{total_groups}"
|
||||
else:
|
||||
caption = f"📸 {year} {make} {model}\n📋 VIN: {vin}\n📊 Total photos: {total_photos}\n🗂️ Group {group_num}/{total_groups}"
|
||||
elif i == 0:
|
||||
# Первая фотография других групп с номером группы
|
||||
caption = f"🗂️ Group {group_num}/{total_groups}"
|
||||
else:
|
||||
# Остальные фотографии без описания
|
||||
caption = None
|
||||
|
||||
if caption:
|
||||
media_group.append(InputMediaPhoto(
|
||||
media=FSInputFile(photo_path),
|
||||
caption=caption
|
||||
))
|
||||
else:
|
||||
media_group.append(InputMediaPhoto(
|
||||
media=FSInputFile(photo_path)
|
||||
))
|
||||
|
||||
sent_count += 1
|
||||
logging.info(f"Added photo {sent_count}/{total_photos}: {photo_path}")
|
||||
else:
|
||||
logging.warning(f"Photo file not found: {photo_path}")
|
||||
except Exception as photo_error:
|
||||
logging.error(f"Error processing photo {sent_count + 1} ({photo_path}): {photo_error}")
|
||||
continue
|
||||
|
||||
if media_group:
|
||||
# Отправляем media group
|
||||
await message.answer_media_group(media_group)
|
||||
logging.info(f"Successfully sent group {group_num}/{total_groups} with {len(media_group)} photos")
|
||||
|
||||
# Небольшая пауза между группами для избежания rate limiting
|
||||
if group_num < total_groups:
|
||||
await asyncio.sleep(0.5)
|
||||
else:
|
||||
logging.warning(f"No valid photos in group {group_num}")
|
||||
|
||||
except Exception as group_error:
|
||||
logging.error(f"Error sending photo group {group_num}: {group_error}")
|
||||
await message.answer(f"❌ Error sending photo group {group_num}. Continuing with remaining photos...")
|
||||
continue
|
||||
|
||||
if sent_count > 0:
|
||||
# Отправляем итоговое сообщение
|
||||
await message.answer(
|
||||
f"✅ **Photos sent successfully!**\n"
|
||||
f"📊 **{sent_count} of {total_photos} photos** delivered\n"
|
||||
f"🗂️ **{total_groups} photo groups** sent"
|
||||
)
|
||||
logging.info(f"Successfully sent {sent_count}/{total_photos} photos for VIN: {vin}")
|
||||
else:
|
||||
# Если ни одна фотография не найдена
|
||||
await message.answer(
|
||||
"❌ **Error:** Photo files not found on server.\n"
|
||||
"Please contact support with your transaction details."
|
||||
)
|
||||
logging.error(f"No valid photo files found for VIN: {vin}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error sending photos for VIN {vin}: {e}")
|
||||
await message.answer(
|
||||
"❌ **Error sending photos.** Please contact support with your transaction details.\n"
|
||||
f"Error details: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# Настройка системы логирования
|
||||
setup_logging()
|
||||
|
||||
# Логируем информацию о системе
|
||||
log_system_info()
|
||||
|
||||
|
||||
TOKEN = getenv("BOT_TOKEN")
|
||||
@ -167,13 +422,13 @@ IMG_PRICE = getenv("IMG_PRICE",100)
|
||||
if is_windows():
|
||||
image_path = "D:\\SALVAGEDB\\salvagedb_bot\\images"
|
||||
else:
|
||||
image_path = "/images/"
|
||||
image_path = "/images"
|
||||
|
||||
|
||||
oracle_db = OracleDatabase(
|
||||
user= getenv("db_user"),
|
||||
password= getenv("db_password"),
|
||||
dsn= getenv("db_dsn")
|
||||
user= getenv("DB_USER"),
|
||||
password= getenv("DB_PASSWORD"),
|
||||
dsn= getenv("DB_DSN")
|
||||
)
|
||||
|
||||
|
||||
@ -182,6 +437,7 @@ dp = Dispatcher()
|
||||
class VinStates(StatesGroup):
|
||||
waiting_for_vin = State()
|
||||
waiting_for_check_vin = State()
|
||||
waiting_for_photo_vin = State()
|
||||
|
||||
|
||||
# Command handler
|
||||
@ -240,6 +496,19 @@ async def check_vin_callback(callback: CallbackQuery, state: FSMContext, db: Ora
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@dp.callback_query(lambda c: c.data == "search_car_photo")
|
||||
async def search_car_photo_callback(callback: CallbackQuery, state: FSMContext, db: OracleDatabase = None):
|
||||
# Используем переданный db или глобальный oracle_db
|
||||
database = db or oracle_db
|
||||
|
||||
# Сохраняем данные пользователя при нажатии кнопки
|
||||
await database.save_user(callback.from_user, "search_car_photo_button")
|
||||
|
||||
await callback.message.answer("Please enter the vehicle VIN to search for damage photos.")
|
||||
await state.set_state(VinStates.waiting_for_photo_vin)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@dp.callback_query(lambda c: c.data == "main_menu")
|
||||
async def main_menu_callback(callback: CallbackQuery, state: FSMContext, db: OracleDatabase = None):
|
||||
# Используем переданный db или глобальный oracle_db
|
||||
@ -253,6 +522,109 @@ async def main_menu_callback(callback: CallbackQuery, state: FSMContext, db: Ora
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@dp.callback_query(lambda c: c.data == "help")
|
||||
async def help_callback(callback: CallbackQuery, db: OracleDatabase = None):
|
||||
# Используем переданный db или глобальный oracle_db
|
||||
database = db or oracle_db
|
||||
|
||||
# Сохраняем данные пользователя при просмотре справки
|
||||
await database.save_user(callback.from_user, "help_button")
|
||||
|
||||
help_text = (
|
||||
"ℹ️ **Help - Our Services**\n\n"
|
||||
|
||||
"🔍 **1. Decode VIN**\n"
|
||||
f"• Free basic vehicle information (make, model, year)\n"
|
||||
f"• Detailed specifications for {DECODE_PRICE} ⭐ (engine, transmission, safety features, etc.)\n"
|
||||
f"• Comprehensive technical data from NHTSA database\n\n"
|
||||
|
||||
"🚨 **2. Check VIN**\n"
|
||||
f"• Free salvage records count\n"
|
||||
f"• Detailed damage history for {CHECK_PRICE} ⭐ (auction data, damage types, repair costs)\n"
|
||||
f"• Sale dates and locations from insurance auctions\n\n"
|
||||
|
||||
"📸 **3. Search Car Photos**\n"
|
||||
f"• Check availability of damage photos\n"
|
||||
f"• Access to actual auction photos for {IMG_PRICE} ⭐\n"
|
||||
f"• High-quality images showing vehicle condition and damage\n\n"
|
||||
|
||||
"💡 **How to use:**\n"
|
||||
"• Enter any 17-character VIN number\n"
|
||||
"• VIN must contain only letters and numbers (no I, O, Q)\n"
|
||||
"• All payments are made with Telegram Stars ⭐\n\n"
|
||||
|
||||
"⚠️ **Important:** Our reports show historical data for informed decision-making. "
|
||||
"Always consult automotive experts for professional vehicle evaluation."
|
||||
)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🔍 Try Decode VIN", callback_data="decode_vin")
|
||||
builder.button(text="🚨 Try Check VIN", callback_data="check_vin")
|
||||
builder.button(text="📸 Try Search Photos", callback_data="search_car_photo")
|
||||
builder.adjust(3)
|
||||
builder.button(text="🏠 Back to Main Menu", callback_data="main_menu")
|
||||
builder.adjust(3, 1)
|
||||
|
||||
await callback.message.answer(help_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@dp.callback_query(lambda c: c.data == "prices")
|
||||
async def prices_callback(callback: CallbackQuery, db: OracleDatabase = None):
|
||||
# Используем переданный db или глобальный oracle_db
|
||||
database = db or oracle_db
|
||||
|
||||
# Сохраняем данные пользователя при просмотре цен
|
||||
await database.save_user(callback.from_user, "prices_button")
|
||||
|
||||
prices_text = (
|
||||
"💰 **Our Service Prices**\n\n"
|
||||
|
||||
"🔍 **VIN Decoding Service:**\n"
|
||||
f"• Basic info (make, model, year): **FREE** 🆓\n"
|
||||
f"• Detailed specifications: **{DECODE_PRICE} ⭐**\n"
|
||||
f" └ Engine details, transmission, safety features\n"
|
||||
f" └ Dimensions, construction, brake system\n"
|
||||
f" └ Lighting, additional features, NCSA data\n\n"
|
||||
|
||||
"🚨 **Salvage Check Service:**\n"
|
||||
f"• Records count check: **FREE** 🆓\n"
|
||||
f"• Detailed damage history: **{CHECK_PRICE} ⭐**\n"
|
||||
f" └ Primary and secondary damage types\n"
|
||||
f" └ Sale dates and auction locations\n"
|
||||
f" └ Odometer readings and repair costs\n"
|
||||
f" └ Engine/drive status information\n\n"
|
||||
|
||||
"📸 **Vehicle Photos Service:**\n"
|
||||
f"• Photo availability check: **FREE** 🆓\n"
|
||||
f"• Access to damage photos: **{IMG_PRICE} ⭐**\n"
|
||||
f" └ High-quality auction images\n"
|
||||
f" └ Multiple angles of vehicle damage\n"
|
||||
f" └ Before and after condition photos\n\n"
|
||||
|
||||
"⭐ **Payment Information:**\n"
|
||||
"• All payments made with **Telegram Stars**\n"
|
||||
"• Instant delivery after successful payment\n"
|
||||
"• Automatic refund if no data found\n"
|
||||
"• Admin users get automatic refunds\n\n"
|
||||
|
||||
"💡 **Money-back guarantee:** If we can't provide the requested data, "
|
||||
"your payment will be automatically refunded!"
|
||||
)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=f"🔍 Decode for {DECODE_PRICE} ⭐", callback_data="decode_vin")
|
||||
builder.button(text=f"🚨 Check for {CHECK_PRICE} ⭐", callback_data="check_vin")
|
||||
builder.button(text=f"📸 Photos for {IMG_PRICE} ⭐", callback_data="search_car_photo")
|
||||
builder.adjust(3)
|
||||
builder.button(text="ℹ️ Help", callback_data="help")
|
||||
builder.button(text="🏠 Back to Main Menu", callback_data="main_menu")
|
||||
builder.adjust(2, 1)
|
||||
|
||||
await callback.message.answer(prices_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@dp.callback_query(lambda c: c.data and c.data.startswith("pay_detailed_info:"))
|
||||
async def pay_detailed_info_callback(callback: CallbackQuery, db: OracleDatabase = None):
|
||||
# Используем переданный db или глобальный oracle_db
|
||||
@ -263,12 +635,12 @@ async def pay_detailed_info_callback(callback: CallbackQuery, db: OracleDatabase
|
||||
|
||||
# Extract VIN from callback data
|
||||
vin = callback.data.split(":")[1]
|
||||
prices = [LabeledPrice(label="Detailed VIN Report", amount=1)]
|
||||
prices = [LabeledPrice(label="Detailed VIN Report", amount=DECODE_PRICE)]
|
||||
logging.info(f"Sending invoice for VIN: {vin}")
|
||||
await callback.bot.send_invoice(
|
||||
chat_id=callback.message.chat.id,
|
||||
title="Detailed VIN Information",
|
||||
description="Get comprehensive vehicle detailed information for 1 Telegram Star",
|
||||
description=f"Get comprehensive vehicle detailed information for {DECODE_PRICE} Telegram Star",
|
||||
payload=f"detailed_vin_info:{vin}", # Include VIN in payload
|
||||
provider_token="", # Empty for Telegram Stars
|
||||
currency="XTR", # Telegram Stars currency
|
||||
@ -364,7 +736,7 @@ async def process_check_vin(message: Message, state: FSMContext, db: OracleDatab
|
||||
|
||||
if salvage_count > 0:
|
||||
# Есть записи - показываем кнопки: Pay 10⭐️ for detailed info, Try another VIN, Back to main menu
|
||||
builder.button(text="Pay 10 ⭐️ for detailed info", callback_data=f"pay_check_detailed:{vin}", pay=True)
|
||||
builder.button(text=f"Pay {CHECK_PRICE} ⭐️ for detailed info", callback_data=f"pay_check_detailed:{vin}", pay=True)
|
||||
builder.button(text="Try another VIN", callback_data="check_vin")
|
||||
builder.button(text="Back to Main Menu", callback_data="main_menu")
|
||||
builder.adjust(1, 1, 1) # Each button on separate row
|
||||
@ -397,13 +769,13 @@ async def pay_check_detailed_callback(callback: CallbackQuery, db: OracleDatabas
|
||||
|
||||
# Извлекаем VIN из callback data
|
||||
vin = callback.data.split(":")[1]
|
||||
prices = [LabeledPrice(label="Detailed Salvage Report", amount=10)]
|
||||
prices = [LabeledPrice(label="Detailed Salvage Report", amount=CHECK_PRICE)]
|
||||
logging.info(f"Sending invoice for salvage check VIN: {vin}")
|
||||
|
||||
await callback.bot.send_invoice(
|
||||
chat_id=callback.message.chat.id,
|
||||
title="Detailed Salvage Report",
|
||||
description="Get comprehensive salvage history and damage information for 10 Telegram Stars",
|
||||
description=f"Get comprehensive salvage history and damage information for {CHECK_PRICE} Telegram Stars",
|
||||
payload=f"detailed_salvage_check:{vin}", # Уникальный payload для этого типа платежа
|
||||
provider_token="", # Empty for Telegram Stars
|
||||
currency="XTR", # Telegram Stars currency
|
||||
@ -412,6 +784,31 @@ async def pay_check_detailed_callback(callback: CallbackQuery, db: OracleDatabas
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@dp.callback_query(lambda c: c.data and c.data.startswith("pay_photos:"))
|
||||
async def pay_photos_callback(callback: CallbackQuery, db: OracleDatabase = None):
|
||||
# Используем переданный db или глобальный oracle_db
|
||||
database = db or oracle_db
|
||||
|
||||
# Сохраняем данные пользователя при инициации платежа
|
||||
await database.save_user(callback.from_user, "photos_payment_initiation")
|
||||
|
||||
# Извлекаем VIN из callback data
|
||||
vin = callback.data.split(":")[1]
|
||||
prices = [LabeledPrice(label="Vehicle Damage Photos", amount=IMG_PRICE)]
|
||||
logging.info(f"Sending invoice for photos VIN: {vin}")
|
||||
|
||||
await callback.bot.send_invoice(
|
||||
chat_id=callback.message.chat.id,
|
||||
title="Vehicle Damage Photos",
|
||||
description=f"Get access to all damage photos for this vehicle for {IMG_PRICE} Telegram Stars",
|
||||
payload=f"vehicle_photos:{vin}", # Уникальный payload для фотографий
|
||||
provider_token="", # Empty for Telegram Stars
|
||||
currency="XTR", # Telegram Stars currency
|
||||
prices=prices
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@dp.pre_checkout_query()
|
||||
async def pre_checkout_handler(pre_checkout_query: PreCheckoutQuery, db: OracleDatabase = None):
|
||||
# Используем переданный db или глобальный oracle_db
|
||||
@ -475,9 +872,24 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
||||
payload = message.successful_payment.invoice_payload
|
||||
|
||||
# Определяем сумму платежа в зависимости от типа
|
||||
payment_amount = 10.0 if payload.startswith("detailed_salvage_check:") else 1.0
|
||||
if payload.startswith("detailed_salvage_check:"):
|
||||
payment_amount = float(CHECK_PRICE)
|
||||
elif payload.startswith("vehicle_photos:"):
|
||||
payment_amount = float(IMG_PRICE)
|
||||
else:
|
||||
payment_amount = float(DECODE_PRICE)
|
||||
|
||||
await database.update_user_payment(message.from_user.id, payment_amount)
|
||||
|
||||
# Подготавливаем базовые данные платежа для логирования
|
||||
payment_data = {
|
||||
'amount': payment_amount,
|
||||
'transaction_id': message.successful_payment.telegram_payment_charge_id,
|
||||
'status': 'completed',
|
||||
'currency': 'XTR',
|
||||
'refund_status': 'no_refund'
|
||||
}
|
||||
|
||||
if payload.startswith("detailed_vin_info:"):
|
||||
vin = payload.split(":")[1]
|
||||
|
||||
@ -604,6 +1016,17 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
||||
await message.answer(plain_report, reply_markup=builder.as_markup())
|
||||
logging.info("Plain text message sent successfully!")
|
||||
|
||||
# Логируем успешную операцию DecodeVin
|
||||
service_result = {
|
||||
'status': 'success',
|
||||
'data_count': len(detailed_info['all_params']),
|
||||
'vehicle_make': params.get('make', 'N/A'),
|
||||
'vehicle_model': params.get('model', 'N/A'),
|
||||
'vehicle_year': params.get('model_year', 'N/A')
|
||||
}
|
||||
await database.save_payment_log(message.from_user, "decode_vin", vin, payment_data, service_result)
|
||||
logging.info(f"Payment logged successfully for DecodeVin service - User: {message.from_user.id}, VIN: {vin}")
|
||||
|
||||
# Проверяем, является ли пользователь администратором и возвращаем звезды
|
||||
if message.from_user.id == ADMIN_USER_ID:
|
||||
try:
|
||||
@ -611,6 +1034,7 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
||||
user_id=message.from_user.id,
|
||||
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
|
||||
)
|
||||
payment_data['refund_status'] = 'admin_refund'
|
||||
await message.answer(
|
||||
"🔧 **Admin Refund**\n\n"
|
||||
f"💰 Payment automatically refunded for admin user.\n"
|
||||
@ -629,6 +1053,16 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
||||
)
|
||||
else:
|
||||
# No detailed information found - refund the payment
|
||||
service_result = {
|
||||
'status': 'no_data',
|
||||
'data_count': 0,
|
||||
'error': 'No detailed information found for VIN'
|
||||
}
|
||||
payment_data['refund_status'] = 'auto_refund'
|
||||
payment_data['refund_reason'] = 'No detailed information found'
|
||||
await database.save_payment_log(message.from_user, "decode_vin", vin, payment_data, service_result)
|
||||
logging.info(f"Payment logged for DecodeVin no data case - User: {message.from_user.id}, VIN: {vin}")
|
||||
|
||||
try:
|
||||
await message.bot.refund_star_payment(
|
||||
user_id=message.from_user.id,
|
||||
@ -647,10 +1081,20 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
||||
"⚠️ Please contact support with this transaction ID for a refund:\n"
|
||||
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting detailed VIN info for {vin}: {e}")
|
||||
|
||||
# Логируем ошибку
|
||||
service_result = {
|
||||
'status': 'error',
|
||||
'data_count': 0,
|
||||
'error': str(e)
|
||||
}
|
||||
payment_data['refund_status'] = 'auto_refund'
|
||||
payment_data['refund_reason'] = 'Service error'
|
||||
await database.save_payment_log(message.from_user, "decode_vin", vin, payment_data, service_result)
|
||||
logging.info(f"Payment logged for DecodeVin error case - User: {message.from_user.id}, VIN: {vin}")
|
||||
|
||||
# Attempt to refund the payment due to service error
|
||||
try:
|
||||
await message.bot.refund_star_payment(
|
||||
@ -785,6 +1229,17 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
||||
await message.answer(plain_report, reply_markup=builder.as_markup())
|
||||
logging.info("Plain text message sent successfully!")
|
||||
|
||||
# Логируем успешную операцию CheckSalvage
|
||||
service_result = {
|
||||
'status': 'success',
|
||||
'data_count': len(salvage_records),
|
||||
'vehicle_make': make,
|
||||
'vehicle_model': model,
|
||||
'vehicle_year': year
|
||||
}
|
||||
await database.save_payment_log(message.from_user, "check_salvage", vin, payment_data, service_result)
|
||||
logging.info(f"Payment logged successfully for CheckSalvage service - User: {message.from_user.id}, VIN: {vin}")
|
||||
|
||||
# Отправляем отдельное сообщение о фотографиях
|
||||
if salvage_records and salvage_records[0]['img_count'] > 0:
|
||||
img_count = salvage_records[0]['img_count']
|
||||
@ -802,6 +1257,7 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
||||
user_id=message.from_user.id,
|
||||
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
|
||||
)
|
||||
payment_data['refund_status'] = 'admin_refund'
|
||||
await message.answer(
|
||||
"🔧 **Admin Refund**\n\n"
|
||||
f"💰 Payment automatically refunded for admin user.\n"
|
||||
@ -820,6 +1276,16 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
||||
)
|
||||
else:
|
||||
# Нет записей - возвращаем деньги
|
||||
service_result = {
|
||||
'status': 'no_data',
|
||||
'data_count': 0,
|
||||
'error': 'No salvage records found for VIN'
|
||||
}
|
||||
payment_data['refund_status'] = 'auto_refund'
|
||||
payment_data['refund_reason'] = 'No salvage records found'
|
||||
await database.save_payment_log(message.from_user, "check_salvage", vin, payment_data, service_result)
|
||||
logging.info(f"Payment logged for CheckSalvage no data case - User: {message.from_user.id}, VIN: {vin}")
|
||||
|
||||
try:
|
||||
await message.bot.refund_star_payment(
|
||||
user_id=message.from_user.id,
|
||||
@ -842,6 +1308,17 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting salvage info for {vin}: {e}")
|
||||
|
||||
# Логируем ошибку
|
||||
service_result = {
|
||||
'status': 'error',
|
||||
'data_count': 0,
|
||||
'error': str(e)
|
||||
}
|
||||
payment_data['refund_status'] = 'auto_refund'
|
||||
payment_data['refund_reason'] = 'Service error'
|
||||
await database.save_payment_log(message.from_user, "check_salvage", vin, payment_data, service_result)
|
||||
logging.info(f"Payment logged for CheckSalvage error case - User: {message.from_user.id}, VIN: {vin}")
|
||||
|
||||
# Возвращаем деньги при ошибке
|
||||
try:
|
||||
await message.bot.refund_star_payment(
|
||||
@ -861,6 +1338,213 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
||||
"⚠️ Please contact support immediately with this transaction ID for a manual refund:\n"
|
||||
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
|
||||
)
|
||||
elif payload.startswith("vehicle_photos:"):
|
||||
vin = payload.split(":")[1]
|
||||
|
||||
try:
|
||||
# Получаем информацию о VIN и количество фотографий
|
||||
make, model, year, cnt = await database.fetch_vin_info(vin)
|
||||
photo_count = await database.count_photo_records(vin)
|
||||
|
||||
logging.info(f"Photos payment for VIN: {vin}, make: {make}, model: {model}, year: {year}, photo_count: {photo_count}")
|
||||
|
||||
if photo_count > 0:
|
||||
# Есть фотографии - предоставляем доступ (пока заглушка)
|
||||
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
|
||||
response_text = f"📸 **Vehicle Damage Photos**\n\n"
|
||||
else:
|
||||
response_text = f"🚗 **{year} {make} {model}**\n\n"
|
||||
response_text += f"📸 **Vehicle Damage Photos**\n\n"
|
||||
|
||||
response_text += f"✅ **Payment successful!** You now have access to **{photo_count} damage photos** for this vehicle.\n\n"
|
||||
response_text += f"📁 **Photos will be sent separately** - please wait while we prepare your images.\n\n"
|
||||
response_text += f"---\n"
|
||||
response_text += f"💰 **Transaction ID:** {escape_markdown(message.successful_payment.telegram_payment_charge_id)}\n"
|
||||
response_text += f"📋 **VIN:** {escape_markdown(vin)}"
|
||||
|
||||
# Создаем клавиатуру с дополнительными действиями
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="Search another VIN", callback_data="search_car_photo")
|
||||
builder.button(text="Back to Main Menu", callback_data="main_menu")
|
||||
builder.adjust(2)
|
||||
|
||||
logging.info("Attempting to send photos payment success message...")
|
||||
try:
|
||||
await message.answer(response_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
|
||||
logging.info("Photos payment message sent successfully!")
|
||||
|
||||
# Получаем пути к фотографиям из базы данных
|
||||
logging.info(f"Fetching photo paths for VIN: {vin}")
|
||||
db_photo_paths = await database.fetch_photo_paths(vin)
|
||||
logging.info(f"Found {len(db_photo_paths)} photo paths in database")
|
||||
|
||||
if db_photo_paths:
|
||||
# Подготавливаем полные пути к файлам
|
||||
full_photo_paths = prepare_photo_paths(db_photo_paths)
|
||||
logging.info(f"Prepared {len(full_photo_paths)} full photo paths")
|
||||
|
||||
# Отправляем фотографии
|
||||
await send_vehicle_photos(message, vin, full_photo_paths, make, model, year)
|
||||
|
||||
# Логируем успешную операцию GetPhotos
|
||||
service_result = {
|
||||
'status': 'success',
|
||||
'data_count': len(full_photo_paths),
|
||||
'vehicle_make': make,
|
||||
'vehicle_model': model,
|
||||
'vehicle_year': year
|
||||
}
|
||||
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
|
||||
logging.info(f"Payment logged successfully for GetPhotos service - User: {message.from_user.id}, VIN: {vin}")
|
||||
else:
|
||||
await message.answer(
|
||||
"⚠️ **Warning:** No photo paths found in database despite photo count > 0.\n"
|
||||
"Please contact support with your transaction details."
|
||||
)
|
||||
logging.warning(f"No photo paths found for VIN {vin} despite photo_count = {photo_count}")
|
||||
|
||||
# Логируем проблему с путями к фотографиям
|
||||
service_result = {
|
||||
'status': 'error',
|
||||
'data_count': 0,
|
||||
'error': 'Photo paths not found despite photo count > 0'
|
||||
}
|
||||
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
|
||||
logging.info(f"Payment logged for GetPhotos path error case - User: {message.from_user.id}, VIN: {vin}")
|
||||
|
||||
except Exception as markdown_error:
|
||||
logging.error(f"Markdown parsing failed for photos payment: {markdown_error}")
|
||||
plain_response = response_text.replace("**", "").replace("*", "")
|
||||
await message.answer(plain_response, reply_markup=builder.as_markup())
|
||||
logging.info("Plain text photos payment message sent successfully!")
|
||||
|
||||
# Получаем пути к фотографиям из базы данных (fallback)
|
||||
logging.info(f"Fetching photo paths for VIN: {vin} (fallback)")
|
||||
db_photo_paths = await database.fetch_photo_paths(vin)
|
||||
logging.info(f"Found {len(db_photo_paths)} photo paths in database (fallback)")
|
||||
|
||||
if db_photo_paths:
|
||||
# Подготавливаем полные пути к файлам
|
||||
full_photo_paths = prepare_photo_paths(db_photo_paths)
|
||||
logging.info(f"Prepared {len(full_photo_paths)} full photo paths (fallback)")
|
||||
|
||||
# Отправляем фотографии
|
||||
await send_vehicle_photos(message, vin, full_photo_paths, make, model, year)
|
||||
|
||||
# Логируем успешную операцию GetPhotos (fallback)
|
||||
service_result = {
|
||||
'status': 'success',
|
||||
'data_count': len(full_photo_paths),
|
||||
'vehicle_make': make,
|
||||
'vehicle_model': model,
|
||||
'vehicle_year': year
|
||||
}
|
||||
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
|
||||
logging.info(f"Payment logged successfully for GetPhotos service (fallback) - User: {message.from_user.id}, VIN: {vin}")
|
||||
else:
|
||||
await message.answer(
|
||||
"Warning: No photo paths found in database despite photo count > 0. "
|
||||
"Please contact support with your transaction details."
|
||||
)
|
||||
logging.warning(f"No photo paths found for VIN {vin} despite photo_count = {photo_count} (fallback)")
|
||||
|
||||
# Логируем проблему с путями к фотографиям (fallback)
|
||||
service_result = {
|
||||
'status': 'error',
|
||||
'data_count': 0,
|
||||
'error': 'Photo paths not found despite photo count > 0 (fallback)'
|
||||
}
|
||||
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
|
||||
logging.info(f"Payment logged for GetPhotos fallback path error case - User: {message.from_user.id}, VIN: {vin}")
|
||||
|
||||
# Проверяем, является ли пользователь администратором и возвращаем звезды
|
||||
if message.from_user.id == ADMIN_USER_ID:
|
||||
try:
|
||||
await message.bot.refund_star_payment(
|
||||
user_id=message.from_user.id,
|
||||
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
|
||||
)
|
||||
payment_data['refund_status'] = 'admin_refund'
|
||||
await message.answer(
|
||||
"🔧 **Admin Refund**\n\n"
|
||||
f"💰 Payment automatically refunded for admin user.\n"
|
||||
f"🆔 Transaction ID: {escape_markdown(message.successful_payment.telegram_payment_charge_id)}\n"
|
||||
"ℹ️ Admin access - no charges applied.",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
logging.info(f"Admin refund successful for user {message.from_user.id}")
|
||||
except Exception as refund_error:
|
||||
logging.error(f"Failed to refund admin payment: {refund_error}")
|
||||
await message.answer(
|
||||
"⚠️ **Admin Refund Failed**\n\n"
|
||||
"Could not automatically refund admin payment. Please contact technical support.\n"
|
||||
f"🆔 Transaction ID: {escape_markdown(message.successful_payment.telegram_payment_charge_id)}",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
else:
|
||||
# Нет фотографий - возвращаем деньги
|
||||
service_result = {
|
||||
'status': 'no_data',
|
||||
'data_count': 0,
|
||||
'error': 'No photos found for VIN'
|
||||
}
|
||||
payment_data['refund_status'] = 'auto_refund'
|
||||
payment_data['refund_reason'] = 'No photos found'
|
||||
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
|
||||
logging.info(f"Payment logged for GetPhotos no data case - User: {message.from_user.id}, VIN: {vin}")
|
||||
|
||||
try:
|
||||
await message.bot.refund_star_payment(
|
||||
user_id=message.from_user.id,
|
||||
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
|
||||
)
|
||||
await message.answer(
|
||||
"❌ No photos found for this VIN in our database.\n"
|
||||
"💰 Your payment has been automatically refunded.\n"
|
||||
"Please try another VIN or contact support if you believe this is an error."
|
||||
)
|
||||
logging.info(f"Refund successful for user {message.from_user.id} - no photos found for VIN {vin}")
|
||||
except Exception as refund_error:
|
||||
logging.error(f"Failed to refund payment for user {message.from_user.id}: {refund_error}")
|
||||
await message.answer(
|
||||
"❌ No photos found for this VIN.\n"
|
||||
"⚠️ Please contact support with this transaction ID for a refund:\n"
|
||||
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting photos info for {vin}: {e}")
|
||||
|
||||
# Логируем ошибку
|
||||
service_result = {
|
||||
'status': 'error',
|
||||
'data_count': 0,
|
||||
'error': str(e)
|
||||
}
|
||||
payment_data['refund_status'] = 'auto_refund'
|
||||
payment_data['refund_reason'] = 'Service error'
|
||||
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
|
||||
logging.info(f"Payment logged for GetPhotos error case - User: {message.from_user.id}, VIN: {vin}")
|
||||
|
||||
# Возвращаем деньги при ошибке
|
||||
try:
|
||||
await message.bot.refund_star_payment(
|
||||
user_id=message.from_user.id,
|
||||
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
|
||||
)
|
||||
await message.answer(
|
||||
"❌ Error retrieving photos information from our database.\n"
|
||||
"💰 Your payment has been automatically refunded.\n"
|
||||
"Please try again later or contact support if the issue persists."
|
||||
)
|
||||
logging.info(f"Refund successful for user {message.from_user.id}, charge_id: {message.successful_payment.telegram_payment_charge_id}")
|
||||
except Exception as refund_error:
|
||||
logging.error(f"Failed to refund payment for user {message.from_user.id}: {refund_error}")
|
||||
await message.answer(
|
||||
"❌ Error retrieving photos information from our database.\n"
|
||||
"⚠️ Please contact support immediately with this transaction ID for a manual refund:\n"
|
||||
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
f"✅ Payment successful! Thank you for your purchase.\n"
|
||||
@ -892,8 +1576,67 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
||||
)
|
||||
|
||||
|
||||
@dp.message(VinStates.waiting_for_photo_vin)
|
||||
async def process_photo_vin(message: Message, state: FSMContext, db: OracleDatabase = None):
|
||||
# Используем переданный db или глобальный oracle_db
|
||||
database = db or oracle_db
|
||||
|
||||
# Сохраняем данные пользователя при обработке VIN
|
||||
await database.save_user(message.from_user, "photo_vin_processing")
|
||||
|
||||
vin = message.text.strip().upper()
|
||||
if len(vin) == 17 and vin.isalnum() and all(c not in vin for c in ["I", "O", "Q"]):
|
||||
try:
|
||||
# Получаем базовую информацию о VIN для заголовка
|
||||
make, model, year, cnt = await database.fetch_vin_info(vin)
|
||||
|
||||
# Получаем количество фотографий
|
||||
photo_count = await database.count_photo_records(vin)
|
||||
|
||||
logging.info(f"Photo search VIN: make: {make}, model: {model}, year: {year}, photo_count: {photo_count}")
|
||||
|
||||
# Формируем ответ в зависимости от наличия фотографий
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
if photo_count > 0:
|
||||
# Есть фотографии - показываем информацию и кнопку оплаты
|
||||
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
|
||||
response_text = f"📸 **Photo Information**\n\n"
|
||||
else:
|
||||
response_text = f"🚗 **{year} {make} {model}**\n\n"
|
||||
response_text += f"📸 **Photo Information**\n\n"
|
||||
|
||||
response_text += f"🖼️ **{photo_count} damage photos** found in our database for this vehicle.\n"
|
||||
response_text += f"These photos show the actual condition and damage of the vehicle during auction."
|
||||
|
||||
builder.button(text=f"Pay {IMG_PRICE} ⭐️ for photos", callback_data=f"pay_photos:{vin}", pay=True)
|
||||
builder.button(text="Try another VIN", callback_data="search_car_photo")
|
||||
builder.button(text="Back to Main Menu", callback_data="main_menu")
|
||||
builder.adjust(1, 1, 1) # Each button on separate row
|
||||
else:
|
||||
# Нет фотографий
|
||||
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
|
||||
response_text = f"❌ **Unable to decode VIN or no photos found**\n\n"
|
||||
else:
|
||||
response_text = f"🚗 **{year} {make} {model}**\n\n"
|
||||
|
||||
response_text += f"📸 **No damage photos found** for this VIN in our database."
|
||||
|
||||
builder.button(text="Try another VIN", callback_data="search_car_photo")
|
||||
builder.button(text="Back to Main Menu", callback_data="main_menu")
|
||||
builder.adjust(1, 1) # Each button on separate row
|
||||
|
||||
await message.answer(response_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Database error for photo search VIN {vin}: {e}")
|
||||
await message.answer("Error retrieving data from database. Please try again later.")
|
||||
await state.clear()
|
||||
else:
|
||||
await message.answer("Invalid VIN. Please enter a valid 17-character VIN (letters and numbers, no I, O, Q).")
|
||||
|
||||
|
||||
async def on_startup():
|
||||
log_system_info() # Логируем информацию о системе
|
||||
await oracle_db.connect()
|
||||
# Регистрируем middleware для всех типов событий
|
||||
dp.message.middleware(DbSessionMiddleware(oracle_db))
|
||||
|
||||
540
read_dev.md
Normal file
540
read_dev.md
Normal file
@ -0,0 +1,540 @@
|
||||
# SalvageDB Telegram Bot - Developer Documentation
|
||||
|
||||
## Оглавление
|
||||
1. [Архитектура бота](#архитектура-бота)
|
||||
2. [Переменные окружения](#переменные-окружения)
|
||||
3. [Состояния FSM](#состояния-fsm)
|
||||
4. [Основные функции](#основные-функции)
|
||||
5. [Callback Handlers](#callback-handlers)
|
||||
6. [Message Handlers](#message-handlers)
|
||||
7. [Функции базы данных](#функции-базы-данных)
|
||||
8. [Система платежей](#система-платежей)
|
||||
9. [Работа с фотографиями](#работа-с-фотографиями)
|
||||
10. [Системные функции](#системные-функции)
|
||||
11. [Middleware](#middleware)
|
||||
12. [Структура проекта](#структура-проекта)
|
||||
13. [API Endpoints](#api-endpoints)
|
||||
|
||||
---
|
||||
|
||||
## Архитектура бота
|
||||
|
||||
Бот построен на фреймворке `aiogram 3.x` с использованием Oracle Database для хранения данных о транспортных средствах и их истории повреждений.
|
||||
|
||||
### Основные компоненты:
|
||||
- **main.py** - основной файл с логикой бота
|
||||
- **db.py** - класс для работы с Oracle Database
|
||||
- **middlewares/db.py** - middleware для передачи подключения к БД
|
||||
|
||||
### Схема работы:
|
||||
```
|
||||
User Input → FSM States → Database Query → Response Formatting → Telegram API
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
### Обязательные переменные:
|
||||
```env
|
||||
BOT_TOKEN=ваш_токен_бота
|
||||
BOT_NAME=имя_бота
|
||||
db_user=пользователь_oracle
|
||||
db_password=пароль_oracle
|
||||
db_dsn=строка_подключения_oracle
|
||||
ADMIN_USER_ID=id_администратора
|
||||
```
|
||||
|
||||
### Опциональные переменные:
|
||||
```env
|
||||
DEBUG=1 # Включает детальное логирование
|
||||
DECODE_PRICE=1 # Цена за детальную информацию VIN (звезды)
|
||||
CHECK_PRICE=10 # Цена за проверку salvage записей (звезды)
|
||||
IMG_PRICE=100 # Цена за фотографии (звезды)
|
||||
```
|
||||
|
||||
### Автоматические пути к изображениям:
|
||||
```python
|
||||
if is_windows():
|
||||
image_path = "D:\\SALVAGEDB\\salvagedb_bot\\images"
|
||||
else:
|
||||
image_path = "/images/"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Состояния FSM
|
||||
|
||||
Бот использует Finite State Machine для управления диалогами:
|
||||
|
||||
```python
|
||||
class VinStates(StatesGroup):
|
||||
waiting_for_vin = State() # Ожидание VIN для декодирования
|
||||
waiting_for_check_vin = State() # Ожидание VIN для проверки salvage
|
||||
waiting_for_photo_vin = State() # Ожидание VIN для поиска фотографий
|
||||
```
|
||||
|
||||
### Переходы между состояниями:
|
||||
- **Start** → `waiting_for_vin` (кнопка "Decode VIN")
|
||||
- **Start** → `waiting_for_check_vin` (кнопка "Check VIN")
|
||||
- **Start** → `waiting_for_photo_vin` (кнопка "Search car Photo")
|
||||
- **Any State** → **Start** (кнопка "Back to Main Menu")
|
||||
|
||||
---
|
||||
|
||||
## Основные функции
|
||||
|
||||
### Системные функции
|
||||
|
||||
#### `get_operating_system() -> str`
|
||||
Определяет операционную систему.
|
||||
```python
|
||||
Returns: 'Windows', 'Linux', 'macOS' или 'Unknown'
|
||||
```
|
||||
|
||||
#### `is_windows() -> bool`, `is_linux() -> bool`, `is_macos() -> bool`
|
||||
Быстрые проверки операционной системы.
|
||||
|
||||
#### `log_system_info()`
|
||||
Логирует подробную информацию о системе при запуске.
|
||||
|
||||
### Функции обработки данных
|
||||
|
||||
#### `get_us_state_name(state_code: str) -> str`
|
||||
Конвертирует двухбуквенный код штата США в полное название.
|
||||
```python
|
||||
get_us_state_name("TX") # Returns: "Texas"
|
||||
```
|
||||
|
||||
#### `format_sale_date(date_str: str) -> str`
|
||||
Форматирует дату из MM/YYYY в читаемый формат.
|
||||
```python
|
||||
format_sale_date("3/2023") # Returns: "March 2023"
|
||||
```
|
||||
|
||||
#### `parse_location(location_str: str) -> str`
|
||||
Парсит локацию из формата ST/TOWN в "City, State".
|
||||
```python
|
||||
parse_location("TX/DALLAS") # Returns: "Dallas, Texas"
|
||||
```
|
||||
|
||||
#### `escape_markdown(text: str) -> str`
|
||||
Экранирует специальные символы Markdown для безопасной отправки.
|
||||
|
||||
### Функции работы с фотографиями
|
||||
|
||||
#### `convert_photo_path(db_path: str) -> str`
|
||||
Конвертирует путь из БД в полный путь с учетом OS.
|
||||
```python
|
||||
# Windows
|
||||
convert_photo_path("20250530/vin/photo.jpg")
|
||||
# Returns: "D:\SALVAGEDB\salvagedb_bot\images\20250530\vin\photo.jpg"
|
||||
|
||||
# Linux
|
||||
convert_photo_path("20250530/vin/photo.jpg")
|
||||
# Returns: "/images/20250530/vin/photo.jpg"
|
||||
```
|
||||
|
||||
#### `prepare_photo_paths(db_paths: list) -> list`
|
||||
Подготавливает список полных путей к фотографиям.
|
||||
|
||||
#### `send_vehicle_photos(message, vin, photo_paths, make, model, year)`
|
||||
Отправляет фотографии пользователю группами по 10 штук.
|
||||
|
||||
**Особенности:**
|
||||
- Разбивает фотографии на группы по 10 (лимит Telegram)
|
||||
- Добавляет описания к первым фото каждой группы
|
||||
- Пауза 0.5 сек между группами
|
||||
- Обработка ошибок для каждой фотографии
|
||||
- Итоговое сообщение с количеством отправленных фото
|
||||
|
||||
---
|
||||
|
||||
## Callback Handlers
|
||||
|
||||
### `@dp.callback_query(lambda c: c.data == "decode_vin")`
|
||||
**Функция:** `decode_vin_callback`
|
||||
**Действие:** Устанавливает состояние `waiting_for_vin`, запрашивает VIN для декодирования.
|
||||
|
||||
### `@dp.callback_query(lambda c: c.data == "check_vin")`
|
||||
**Функция:** `check_vin_callback`
|
||||
**Действие:** Устанавливает состояние `waiting_for_check_vin`, запрашивает VIN для проверки salvage.
|
||||
|
||||
### `@dp.callback_query(lambda c: c.data == "search_car_photo")`
|
||||
**Функция:** `search_car_photo_callback`
|
||||
**Действие:** Устанавливает состояние `waiting_for_photo_vin`, запрашивает VIN для поиска фотографий.
|
||||
|
||||
### `@dp.callback_query(lambda c: c.data == "main_menu")`
|
||||
**Функция:** `main_menu_callback`
|
||||
**Действие:** Очищает состояние, возвращает в главное меню.
|
||||
|
||||
### Платежные handlers
|
||||
|
||||
#### `@dp.callback_query(lambda c: c.data.startswith("pay_detailed_info:"))`
|
||||
**Функция:** `pay_detailed_info_callback`
|
||||
**Payload:** `detailed_vin_info:{vin}`
|
||||
**Цена:** `DECODE_PRICE` звезд
|
||||
|
||||
#### `@dp.callback_query(lambda c: c.data.startswith("pay_check_detailed:"))`
|
||||
**Функция:** `pay_check_detailed_callback`
|
||||
**Payload:** `detailed_salvage_check:{vin}`
|
||||
**Цена:** `CHECK_PRICE` звезд
|
||||
|
||||
#### `@dp.callback_query(lambda c: c.data.startswith("pay_photos:"))`
|
||||
**Функция:** `pay_photos_callback`
|
||||
**Payload:** `vehicle_photos:{vin}`
|
||||
**Цена:** `IMG_PRICE` звезд
|
||||
|
||||
---
|
||||
|
||||
## Message Handlers
|
||||
|
||||
### `@dp.message(Command("start"))`
|
||||
**Функция:** `command_start_handler`
|
||||
**Действие:** Показывает приветственное сообщение и главное меню.
|
||||
|
||||
**Кнопки:**
|
||||
- Decode VIN
|
||||
- Check VIN
|
||||
- Search car Photo
|
||||
- Help
|
||||
- Prices
|
||||
- Go Salvagedb.com
|
||||
|
||||
### `@dp.message(Command("admin_stats"))`
|
||||
**Функция:** `admin_stats_handler`
|
||||
**Доступ:** Только для `ADMIN_USER_ID`
|
||||
**Действие:** Показывает статистику пользователей бота.
|
||||
|
||||
### Обработчики состояний
|
||||
|
||||
#### `@dp.message(VinStates.waiting_for_vin)`
|
||||
**Функция:** `process_vin`
|
||||
**Логика:**
|
||||
1. Валидация VIN (17 символов, без I/O/Q)
|
||||
2. Запрос `fetch_vin_info(vin)`
|
||||
3. Проверка успешности декодирования (не все UNKNOWN)
|
||||
4. Показ кнопки детальной информации при `cnt > 9`
|
||||
|
||||
#### `@dp.message(VinStates.waiting_for_check_vin)`
|
||||
**Функция:** `process_check_vin`
|
||||
**Логика:**
|
||||
1. Валидация VIN
|
||||
2. Запрос `fetch_vin_info(vin)` и `count_salvage_records(vin)`
|
||||
3. Показ информации о количестве найденных записей
|
||||
4. Кнопка оплаты при наличии записей
|
||||
|
||||
#### `@dp.message(VinStates.waiting_for_photo_vin)`
|
||||
**Функция:** `process_photo_vin`
|
||||
**Логика:**
|
||||
1. Валидация VIN
|
||||
2. Запрос `fetch_vin_info(vin)` и `count_photo_records(vin)`
|
||||
3. Показ информации о количестве фотографий
|
||||
4. Кнопка оплаты при наличии фотографий
|
||||
|
||||
### `@dp.message(lambda message: message.successful_payment)`
|
||||
**Функция:** `successful_payment_handler`
|
||||
**Логика обработки платежей по payload:**
|
||||
|
||||
#### `detailed_vin_info:{vin}`
|
||||
1. Запрос `fetch_detailed_vin_info(vin)`
|
||||
2. Форматирование детального отчета по категориям
|
||||
3. Возврат денег при отсутствии данных
|
||||
4. Автоматический возврат админу
|
||||
|
||||
#### `detailed_salvage_check:{vin}`
|
||||
1. Запрос `fetch_salvage_detailed_info(vin)`
|
||||
2. Форматирование salvage отчета с повреждениями
|
||||
3. Отдельное сообщение о фотографиях
|
||||
4. Возврат денег при отсутствии данных
|
||||
5. Автоматический возврат админу
|
||||
|
||||
#### `vehicle_photos:{vin}`
|
||||
1. Запрос `fetch_photo_paths(vin)`
|
||||
2. Конвертация путей под текущую OS
|
||||
3. Отправка фотографий группами по 10
|
||||
4. Возврат денег при отсутствии фотографий
|
||||
5. Автоматический возврат админу
|
||||
|
||||
---
|
||||
|
||||
## Функции базы данных
|
||||
|
||||
### Класс `OracleDatabase`
|
||||
|
||||
#### `__init__(user, password, dsn)`
|
||||
Инициализация подключения к Oracle DB.
|
||||
|
||||
#### `async def connect()`
|
||||
Создает пул подключений (min=1, max=4).
|
||||
|
||||
#### `async def fetch_vin_info(vin: str) -> Tuple[str, str, str, int]`
|
||||
**Возвращает:** `(make, model, year, nhtsa_records_count)`
|
||||
**SQL запрос:**
|
||||
```sql
|
||||
select 'None',
|
||||
COALESCE((select value from salvagedb.m_JSONS_FROM_NHTSA v3
|
||||
where v3.svin = s.svin and v3.variableid = '26'),
|
||||
(select val from salvagedb.vind2
|
||||
where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2)
|
||||
and varb = 'Make'),'UNKNOWN') make,
|
||||
-- аналогично для model и year
|
||||
(select count(*) from salvagedb.m_JSONS_FROM_NHTSA v3
|
||||
where v3.svin = s.svin) cnt
|
||||
from (select substr(:vin,1,10) svin, :vin vin from dual) s
|
||||
```
|
||||
|
||||
#### `async def count_salvage_records(vin: str) -> int`
|
||||
**SQL:** `SELECT COUNT(*) FROM salvagedb.salvagedb WHERE vin = :vin and svin = substr(:vin,1,10)`
|
||||
|
||||
#### `async def count_photo_records(vin: str) -> int`
|
||||
**SQL:** `SELECT COUNT(*) FROM salvagedb.salvage_images WHERE vin = :vin AND fn = 1`
|
||||
|
||||
#### `async def fetch_photo_paths(vin: str) -> list`
|
||||
**SQL:** `SELECT ipath FROM salvagedb.salvage_images WHERE fn = 1 AND vin = :vin`
|
||||
**Возвращает:** Список путей к фотографиям.
|
||||
|
||||
#### `async def fetch_salvage_detailed_info(vin: str) -> list`
|
||||
**Возвращает:** Детальную информацию о salvage записях.
|
||||
**SQL запрос:**
|
||||
```sql
|
||||
SELECT
|
||||
odo, odos, dem1, dem2,
|
||||
month||'/'||year as sale_date,
|
||||
JSON_VALUE(jdata, '$.RepCost') AS j_rep_cost,
|
||||
JSON_VALUE(jdata, '$.Runs_Drive') AS j_runs_drive,
|
||||
JSON_VALUE(jdata, '$.Locate') AS j_locate,
|
||||
(select count(*) from salvagedb.salvage_images si
|
||||
where si.vin = s.vin and fn = 1) img_count
|
||||
FROM salvagedb.salvagedb s
|
||||
LEFT JOIN salvagedb.addinfo i ON s.num = i.numid
|
||||
WHERE vin = :vin AND svin = substr(:vin, 1, 10)
|
||||
ORDER BY year DESC, month DESC
|
||||
```
|
||||
|
||||
#### `async def fetch_detailed_vin_info(vin: str) -> dict`
|
||||
**Возвращает:** Детальную информацию о VIN по категориям.
|
||||
**Категории:**
|
||||
- basic_characteristics
|
||||
- engine_and_powertrain
|
||||
- transmission
|
||||
- active_safety
|
||||
- passive_safety
|
||||
- dimensions_and_construction
|
||||
- brake_system
|
||||
- lighting
|
||||
- additional_features
|
||||
- manufacturing_and_localization
|
||||
- ncsa_data
|
||||
- technical_information_and_errors
|
||||
|
||||
#### Пользовательские функции
|
||||
|
||||
#### `async def save_user(user: User, interaction_source: str) -> bool`
|
||||
Сохраняет/обновляет данные пользователя при каждом взаимодействии.
|
||||
|
||||
#### `async def update_user_payment(user_id: int, payment_amount: float) -> bool`
|
||||
Обновляет статистику платежей пользователя.
|
||||
|
||||
#### `async def get_user_stats(user_id: int) -> dict`
|
||||
Возвращает статистику конкретного пользователя.
|
||||
|
||||
#### `async def get_users_summary() -> dict`
|
||||
Возвращает общую статистику по всем пользователям.
|
||||
|
||||
---
|
||||
|
||||
## Система платежей
|
||||
|
||||
### Telegram Stars
|
||||
Бот использует Telegram Stars для приема платежей.
|
||||
|
||||
### Типы платежей:
|
||||
1. **Детальная информация VIN** - `DECODE_PRICE` звезд
|
||||
2. **Salvage проверка** - `CHECK_PRICE` звезд
|
||||
3. **Фотографии автомобиля** - `IMG_PRICE` звезд
|
||||
|
||||
### Процесс оплаты:
|
||||
1. Пользователь нажимает кнопку оплаты
|
||||
2. Создается invoice с уникальным payload
|
||||
3. Pre-checkout подтверждение
|
||||
4. Successful payment обработка
|
||||
5. Предоставление услуги или возврат средств
|
||||
|
||||
### Автоматический возврат админу:
|
||||
Если `message.from_user.id == ADMIN_USER_ID`, после предоставления услуги автоматически возвращаются потраченные звезды.
|
||||
|
||||
### Обработка ошибок:
|
||||
- Отсутствие данных → автоматический возврат
|
||||
- Ошибка сервера → автоматический возврат
|
||||
- Ошибка возврата → уведомление с transaction ID
|
||||
|
||||
---
|
||||
|
||||
## Работа с фотографиями
|
||||
|
||||
### Структура хранения:
|
||||
```
|
||||
База данных: 20250530/1c4hjxdg0pw697757/photo.jpg
|
||||
Windows: D:\SALVAGEDB\salvagedb_bot\images\20250530\1c4hjxdg0pw697757\photo.jpg
|
||||
Linux: /images/20250530/1c4hjxdg0pw697757/photo.jpg
|
||||
```
|
||||
|
||||
### Процесс отправки:
|
||||
1. Получение путей из БД
|
||||
2. Конвертация под текущую OS
|
||||
3. Разбивка на группы по 10 фотографий
|
||||
4. Отправка media groups с описаниями
|
||||
5. Итоговое сообщение
|
||||
|
||||
### Ограничения:
|
||||
- Максимум 10 фотографий в одной группе (Telegram API)
|
||||
- Пауза 0.5 сек между группами
|
||||
- Проверка существования файлов
|
||||
|
||||
---
|
||||
|
||||
## Системные функции
|
||||
|
||||
### Логирование:
|
||||
```python
|
||||
# Основное логирование
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
|
||||
# Debug режим
|
||||
if getenv("DEBUG",'0') == '1':
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
```
|
||||
|
||||
### Middleware:
|
||||
```python
|
||||
# Передача подключения к БД во все handlers
|
||||
dp.message.middleware(DbSessionMiddleware(oracle_db))
|
||||
dp.callback_query.middleware(DbSessionMiddleware(oracle_db))
|
||||
dp.pre_checkout_query.middleware(DbSessionMiddleware(oracle_db))
|
||||
```
|
||||
|
||||
### Startup/Shutdown:
|
||||
```python
|
||||
async def on_startup():
|
||||
log_system_info() # Системная информация
|
||||
await oracle_db.connect() # Подключение к БД
|
||||
# Регистрация middleware
|
||||
|
||||
async def on_shutdown():
|
||||
await oracle_db.close() # Закрытие пула подключений
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Middleware
|
||||
|
||||
### `DbSessionMiddleware`
|
||||
**Файл:** `middlewares/db.py`
|
||||
**Назначение:** Передает экземпляр подключения к БД во все handlers.
|
||||
|
||||
**Использование:**
|
||||
```python
|
||||
async def handler(message: Message, db: OracleDatabase = None):
|
||||
database = db or oracle_db # Fallback на глобальный
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
salvagedb_bot/
|
||||
├── main.py # Основной файл бота
|
||||
├── db.py # Класс работы с БД
|
||||
├── read_dev.md # Документация (этот файл)
|
||||
├── middlewares/
|
||||
│ └── db.py # Middleware для БД
|
||||
└── images/ # Директория с фотографиями (Windows)
|
||||
└── 20250530/
|
||||
└── vin_lower/
|
||||
└── photo.jpg
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Telegram Bot API
|
||||
Бот использует стандартные методы Telegram Bot API:
|
||||
|
||||
#### Отправка сообщений:
|
||||
- `message.answer()` - текстовые сообщения
|
||||
- `message.answer_media_group()` - группы фотографий
|
||||
|
||||
#### Платежи:
|
||||
- `bot.send_invoice()` - создание счета
|
||||
- `bot.refund_star_payment()` - возврат звезд
|
||||
|
||||
#### Webhook vs Polling:
|
||||
Текущая конфигурация использует polling:
|
||||
```python
|
||||
await dp.start_polling(bot)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### Запуск бота:
|
||||
```bash
|
||||
# Установка переменных окружения
|
||||
export BOT_TOKEN="your_token"
|
||||
export db_user="oracle_user"
|
||||
# ... другие переменные
|
||||
|
||||
# Запуск
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Тестирование платежей:
|
||||
1. Установить `ADMIN_USER_ID` на свой Telegram ID
|
||||
2. Выполнить любую оплату
|
||||
3. Получить услугу + автоматический возврат звезд
|
||||
|
||||
### Debug режим:
|
||||
```bash
|
||||
export DEBUG=1
|
||||
python main.py
|
||||
```
|
||||
Включает детальное логирование всех операций.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Проблемы с фотографиями:
|
||||
1. Проверить права доступа к директории images
|
||||
2. Убедиться в правильности путей для текущей OS
|
||||
3. Проверить логи на ошибки файловой системы
|
||||
|
||||
### Проблемы с БД:
|
||||
1. Проверить строку подключения DSN
|
||||
2. Убедиться в доступности Oracle DB
|
||||
3. Проверить права пользователя БД
|
||||
|
||||
### Проблемы с платежами:
|
||||
1. Убедиться в корректности BOT_TOKEN
|
||||
2. Проверить что бот добавлен в Telegram Stars
|
||||
3. Проверить логи на ошибки Telegram API
|
||||
|
||||
---
|
||||
|
||||
## Обновления и изменения
|
||||
|
||||
При внесении изменений в код обязательно обновляйте эту документацию.
|
||||
|
||||
### История изменений:
|
||||
- **v1.0** - Базовая функциональность (decode VIN, check VIN)
|
||||
- **v1.1** - Добавлена поддержка фотографий
|
||||
- **v1.2** - Улучшена отправка множественных фотографий
|
||||
- **v1.3** - Добавлены функции определения OS и автоматические пути
|
||||
|
||||
---
|
||||
|
||||
*Документация актуальна на момент последнего обновления кода.*
|
||||
20
requirements.txt
Normal file
20
requirements.txt
Normal file
@ -0,0 +1,20 @@
|
||||
# Telegram Bot Framework
|
||||
aiogram==3.13.1
|
||||
|
||||
# Oracle Database
|
||||
oracledb==2.5.0
|
||||
|
||||
# Async HTTP client (зависимость aiogram)
|
||||
aiohttp==3.10.11
|
||||
|
||||
# Дополнительные зависимости для работы с данными
|
||||
typing-extensions==4.12.2
|
||||
|
||||
# Для работы с датами и временем
|
||||
python-dateutil==2.9.0
|
||||
|
||||
# Для логирования (встроенно в Python, но для совместимости)
|
||||
# logging - встроенно
|
||||
|
||||
# Для работы с переменными окружения
|
||||
python-dotenv==1.0.1
|
||||
BIN
salvagebot
Normal file
BIN
salvagebot
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user