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
|
import asyncio
|
||||||
return await asyncio.to_thread(_query)
|
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:
|
async def fetch_detailed_vin_info(self, vin: str) -> dict:
|
||||||
# Manual async wrapper since oracledb is synchronous (threaded)
|
# Manual async wrapper since oracledb is synchronous (threaded)
|
||||||
def _query():
|
def _query():
|
||||||
@ -378,3 +391,84 @@ class OracleDatabase:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error getting users summary: {e}")
|
print(f"Error getting users summary: {e}")
|
||||||
return {}
|
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
|
from datetime import datetime
|
||||||
import platform
|
import platform
|
||||||
import os
|
import os
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher
|
from aiogram import Bot, Dispatcher
|
||||||
from aiogram.filters import Command
|
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.utils.keyboard import InlineKeyboardBuilder
|
||||||
from aiogram.fsm.state import State, StatesGroup
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
@ -15,6 +16,80 @@ from db import OracleDatabase
|
|||||||
from middlewares.db import DbSessionMiddleware
|
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:
|
def get_us_state_name(state_code: str) -> str:
|
||||||
"""
|
"""
|
||||||
Конвертирует двухбуквенный код штата США в полное название
|
Конвертирует двухбуквенный код штата США в полное название
|
||||||
@ -148,13 +223,193 @@ def is_macos() -> bool:
|
|||||||
return get_operating_system() == 'macOS'
|
return get_operating_system() == 'macOS'
|
||||||
|
|
||||||
|
|
||||||
if getenv("DEBUG",'0') == '1':
|
def convert_photo_path(db_path: str) -> str:
|
||||||
logging.basicConfig(level=logging.INFO)
|
"""
|
||||||
else:
|
Конвертирует путь к фотографии в зависимости от операционной системы
|
||||||
logging.basicConfig(level=logging.WARNING)
|
Args:
|
||||||
|
db_path: путь из базы данных в Linux формате (с /)
|
||||||
|
Returns:
|
||||||
|
str: полный путь к файлу с учетом OS и базового пути
|
||||||
|
"""
|
||||||
|
if not db_path:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Убираем лишние пробелы из пути
|
||||||
|
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
|
||||||
|
|
||||||
# Временно включаем детальное логирование для отладки
|
|
||||||
logging.getLogger().setLevel(logging.INFO)
|
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")
|
TOKEN = getenv("BOT_TOKEN")
|
||||||
@ -167,13 +422,13 @@ IMG_PRICE = getenv("IMG_PRICE",100)
|
|||||||
if is_windows():
|
if is_windows():
|
||||||
image_path = "D:\\SALVAGEDB\\salvagedb_bot\\images"
|
image_path = "D:\\SALVAGEDB\\salvagedb_bot\\images"
|
||||||
else:
|
else:
|
||||||
image_path = "/images/"
|
image_path = "/images"
|
||||||
|
|
||||||
|
|
||||||
oracle_db = OracleDatabase(
|
oracle_db = OracleDatabase(
|
||||||
user= getenv("db_user"),
|
user= getenv("DB_USER"),
|
||||||
password= getenv("db_password"),
|
password= getenv("DB_PASSWORD"),
|
||||||
dsn= getenv("db_dsn")
|
dsn= getenv("DB_DSN")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -182,6 +437,7 @@ dp = Dispatcher()
|
|||||||
class VinStates(StatesGroup):
|
class VinStates(StatesGroup):
|
||||||
waiting_for_vin = State()
|
waiting_for_vin = State()
|
||||||
waiting_for_check_vin = State()
|
waiting_for_check_vin = State()
|
||||||
|
waiting_for_photo_vin = State()
|
||||||
|
|
||||||
|
|
||||||
# Command handler
|
# Command handler
|
||||||
@ -240,6 +496,19 @@ async def check_vin_callback(callback: CallbackQuery, state: FSMContext, db: Ora
|
|||||||
await callback.answer()
|
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")
|
@dp.callback_query(lambda c: c.data == "main_menu")
|
||||||
async def main_menu_callback(callback: CallbackQuery, state: FSMContext, db: OracleDatabase = None):
|
async def main_menu_callback(callback: CallbackQuery, state: FSMContext, db: OracleDatabase = None):
|
||||||
# Используем переданный db или глобальный oracle_db
|
# Используем переданный db или глобальный oracle_db
|
||||||
@ -253,6 +522,109 @@ async def main_menu_callback(callback: CallbackQuery, state: FSMContext, db: Ora
|
|||||||
await callback.answer()
|
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:"))
|
@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):
|
async def pay_detailed_info_callback(callback: CallbackQuery, db: OracleDatabase = None):
|
||||||
# Используем переданный db или глобальный oracle_db
|
# Используем переданный db или глобальный oracle_db
|
||||||
@ -263,12 +635,12 @@ async def pay_detailed_info_callback(callback: CallbackQuery, db: OracleDatabase
|
|||||||
|
|
||||||
# Extract VIN from callback data
|
# Extract VIN from callback data
|
||||||
vin = callback.data.split(":")[1]
|
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}")
|
logging.info(f"Sending invoice for VIN: {vin}")
|
||||||
await callback.bot.send_invoice(
|
await callback.bot.send_invoice(
|
||||||
chat_id=callback.message.chat.id,
|
chat_id=callback.message.chat.id,
|
||||||
title="Detailed VIN Information",
|
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
|
payload=f"detailed_vin_info:{vin}", # Include VIN in payload
|
||||||
provider_token="", # Empty for Telegram Stars
|
provider_token="", # Empty for Telegram Stars
|
||||||
currency="XTR", # Telegram Stars currency
|
currency="XTR", # Telegram Stars currency
|
||||||
@ -364,7 +736,7 @@ async def process_check_vin(message: Message, state: FSMContext, db: OracleDatab
|
|||||||
|
|
||||||
if salvage_count > 0:
|
if salvage_count > 0:
|
||||||
# Есть записи - показываем кнопки: Pay 10⭐️ for detailed info, Try another VIN, Back to main menu
|
# Есть записи - показываем кнопки: 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="Try another VIN", callback_data="check_vin")
|
||||||
builder.button(text="Back to Main Menu", callback_data="main_menu")
|
builder.button(text="Back to Main Menu", callback_data="main_menu")
|
||||||
builder.adjust(1, 1, 1) # Each button on separate row
|
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
|
||||||
vin = callback.data.split(":")[1]
|
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}")
|
logging.info(f"Sending invoice for salvage check VIN: {vin}")
|
||||||
|
|
||||||
await callback.bot.send_invoice(
|
await callback.bot.send_invoice(
|
||||||
chat_id=callback.message.chat.id,
|
chat_id=callback.message.chat.id,
|
||||||
title="Detailed Salvage Report",
|
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 для этого типа платежа
|
payload=f"detailed_salvage_check:{vin}", # Уникальный payload для этого типа платежа
|
||||||
provider_token="", # Empty for Telegram Stars
|
provider_token="", # Empty for Telegram Stars
|
||||||
currency="XTR", # Telegram Stars currency
|
currency="XTR", # Telegram Stars currency
|
||||||
@ -412,6 +784,31 @@ async def pay_check_detailed_callback(callback: CallbackQuery, db: OracleDatabas
|
|||||||
await callback.answer()
|
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()
|
@dp.pre_checkout_query()
|
||||||
async def pre_checkout_handler(pre_checkout_query: PreCheckoutQuery, db: OracleDatabase = None):
|
async def pre_checkout_handler(pre_checkout_query: PreCheckoutQuery, db: OracleDatabase = None):
|
||||||
# Используем переданный db или глобальный oracle_db
|
# Используем переданный db или глобальный oracle_db
|
||||||
@ -475,9 +872,24 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
|||||||
payload = message.successful_payment.invoice_payload
|
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)
|
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:"):
|
if payload.startswith("detailed_vin_info:"):
|
||||||
vin = payload.split(":")[1]
|
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())
|
await message.answer(plain_report, reply_markup=builder.as_markup())
|
||||||
logging.info("Plain text message sent successfully!")
|
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:
|
if message.from_user.id == ADMIN_USER_ID:
|
||||||
try:
|
try:
|
||||||
@ -611,6 +1034,7 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
|||||||
user_id=message.from_user.id,
|
user_id=message.from_user.id,
|
||||||
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
|
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
|
||||||
)
|
)
|
||||||
|
payment_data['refund_status'] = 'admin_refund'
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"🔧 **Admin Refund**\n\n"
|
"🔧 **Admin Refund**\n\n"
|
||||||
f"💰 Payment automatically refunded for admin user.\n"
|
f"💰 Payment automatically refunded for admin user.\n"
|
||||||
@ -629,6 +1053,16 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# No detailed information found - refund the payment
|
# 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:
|
try:
|
||||||
await message.bot.refund_star_payment(
|
await message.bot.refund_star_payment(
|
||||||
user_id=message.from_user.id,
|
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"
|
"⚠️ Please contact support with this transaction ID for a refund:\n"
|
||||||
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
|
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting detailed VIN info for {vin}: {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
|
# Attempt to refund the payment due to service error
|
||||||
try:
|
try:
|
||||||
await message.bot.refund_star_payment(
|
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())
|
await message.answer(plain_report, reply_markup=builder.as_markup())
|
||||||
logging.info("Plain text message sent successfully!")
|
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:
|
if salvage_records and salvage_records[0]['img_count'] > 0:
|
||||||
img_count = salvage_records[0]['img_count']
|
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,
|
user_id=message.from_user.id,
|
||||||
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
|
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
|
||||||
)
|
)
|
||||||
|
payment_data['refund_status'] = 'admin_refund'
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"🔧 **Admin Refund**\n\n"
|
"🔧 **Admin Refund**\n\n"
|
||||||
f"💰 Payment automatically refunded for admin user.\n"
|
f"💰 Payment automatically refunded for admin user.\n"
|
||||||
@ -820,6 +1276,16 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
|||||||
)
|
)
|
||||||
else:
|
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:
|
try:
|
||||||
await message.bot.refund_star_payment(
|
await message.bot.refund_star_payment(
|
||||||
user_id=message.from_user.id,
|
user_id=message.from_user.id,
|
||||||
@ -842,6 +1308,17 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting salvage info for {vin}: {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:
|
try:
|
||||||
await message.bot.refund_star_payment(
|
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"
|
"⚠️ Please contact support immediately with this transaction ID for a manual refund:\n"
|
||||||
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
|
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:
|
else:
|
||||||
await message.answer(
|
await message.answer(
|
||||||
f"✅ Payment successful! Thank you for your purchase.\n"
|
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():
|
async def on_startup():
|
||||||
log_system_info() # Логируем информацию о системе
|
|
||||||
await oracle_db.connect()
|
await oracle_db.connect()
|
||||||
# Регистрируем middleware для всех типов событий
|
# Регистрируем middleware для всех типов событий
|
||||||
dp.message.middleware(DbSessionMiddleware(oracle_db))
|
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