Добавлены функции для получения путей к фотографиям и подсчета их количества по VIN в классе OracleDatabase. Обновлены обработчики в main.py для обработки запросов на получение фотографий, включая логику оплаты и отправки изображений пользователю. Эти изменения улучшают функциональность бота и позволяют пользователям получать доступ к фотографиям повреждений автомобилей.

This commit is contained in:
Vlad 2025-06-02 00:37:53 +03:00
parent 3474fe5f96
commit 5f3d478adb
4 changed files with 550 additions and 2 deletions

26
db.py
View File

@ -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,16 @@ 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)

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;

413
main.py
View File

@ -7,7 +7,7 @@ import os
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
@ -148,6 +148,165 @@ def is_macos() -> bool:
return get_operating_system() == 'macOS' return get_operating_system() == 'macOS'
def convert_photo_path(db_path: str) -> str:
"""
Конвертирует путь к фотографии в зависимости от операционной системы
Args:
db_path: путь из базы данных в Linux формате (с /)
Returns:
str: полный путь к файлу с учетом OS и базового пути
"""
if not db_path:
return ""
# Базовый путь из константы
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:
# 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:
# Проверяем существование файла
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)}"
)
if getenv("DEBUG",'0') == '1': if getenv("DEBUG",'0') == '1':
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
else: else:
@ -182,6 +341,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 +400,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
@ -412,6 +585,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,7 +673,13 @@ 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)
if payload.startswith("detailed_vin_info:"): if payload.startswith("detailed_vin_info:"):
@ -861,6 +1065,151 @@ 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)
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}")
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)
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)")
# Проверяем, является ли пользователь администратором и возвращаем звезды
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
)
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:
# Нет фотографий - возвращаем деньги
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}")
# Возвращаем деньги при ошибке
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,6 +1241,66 @@ 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() # Логируем информацию о системе log_system_info() # Логируем информацию о системе
await oracle_db.connect() await oracle_db.connect()