Compare commits

...

10 Commits

Author SHA1 Message Date
e553b9584c Обновлен Dockerfile для создания пользователя salvagebot с UID 1027 и добавления в группу users. Исправлены права доступа для рабочих директорий и файлов, что улучшает безопасность и совместимость. В main.py добавлено логирование информации о текущем пользователе и файлах, что упрощает отладку. Эти изменения повышают безопасность и информативность приложения. 2025-06-06 08:37:50 +03:00
df0eb4536a Обновлен файл docker-compose.yml для использования образа salvagebot и изменения переменных окружения на значения по умолчанию. Изменены пути для логов и изображений. Обновлен Dockerfile для создания рабочей директории с правильными правами. В main.py исправлены ссылки на переменные окружения. Эти изменения упрощают настройку и улучшают структуру проекта. 2025-06-06 00:15:56 +03:00
3e1a090fc8 Обновлен файл docker-compose.yml с заменой переменных окружения на значения по умолчанию для упрощения настройки. Исправлены ссылки на переменные в Makefile для соответствия новым именам. Обновлена документация в README_DOCKER.md для отражения изменений в конфигурации и обязательных значений. Эти изменения улучшают удобство настройки и документацию проекта. 2025-06-04 08:07:29 +03:00
c0a8cf1334 Добавлено логирование информации о запуске приложения в Docker-контейнере в функции настройки логирования. Теперь выводится информация о временной зоне и пользователе контейнера, что улучшает отслеживание работы приложения в различных средах. 2025-06-04 08:02:53 +03:00
98ba1cb3c1 Добавлена функция настройки логирования с ротацией файлов в main.py. Реализована логика создания каталога для логов, определения уровня логирования и добавления обработчиков для консольного и файлового вывода. Эти изменения улучшают отслеживание событий и упрощают отладку приложения. 2025-06-04 07:46:31 +03:00
a25c39e9cd Добавлено логирование операций оплаты в классе OracleDatabase и обновлены обработчики в main.py для сохранения информации о платежах. Реализована логика логирования успешных и неудачных операций для услуг DecodeVin, CheckSalvage и GetPhotos. Эти изменения улучшают отслеживание платежей и помогают в отладке. 2025-06-03 00:38:29 +03:00
f43580e1b0 Добавлен новый обработчик для отображения цен на услуги в main.py. Реализована логика сохранения данных пользователя при просмотре цен и формирование сообщения с информацией о ценах на услуги. Эти изменения улучшают информативность и взаимодействие с пользователем. 2025-06-03 00:05:46 +03:00
f2fe946244 Добавлена функция справки в main.py, которая предоставляет пользователям информацию о доступных услугах и ценах. Обновлены обработчики для динамического отображения цен на услуги. Эти изменения улучшают взаимодействие с пользователем и делают информацию более доступной. 2025-06-02 23:46:07 +03:00
fe0aaefbec Обновлены обработчики в main.py для улучшения логики обработки запросов на получение информации о повреждениях. Добавлены новые функции для работы с данными о повреждениях, включая оптимизацию SQL-запросов и улучшение форматирования выводимой информации. Эти изменения повышают точность и удобство использования бота. 2025-06-02 00:48:40 +03:00
5f3d478adb Добавлены функции для получения путей к фотографиям и подсчета их количества по VIN в классе OracleDatabase. Обновлены обработчики в main.py для обработки запросов на получение фотографий, включая логику оплаты и отправки изображений пользователю. Эти изменения улучшают функциональность бота и позволяют пользователям получать доступ к фотографиям повреждений автомобилей. 2025-06-02 00:37:53 +03:00
16 changed files with 2399 additions and 19 deletions

91
.dockerignore Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -127,6 +127,19 @@ class OracleDatabase:
import asyncio
return await asyncio.to_thread(_query)
async def fetch_photo_paths(self, vin: str) -> list:
"""
Получает список путей к фотографиям для данного VIN
"""
def _query():
with self._pool.acquire() as conn:
with conn.cursor() as cur:
cur.execute("SELECT ipath FROM salvagedb.salvage_images WHERE fn = 1 AND vin = :vin", {"vin": vin})
results = cur.fetchall()
return [row[0] for row in results if row[0]] if results else []
import asyncio
return await asyncio.to_thread(_query)
async def fetch_detailed_vin_info(self, vin: str) -> dict:
# Manual async wrapper since oracledb is synchronous (threaded)
def _query():
@ -378,3 +391,84 @@ class OracleDatabase:
except Exception as e:
print(f"Error getting users summary: {e}")
return {}
async def count_photo_records(self, vin: str) -> int:
"""
Подсчитывает количество фотографий для данного VIN
"""
def _query():
with self._pool.acquire() as conn:
with conn.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM salvagedb.salvage_images WHERE vin = :vin AND fn = 1", {"vin": vin})
result = cur.fetchone()
return result[0] if result else 0
import asyncio
return await asyncio.to_thread(_query)
async def save_payment_log(self, user: User, service_type: str, vin: str, payment_data: dict, service_result: dict = None) -> bool:
"""
Логирует операцию оплаты с полной информацией о пользователе, услуге и результате
Args:
user: Объект пользователя Telegram
service_type: Тип услуги ('decode_vin', 'check_salvage', 'get_photos')
vin: VIN номер автомобиля
payment_data: Данные о платеже (сумма, transaction_id, статус)
service_result: Результат предоставления услуги (количество данных, статус)
"""
def _save_log():
with self._pool.acquire() as conn:
with conn.cursor() as cur:
insert_query = """
INSERT INTO payment_logs (
log_id, user_id, user_first_name, user_last_name, user_username,
user_language_code, user_is_premium, service_type, vin_number,
payment_amount, transaction_id, payment_status, payment_currency,
service_status, data_found_count, refund_status, refund_reason,
vehicle_make, vehicle_model, vehicle_year, error_message,
created_date, ip_address
) VALUES (
payment_logs_seq.NEXTVAL, :user_id, :user_first_name, :user_last_name, :user_username,
:user_language_code, :user_is_premium, :service_type, :vin_number,
:payment_amount, :transaction_id, :payment_status, :payment_currency,
:service_status, :data_found_count, :refund_status, :refund_reason,
:vehicle_make, :vehicle_model, :vehicle_year, :error_message,
SYSDATE, :ip_address
)
"""
params = {
"user_id": user.id,
"user_first_name": user.first_name,
"user_last_name": user.last_name,
"user_username": user.username,
"user_language_code": user.language_code,
"user_is_premium": 1 if user.is_premium else 0,
"service_type": service_type,
"vin_number": vin,
"payment_amount": payment_data.get('amount', 0),
"transaction_id": payment_data.get('transaction_id'),
"payment_status": payment_data.get('status', 'completed'),
"payment_currency": payment_data.get('currency', 'XTR'),
"service_status": service_result.get('status', 'success') if service_result else 'pending',
"data_found_count": service_result.get('data_count', 0) if service_result else 0,
"refund_status": payment_data.get('refund_status', 'no_refund'),
"refund_reason": payment_data.get('refund_reason'),
"vehicle_make": service_result.get('vehicle_make') if service_result else None,
"vehicle_model": service_result.get('vehicle_model') if service_result else None,
"vehicle_year": service_result.get('vehicle_year') if service_result else None,
"error_message": service_result.get('error') if service_result else None,
"ip_address": None # Telegram не предоставляет IP адреса
}
cur.execute(insert_query, params)
conn.commit()
return True
try:
import asyncio
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _save_log)
except Exception as e:
print(f"Error saving payment log for user {user.id}: {e}")
return False

100
db_sql/log_payment.sql Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -4,10 +4,11 @@ import logging
from datetime import datetime
import platform
import os
from logging.handlers import TimedRotatingFileHandler
from aiogram import Bot, Dispatcher
from aiogram.filters import Command
from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, LabeledPrice, PreCheckoutQuery
from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, LabeledPrice, PreCheckoutQuery, InputMediaPhoto, FSInputFile
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext
@ -15,6 +16,80 @@ from db import OracleDatabase
from middlewares.db import DbSessionMiddleware
def setup_logging():
"""
Настройка системы логирования с ротацией файлов и выводом в консоль
"""
# Создаем каталог logs если он не существует
if is_windows():
logs_dir = "logs"
else:
logs_dir = "/logs/"
if not os.path.exists(logs_dir):
os.makedirs(logs_dir)
print(f"Created logs directory: {logs_dir}")
# Определяем уровень логирования
if getenv("DEBUG", '0') == '1':
log_level = logging.INFO
else:
log_level = logging.WARNING
# Временно включаем детальное логирование для отладки
log_level = logging.INFO
# Создаем основной логгер
logger = logging.getLogger()
logger.setLevel(log_level)
# Очищаем существующие обработчики
logger.handlers.clear()
# Формат логов
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Настройка файлового логирования с ротацией
file_handler = TimedRotatingFileHandler(
filename=os.path.join(logs_dir, "salvagedb_bot.log"),
when='midnight', # Ротация в полночь
interval=1, # Каждый день
backupCount=30, # Храним 30 дней
encoding='utf-8',
utc=False # Используем локальное время
)
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)
file_handler.suffix = "%Y-%m-%d" # Формат суффикса для файлов
# Настройка консольного логирования
console_handler = logging.StreamHandler()
console_handler.setLevel(log_level)
console_handler.setFormatter(formatter)
# Добавляем обработчики к логгеру
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# Логируем успешную настройку
logging.info("=== LOGGING SYSTEM INITIALIZED ===")
logging.info(f"Log level: {logging.getLevelName(log_level)}")
logging.info(f"Logs directory: {os.path.abspath(logs_dir)}")
logging.info(f"Log rotation: daily, keeping 30 days")
# Проверяем запуск в Docker
if os.path.exists('/.dockerenv'):
logging.info("Running inside Docker container")
logging.info(f"Container timezone: {getenv('TZ', 'UTC')}")
logging.info(f"Container user: {getenv('USER', 'unknown')}")
else:
logging.info("Running on host system")
logging.info("=== LOGGING SETUP COMPLETE ===")
def get_us_state_name(state_code: str) -> str:
"""
Конвертирует двухбуквенный код штата США в полное название
@ -148,13 +223,193 @@ def is_macos() -> bool:
return get_operating_system() == 'macOS'
if getenv("DEBUG",'0') == '1':
logging.basicConfig(level=logging.INFO)
else:
logging.basicConfig(level=logging.WARNING)
def convert_photo_path(db_path: str) -> str:
"""
Конвертирует путь к фотографии в зависимости от операционной системы
Args:
db_path: путь из базы данных в Linux формате (с /)
Returns:
str: полный путь к файлу с учетом OS и базового пути
"""
if not db_path:
return ""
# Временно включаем детальное логирование для отладки
logging.getLogger().setLevel(logging.INFO)
# Убираем лишние пробелы из пути
db_path = db_path.strip()
# Базовый путь из константы
base_path = image_path
if is_windows():
# Конвертируем Linux пути в Windows формат
windows_path = db_path.replace('/', '\\')
full_path = f"{base_path}\\{windows_path}"
logging.info(f"Converted path for Windows: {db_path} -> {full_path}")
return full_path
else:
# Для Linux/macOS оставляем как есть
full_path = f"{base_path}/{db_path}"
logging.info(f"Path for Linux/macOS: {db_path} -> {full_path}")
return full_path
def prepare_photo_paths(db_paths: list) -> list:
"""
Подготавливает список полных путей к фотографиям
Args:
db_paths: список путей из базы данных
Returns:
list: список полных путей к файлам
"""
if not db_paths:
return []
full_paths = []
for db_path in db_paths:
full_path = convert_photo_path(db_path)
if full_path:
full_paths.append(full_path)
logging.info(f"Prepared {len(full_paths)} photo paths from {len(db_paths)} database paths")
return full_paths
async def send_vehicle_photos(message: Message, vin: str, photo_paths: list, make: str, model: str, year: str):
"""
Отправляет фотографии автомобиля пользователю
Args:
message: сообщение пользователя
vin: VIN автомобиля
photo_paths: список полных путей к фотографиям
make, model, year: информация об автомобиле
"""
if not photo_paths:
await message.answer("❌ No photos found to send.")
return
try:
# Дебаг информация о текущем пользователе
import pwd
import grp
current_user = pwd.getpwuid(os.getuid())
current_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()]
logging.info(f"DEBUG: Running as user: {current_user.pw_name}({os.getuid()}), groups: {current_groups}")
# Telegram позволяет максимум 10 фотографий в media group
photos_per_group = 10
total_photos = len(photo_paths)
logging.info(f"Attempting to send {total_photos} photos for VIN: {vin}")
# Разбиваем фотографии на группы по 10
photo_groups = [photo_paths[i:i + photos_per_group] for i in range(0, len(photo_paths), photos_per_group)]
total_groups = len(photo_groups)
logging.info(f"Split {total_photos} photos into {total_groups} groups")
sent_count = 0
for group_num, photo_group in enumerate(photo_groups, 1):
try:
logging.info(f"Processing group {group_num}/{total_groups} with {len(photo_group)} photos")
# Создаем media group для текущей группы
media_group = []
for i, photo_path in enumerate(photo_group):
try:
# Проверяем существование файла
# Дебаг информация
import stat
import pwd
import grp
try:
stat_info = os.stat(photo_path)
file_owner = pwd.getpwuid(stat_info.st_uid).pw_name
file_group = grp.getgrgid(stat_info.st_gid).gr_name
file_perms = oct(stat_info.st_mode)[-3:]
logging.info(f"DEBUG: File {photo_path} - owner: {file_owner}({stat_info.st_uid}), group: {file_group}({stat_info.st_gid}), perms: {file_perms}")
except Exception as debug_e:
logging.warning(f"DEBUG: Cannot get file info for {photo_path}: {debug_e}")
if os.path.exists(photo_path):
# Создаем InputMediaPhoto
if i == 0 and group_num == 1:
# Первая фотография первой группы с полным описанием
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
caption = f"📸 Vehicle damage photos\n📋 VIN: {vin}\n📊 Total photos: {total_photos}\n🗂️ Group {group_num}/{total_groups}"
else:
caption = f"📸 {year} {make} {model}\n📋 VIN: {vin}\n📊 Total photos: {total_photos}\n🗂️ Group {group_num}/{total_groups}"
elif i == 0:
# Первая фотография других групп с номером группы
caption = f"🗂️ Group {group_num}/{total_groups}"
else:
# Остальные фотографии без описания
caption = None
if caption:
media_group.append(InputMediaPhoto(
media=FSInputFile(photo_path),
caption=caption
))
else:
media_group.append(InputMediaPhoto(
media=FSInputFile(photo_path)
))
sent_count += 1
logging.info(f"Added photo {sent_count}/{total_photos}: {photo_path}")
else:
logging.warning(f"Photo file not found: {photo_path}")
except Exception as photo_error:
logging.error(f"Error processing photo {sent_count + 1} ({photo_path}): {photo_error}")
continue
if media_group:
# Отправляем media group
await message.answer_media_group(media_group)
logging.info(f"Successfully sent group {group_num}/{total_groups} with {len(media_group)} photos")
# Небольшая пауза между группами для избежания rate limiting
if group_num < total_groups:
await asyncio.sleep(0.5)
else:
logging.warning(f"No valid photos in group {group_num}")
except Exception as group_error:
logging.error(f"Error sending photo group {group_num}: {group_error}")
await message.answer(f"❌ Error sending photo group {group_num}. Continuing with remaining photos...")
continue
if sent_count > 0:
# Отправляем итоговое сообщение
await message.answer(
f"✅ **Photos sent successfully!**\n"
f"📊 **{sent_count} of {total_photos} photos** delivered\n"
f"🗂️ **{total_groups} photo groups** sent"
)
logging.info(f"Successfully sent {sent_count}/{total_photos} photos for VIN: {vin}")
else:
# Если ни одна фотография не найдена
await message.answer(
"❌ **Error:** Photo files not found on server.\n"
"Please contact support with your transaction details."
)
logging.error(f"No valid photo files found for VIN: {vin}")
except Exception as e:
logging.error(f"Error sending photos for VIN {vin}: {e}")
await message.answer(
"❌ **Error sending photos.** Please contact support with your transaction details.\n"
f"Error details: {str(e)}"
)
# Настройка системы логирования
setup_logging()
# Логируем информацию о системе
log_system_info()
TOKEN = getenv("BOT_TOKEN")
@ -167,13 +422,13 @@ IMG_PRICE = getenv("IMG_PRICE",100)
if is_windows():
image_path = "D:\\SALVAGEDB\\salvagedb_bot\\images"
else:
image_path = "/images/"
image_path = "/images"
oracle_db = OracleDatabase(
user= getenv("db_user"),
password= getenv("db_password"),
dsn= getenv("db_dsn")
user= getenv("DB_USER"),
password= getenv("DB_PASSWORD"),
dsn= getenv("DB_DSN")
)
@ -182,6 +437,7 @@ dp = Dispatcher()
class VinStates(StatesGroup):
waiting_for_vin = State()
waiting_for_check_vin = State()
waiting_for_photo_vin = State()
# Command handler
@ -240,6 +496,19 @@ async def check_vin_callback(callback: CallbackQuery, state: FSMContext, db: Ora
await callback.answer()
@dp.callback_query(lambda c: c.data == "search_car_photo")
async def search_car_photo_callback(callback: CallbackQuery, state: FSMContext, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при нажатии кнопки
await database.save_user(callback.from_user, "search_car_photo_button")
await callback.message.answer("Please enter the vehicle VIN to search for damage photos.")
await state.set_state(VinStates.waiting_for_photo_vin)
await callback.answer()
@dp.callback_query(lambda c: c.data == "main_menu")
async def main_menu_callback(callback: CallbackQuery, state: FSMContext, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
@ -253,6 +522,109 @@ async def main_menu_callback(callback: CallbackQuery, state: FSMContext, db: Ora
await callback.answer()
@dp.callback_query(lambda c: c.data == "help")
async def help_callback(callback: CallbackQuery, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при просмотре справки
await database.save_user(callback.from_user, "help_button")
help_text = (
" **Help - Our Services**\n\n"
"🔍 **1. Decode VIN**\n"
f"• Free basic vehicle information (make, model, year)\n"
f"• Detailed specifications for {DECODE_PRICE} ⭐ (engine, transmission, safety features, etc.)\n"
f"• Comprehensive technical data from NHTSA database\n\n"
"🚨 **2. Check VIN**\n"
f"• Free salvage records count\n"
f"• Detailed damage history for {CHECK_PRICE} ⭐ (auction data, damage types, repair costs)\n"
f"• Sale dates and locations from insurance auctions\n\n"
"📸 **3. Search Car Photos**\n"
f"• Check availability of damage photos\n"
f"• Access to actual auction photos for {IMG_PRICE}\n"
f"• High-quality images showing vehicle condition and damage\n\n"
"💡 **How to use:**\n"
"• Enter any 17-character VIN number\n"
"• VIN must contain only letters and numbers (no I, O, Q)\n"
"• All payments are made with Telegram Stars ⭐\n\n"
"⚠️ **Important:** Our reports show historical data for informed decision-making. "
"Always consult automotive experts for professional vehicle evaluation."
)
builder = InlineKeyboardBuilder()
builder.button(text="🔍 Try Decode VIN", callback_data="decode_vin")
builder.button(text="🚨 Try Check VIN", callback_data="check_vin")
builder.button(text="📸 Try Search Photos", callback_data="search_car_photo")
builder.adjust(3)
builder.button(text="🏠 Back to Main Menu", callback_data="main_menu")
builder.adjust(3, 1)
await callback.message.answer(help_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
@dp.callback_query(lambda c: c.data == "prices")
async def prices_callback(callback: CallbackQuery, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при просмотре цен
await database.save_user(callback.from_user, "prices_button")
prices_text = (
"💰 **Our Service Prices**\n\n"
"🔍 **VIN Decoding Service:**\n"
f"• Basic info (make, model, year): **FREE** 🆓\n"
f"• Detailed specifications: **{DECODE_PRICE} ⭐**\n"
f" └ Engine details, transmission, safety features\n"
f" └ Dimensions, construction, brake system\n"
f" └ Lighting, additional features, NCSA data\n\n"
"🚨 **Salvage Check Service:**\n"
f"• Records count check: **FREE** 🆓\n"
f"• Detailed damage history: **{CHECK_PRICE} ⭐**\n"
f" └ Primary and secondary damage types\n"
f" └ Sale dates and auction locations\n"
f" └ Odometer readings and repair costs\n"
f" └ Engine/drive status information\n\n"
"📸 **Vehicle Photos Service:**\n"
f"• Photo availability check: **FREE** 🆓\n"
f"• Access to damage photos: **{IMG_PRICE} ⭐**\n"
f" └ High-quality auction images\n"
f" └ Multiple angles of vehicle damage\n"
f" └ Before and after condition photos\n\n"
"⭐ **Payment Information:**\n"
"• All payments made with **Telegram Stars**\n"
"• Instant delivery after successful payment\n"
"• Automatic refund if no data found\n"
"• Admin users get automatic refunds\n\n"
"💡 **Money-back guarantee:** If we can't provide the requested data, "
"your payment will be automatically refunded!"
)
builder = InlineKeyboardBuilder()
builder.button(text=f"🔍 Decode for {DECODE_PRICE}", callback_data="decode_vin")
builder.button(text=f"🚨 Check for {CHECK_PRICE}", callback_data="check_vin")
builder.button(text=f"📸 Photos for {IMG_PRICE}", callback_data="search_car_photo")
builder.adjust(3)
builder.button(text=" Help", callback_data="help")
builder.button(text="🏠 Back to Main Menu", callback_data="main_menu")
builder.adjust(2, 1)
await callback.message.answer(prices_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
@dp.callback_query(lambda c: c.data and c.data.startswith("pay_detailed_info:"))
async def pay_detailed_info_callback(callback: CallbackQuery, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
@ -263,12 +635,12 @@ async def pay_detailed_info_callback(callback: CallbackQuery, db: OracleDatabase
# Extract VIN from callback data
vin = callback.data.split(":")[1]
prices = [LabeledPrice(label="Detailed VIN Report", amount=1)]
prices = [LabeledPrice(label="Detailed VIN Report", amount=DECODE_PRICE)]
logging.info(f"Sending invoice for VIN: {vin}")
await callback.bot.send_invoice(
chat_id=callback.message.chat.id,
title="Detailed VIN Information",
description="Get comprehensive vehicle detailed information for 1 Telegram Star",
description=f"Get comprehensive vehicle detailed information for {DECODE_PRICE} Telegram Star",
payload=f"detailed_vin_info:{vin}", # Include VIN in payload
provider_token="", # Empty for Telegram Stars
currency="XTR", # Telegram Stars currency
@ -364,7 +736,7 @@ async def process_check_vin(message: Message, state: FSMContext, db: OracleDatab
if salvage_count > 0:
# Есть записи - показываем кнопки: Pay 10⭐ for detailed info, Try another VIN, Back to main menu
builder.button(text="Pay 10 ⭐️ for detailed info", callback_data=f"pay_check_detailed:{vin}", pay=True)
builder.button(text=f"Pay {CHECK_PRICE} ⭐️ for detailed info", callback_data=f"pay_check_detailed:{vin}", pay=True)
builder.button(text="Try another VIN", callback_data="check_vin")
builder.button(text="Back to Main Menu", callback_data="main_menu")
builder.adjust(1, 1, 1) # Each button on separate row
@ -397,13 +769,13 @@ async def pay_check_detailed_callback(callback: CallbackQuery, db: OracleDatabas
# Извлекаем VIN из callback data
vin = callback.data.split(":")[1]
prices = [LabeledPrice(label="Detailed Salvage Report", amount=10)]
prices = [LabeledPrice(label="Detailed Salvage Report", amount=CHECK_PRICE)]
logging.info(f"Sending invoice for salvage check VIN: {vin}")
await callback.bot.send_invoice(
chat_id=callback.message.chat.id,
title="Detailed Salvage Report",
description="Get comprehensive salvage history and damage information for 10 Telegram Stars",
description=f"Get comprehensive salvage history and damage information for {CHECK_PRICE} Telegram Stars",
payload=f"detailed_salvage_check:{vin}", # Уникальный payload для этого типа платежа
provider_token="", # Empty for Telegram Stars
currency="XTR", # Telegram Stars currency
@ -412,6 +784,31 @@ async def pay_check_detailed_callback(callback: CallbackQuery, db: OracleDatabas
await callback.answer()
@dp.callback_query(lambda c: c.data and c.data.startswith("pay_photos:"))
async def pay_photos_callback(callback: CallbackQuery, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при инициации платежа
await database.save_user(callback.from_user, "photos_payment_initiation")
# Извлекаем VIN из callback data
vin = callback.data.split(":")[1]
prices = [LabeledPrice(label="Vehicle Damage Photos", amount=IMG_PRICE)]
logging.info(f"Sending invoice for photos VIN: {vin}")
await callback.bot.send_invoice(
chat_id=callback.message.chat.id,
title="Vehicle Damage Photos",
description=f"Get access to all damage photos for this vehicle for {IMG_PRICE} Telegram Stars",
payload=f"vehicle_photos:{vin}", # Уникальный payload для фотографий
provider_token="", # Empty for Telegram Stars
currency="XTR", # Telegram Stars currency
prices=prices
)
await callback.answer()
@dp.pre_checkout_query()
async def pre_checkout_handler(pre_checkout_query: PreCheckoutQuery, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
@ -475,9 +872,24 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
payload = message.successful_payment.invoice_payload
# Определяем сумму платежа в зависимости от типа
payment_amount = 10.0 if payload.startswith("detailed_salvage_check:") else 1.0
if payload.startswith("detailed_salvage_check:"):
payment_amount = float(CHECK_PRICE)
elif payload.startswith("vehicle_photos:"):
payment_amount = float(IMG_PRICE)
else:
payment_amount = float(DECODE_PRICE)
await database.update_user_payment(message.from_user.id, payment_amount)
# Подготавливаем базовые данные платежа для логирования
payment_data = {
'amount': payment_amount,
'transaction_id': message.successful_payment.telegram_payment_charge_id,
'status': 'completed',
'currency': 'XTR',
'refund_status': 'no_refund'
}
if payload.startswith("detailed_vin_info:"):
vin = payload.split(":")[1]
@ -604,6 +1016,17 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
await message.answer(plain_report, reply_markup=builder.as_markup())
logging.info("Plain text message sent successfully!")
# Логируем успешную операцию DecodeVin
service_result = {
'status': 'success',
'data_count': len(detailed_info['all_params']),
'vehicle_make': params.get('make', 'N/A'),
'vehicle_model': params.get('model', 'N/A'),
'vehicle_year': params.get('model_year', 'N/A')
}
await database.save_payment_log(message.from_user, "decode_vin", vin, payment_data, service_result)
logging.info(f"Payment logged successfully for DecodeVin service - User: {message.from_user.id}, VIN: {vin}")
# Проверяем, является ли пользователь администратором и возвращаем звезды
if message.from_user.id == ADMIN_USER_ID:
try:
@ -611,6 +1034,7 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
payment_data['refund_status'] = 'admin_refund'
await message.answer(
"🔧 **Admin Refund**\n\n"
f"💰 Payment automatically refunded for admin user.\n"
@ -629,6 +1053,16 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
)
else:
# No detailed information found - refund the payment
service_result = {
'status': 'no_data',
'data_count': 0,
'error': 'No detailed information found for VIN'
}
payment_data['refund_status'] = 'auto_refund'
payment_data['refund_reason'] = 'No detailed information found'
await database.save_payment_log(message.from_user, "decode_vin", vin, payment_data, service_result)
logging.info(f"Payment logged for DecodeVin no data case - User: {message.from_user.id}, VIN: {vin}")
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
@ -647,10 +1081,20 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
"⚠️ Please contact support with this transaction ID for a refund:\n"
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
)
except Exception as e:
logging.error(f"Error getting detailed VIN info for {vin}: {e}")
# Логируем ошибку
service_result = {
'status': 'error',
'data_count': 0,
'error': str(e)
}
payment_data['refund_status'] = 'auto_refund'
payment_data['refund_reason'] = 'Service error'
await database.save_payment_log(message.from_user, "decode_vin", vin, payment_data, service_result)
logging.info(f"Payment logged for DecodeVin error case - User: {message.from_user.id}, VIN: {vin}")
# Attempt to refund the payment due to service error
try:
await message.bot.refund_star_payment(
@ -785,6 +1229,17 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
await message.answer(plain_report, reply_markup=builder.as_markup())
logging.info("Plain text message sent successfully!")
# Логируем успешную операцию CheckSalvage
service_result = {
'status': 'success',
'data_count': len(salvage_records),
'vehicle_make': make,
'vehicle_model': model,
'vehicle_year': year
}
await database.save_payment_log(message.from_user, "check_salvage", vin, payment_data, service_result)
logging.info(f"Payment logged successfully for CheckSalvage service - User: {message.from_user.id}, VIN: {vin}")
# Отправляем отдельное сообщение о фотографиях
if salvage_records and salvage_records[0]['img_count'] > 0:
img_count = salvage_records[0]['img_count']
@ -802,6 +1257,7 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
payment_data['refund_status'] = 'admin_refund'
await message.answer(
"🔧 **Admin Refund**\n\n"
f"💰 Payment automatically refunded for admin user.\n"
@ -820,6 +1276,16 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
)
else:
# Нет записей - возвращаем деньги
service_result = {
'status': 'no_data',
'data_count': 0,
'error': 'No salvage records found for VIN'
}
payment_data['refund_status'] = 'auto_refund'
payment_data['refund_reason'] = 'No salvage records found'
await database.save_payment_log(message.from_user, "check_salvage", vin, payment_data, service_result)
logging.info(f"Payment logged for CheckSalvage no data case - User: {message.from_user.id}, VIN: {vin}")
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
@ -842,6 +1308,17 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
except Exception as e:
logging.error(f"Error getting salvage info for {vin}: {e}")
# Логируем ошибку
service_result = {
'status': 'error',
'data_count': 0,
'error': str(e)
}
payment_data['refund_status'] = 'auto_refund'
payment_data['refund_reason'] = 'Service error'
await database.save_payment_log(message.from_user, "check_salvage", vin, payment_data, service_result)
logging.info(f"Payment logged for CheckSalvage error case - User: {message.from_user.id}, VIN: {vin}")
# Возвращаем деньги при ошибке
try:
await message.bot.refund_star_payment(
@ -861,6 +1338,213 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
"⚠️ Please contact support immediately with this transaction ID for a manual refund:\n"
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
)
elif payload.startswith("vehicle_photos:"):
vin = payload.split(":")[1]
try:
# Получаем информацию о VIN и количество фотографий
make, model, year, cnt = await database.fetch_vin_info(vin)
photo_count = await database.count_photo_records(vin)
logging.info(f"Photos payment for VIN: {vin}, make: {make}, model: {model}, year: {year}, photo_count: {photo_count}")
if photo_count > 0:
# Есть фотографии - предоставляем доступ (пока заглушка)
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
response_text = f"📸 **Vehicle Damage Photos**\n\n"
else:
response_text = f"🚗 **{year} {make} {model}**\n\n"
response_text += f"📸 **Vehicle Damage Photos**\n\n"
response_text += f"✅ **Payment successful!** You now have access to **{photo_count} damage photos** for this vehicle.\n\n"
response_text += f"📁 **Photos will be sent separately** - please wait while we prepare your images.\n\n"
response_text += f"---\n"
response_text += f"💰 **Transaction ID:** {escape_markdown(message.successful_payment.telegram_payment_charge_id)}\n"
response_text += f"📋 **VIN:** {escape_markdown(vin)}"
# Создаем клавиатуру с дополнительными действиями
builder = InlineKeyboardBuilder()
builder.button(text="Search another VIN", callback_data="search_car_photo")
builder.button(text="Back to Main Menu", callback_data="main_menu")
builder.adjust(2)
logging.info("Attempting to send photos payment success message...")
try:
await message.answer(response_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
logging.info("Photos payment message sent successfully!")
# Получаем пути к фотографиям из базы данных
logging.info(f"Fetching photo paths for VIN: {vin}")
db_photo_paths = await database.fetch_photo_paths(vin)
logging.info(f"Found {len(db_photo_paths)} photo paths in database")
if db_photo_paths:
# Подготавливаем полные пути к файлам
full_photo_paths = prepare_photo_paths(db_photo_paths)
logging.info(f"Prepared {len(full_photo_paths)} full photo paths")
# Отправляем фотографии
await send_vehicle_photos(message, vin, full_photo_paths, make, model, year)
# Логируем успешную операцию GetPhotos
service_result = {
'status': 'success',
'data_count': len(full_photo_paths),
'vehicle_make': make,
'vehicle_model': model,
'vehicle_year': year
}
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
logging.info(f"Payment logged successfully for GetPhotos service - User: {message.from_user.id}, VIN: {vin}")
else:
await message.answer(
"⚠️ **Warning:** No photo paths found in database despite photo count > 0.\n"
"Please contact support with your transaction details."
)
logging.warning(f"No photo paths found for VIN {vin} despite photo_count = {photo_count}")
# Логируем проблему с путями к фотографиям
service_result = {
'status': 'error',
'data_count': 0,
'error': 'Photo paths not found despite photo count > 0'
}
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
logging.info(f"Payment logged for GetPhotos path error case - User: {message.from_user.id}, VIN: {vin}")
except Exception as markdown_error:
logging.error(f"Markdown parsing failed for photos payment: {markdown_error}")
plain_response = response_text.replace("**", "").replace("*", "")
await message.answer(plain_response, reply_markup=builder.as_markup())
logging.info("Plain text photos payment message sent successfully!")
# Получаем пути к фотографиям из базы данных (fallback)
logging.info(f"Fetching photo paths for VIN: {vin} (fallback)")
db_photo_paths = await database.fetch_photo_paths(vin)
logging.info(f"Found {len(db_photo_paths)} photo paths in database (fallback)")
if db_photo_paths:
# Подготавливаем полные пути к файлам
full_photo_paths = prepare_photo_paths(db_photo_paths)
logging.info(f"Prepared {len(full_photo_paths)} full photo paths (fallback)")
# Отправляем фотографии
await send_vehicle_photos(message, vin, full_photo_paths, make, model, year)
# Логируем успешную операцию GetPhotos (fallback)
service_result = {
'status': 'success',
'data_count': len(full_photo_paths),
'vehicle_make': make,
'vehicle_model': model,
'vehicle_year': year
}
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
logging.info(f"Payment logged successfully for GetPhotos service (fallback) - User: {message.from_user.id}, VIN: {vin}")
else:
await message.answer(
"Warning: No photo paths found in database despite photo count > 0. "
"Please contact support with your transaction details."
)
logging.warning(f"No photo paths found for VIN {vin} despite photo_count = {photo_count} (fallback)")
# Логируем проблему с путями к фотографиям (fallback)
service_result = {
'status': 'error',
'data_count': 0,
'error': 'Photo paths not found despite photo count > 0 (fallback)'
}
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
logging.info(f"Payment logged for GetPhotos fallback path error case - User: {message.from_user.id}, VIN: {vin}")
# Проверяем, является ли пользователь администратором и возвращаем звезды
if message.from_user.id == ADMIN_USER_ID:
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
payment_data['refund_status'] = 'admin_refund'
await message.answer(
"🔧 **Admin Refund**\n\n"
f"💰 Payment automatically refunded for admin user.\n"
f"🆔 Transaction ID: {escape_markdown(message.successful_payment.telegram_payment_charge_id)}\n"
" Admin access - no charges applied.",
parse_mode="Markdown"
)
logging.info(f"Admin refund successful for user {message.from_user.id}")
except Exception as refund_error:
logging.error(f"Failed to refund admin payment: {refund_error}")
await message.answer(
"⚠️ **Admin Refund Failed**\n\n"
"Could not automatically refund admin payment. Please contact technical support.\n"
f"🆔 Transaction ID: {escape_markdown(message.successful_payment.telegram_payment_charge_id)}",
parse_mode="Markdown"
)
else:
# Нет фотографий - возвращаем деньги
service_result = {
'status': 'no_data',
'data_count': 0,
'error': 'No photos found for VIN'
}
payment_data['refund_status'] = 'auto_refund'
payment_data['refund_reason'] = 'No photos found'
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
logging.info(f"Payment logged for GetPhotos no data case - User: {message.from_user.id}, VIN: {vin}")
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
await message.answer(
"❌ No photos found for this VIN in our database.\n"
"💰 Your payment has been automatically refunded.\n"
"Please try another VIN or contact support if you believe this is an error."
)
logging.info(f"Refund successful for user {message.from_user.id} - no photos found for VIN {vin}")
except Exception as refund_error:
logging.error(f"Failed to refund payment for user {message.from_user.id}: {refund_error}")
await message.answer(
"❌ No photos found for this VIN.\n"
"⚠️ Please contact support with this transaction ID for a refund:\n"
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
)
except Exception as e:
logging.error(f"Error getting photos info for {vin}: {e}")
# Логируем ошибку
service_result = {
'status': 'error',
'data_count': 0,
'error': str(e)
}
payment_data['refund_status'] = 'auto_refund'
payment_data['refund_reason'] = 'Service error'
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
logging.info(f"Payment logged for GetPhotos error case - User: {message.from_user.id}, VIN: {vin}")
# Возвращаем деньги при ошибке
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
await message.answer(
"❌ Error retrieving photos information from our database.\n"
"💰 Your payment has been automatically refunded.\n"
"Please try again later or contact support if the issue persists."
)
logging.info(f"Refund successful for user {message.from_user.id}, charge_id: {message.successful_payment.telegram_payment_charge_id}")
except Exception as refund_error:
logging.error(f"Failed to refund payment for user {message.from_user.id}: {refund_error}")
await message.answer(
"❌ Error retrieving photos information from our database.\n"
"⚠️ Please contact support immediately with this transaction ID for a manual refund:\n"
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
)
else:
await message.answer(
f"✅ Payment successful! Thank you for your purchase.\n"
@ -892,8 +1576,67 @@ async def successful_payment_handler(message: Message, db: OracleDatabase = None
)
@dp.message(VinStates.waiting_for_photo_vin)
async def process_photo_vin(message: Message, state: FSMContext, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при обработке VIN
await database.save_user(message.from_user, "photo_vin_processing")
vin = message.text.strip().upper()
if len(vin) == 17 and vin.isalnum() and all(c not in vin for c in ["I", "O", "Q"]):
try:
# Получаем базовую информацию о VIN для заголовка
make, model, year, cnt = await database.fetch_vin_info(vin)
# Получаем количество фотографий
photo_count = await database.count_photo_records(vin)
logging.info(f"Photo search VIN: make: {make}, model: {model}, year: {year}, photo_count: {photo_count}")
# Формируем ответ в зависимости от наличия фотографий
builder = InlineKeyboardBuilder()
if photo_count > 0:
# Есть фотографии - показываем информацию и кнопку оплаты
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
response_text = f"📸 **Photo Information**\n\n"
else:
response_text = f"🚗 **{year} {make} {model}**\n\n"
response_text += f"📸 **Photo Information**\n\n"
response_text += f"🖼️ **{photo_count} damage photos** found in our database for this vehicle.\n"
response_text += f"These photos show the actual condition and damage of the vehicle during auction."
builder.button(text=f"Pay {IMG_PRICE} ⭐️ for photos", callback_data=f"pay_photos:{vin}", pay=True)
builder.button(text="Try another VIN", callback_data="search_car_photo")
builder.button(text="Back to Main Menu", callback_data="main_menu")
builder.adjust(1, 1, 1) # Each button on separate row
else:
# Нет фотографий
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
response_text = f"❌ **Unable to decode VIN or no photos found**\n\n"
else:
response_text = f"🚗 **{year} {make} {model}**\n\n"
response_text += f"📸 **No damage photos found** for this VIN in our database."
builder.button(text="Try another VIN", callback_data="search_car_photo")
builder.button(text="Back to Main Menu", callback_data="main_menu")
builder.adjust(1, 1) # Each button on separate row
await message.answer(response_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
except Exception as e:
logging.error(f"Database error for photo search VIN {vin}: {e}")
await message.answer("Error retrieving data from database. Please try again later.")
await state.clear()
else:
await message.answer("Invalid VIN. Please enter a valid 17-character VIN (letters and numbers, no I, O, Q).")
async def on_startup():
log_system_info() # Логируем информацию о системе
await oracle_db.connect()
# Регистрируем middleware для всех типов событий
dp.message.middleware(DbSessionMiddleware(oracle_db))

540
read_dev.md Normal file
View 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
View 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

Binary file not shown.