Migrating to Postgres from sqlite3
This commit is contained in:
+238
-214
@@ -1,35 +1,36 @@
|
||||
import logging
|
||||
import json
|
||||
import aiosqlite
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from aiohttp import web
|
||||
from aiogram import Bot
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from yookassa.domain.notification import WebhookNotification
|
||||
from yookassa.domain.models import Amount
|
||||
from yookassa.domain.models.amount import Amount as YooKassaAmount
|
||||
|
||||
from db.dal import payment_dal, user_dal
|
||||
|
||||
from db.database import get_db_connection_manager, _setup_db_connection, get_user
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.payment_service import YooKassaService
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from config.settings import Settings
|
||||
from bot.services.payment_service import YooKassaService
|
||||
|
||||
payment_processing_lock = asyncio.Lock()
|
||||
|
||||
YOOKASSA_EVENT_PAYMENT_SUCCEEDED = 'payment.succeeded'
|
||||
YOOKASSA_EVENT_PAYMENT_CANCELED = 'payment.canceled'
|
||||
YOOKASSA_EVENT_PAYMENT_WAITING_FOR_CAPTURE = 'payment.waiting_for_capture'
|
||||
YOOKASSA_EVENT_REFUND_SUCCEEDED = 'refund.succeeded'
|
||||
|
||||
|
||||
async def process_successful_payment(bot: Bot, payment_info_from_webhook: dict,
|
||||
async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
payment_info_from_webhook: dict,
|
||||
i18n: JsonI18n, settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
yk_service: YooKassaService,
|
||||
subscription_service: SubscriptionService,
|
||||
referral_service: ReferralService):
|
||||
metadata = payment_info_from_webhook.get("metadata", {})
|
||||
@@ -43,6 +44,8 @@ async def process_successful_payment(bot: Bot, payment_info_from_webhook: dict,
|
||||
f"Missing crucial metadata for payment: {payment_info_from_webhook.get('id')}, metadata: {metadata}"
|
||||
)
|
||||
return
|
||||
|
||||
db_user = None
|
||||
try:
|
||||
user_id = int(user_id_str)
|
||||
subscription_months = int(subscription_months_str)
|
||||
@@ -50,262 +53,283 @@ async def process_successful_payment(bot: Bot, payment_info_from_webhook: dict,
|
||||
promo_code_id = int(
|
||||
promo_code_id_str
|
||||
) if promo_code_id_str and promo_code_id_str.isdigit() else None
|
||||
|
||||
amount_data = payment_info_from_webhook.get("amount", {})
|
||||
payment_value = float(amount_data.get("value", 0.0))
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
if not db_user:
|
||||
logging.error(
|
||||
f"User {user_id} not found in DB during successful payment processing for YK ID {payment_info_from_webhook.get('id')}. Payment record {payment_db_id}."
|
||||
)
|
||||
|
||||
await payment_dal.update_payment_status_by_db_id(
|
||||
session, payment_db_id, "failed_user_not_found",
|
||||
payment_info_from_webhook.get("id"))
|
||||
|
||||
return
|
||||
|
||||
except (TypeError, ValueError) as e:
|
||||
logging.error(
|
||||
f"Invalid metadata format for payment processing: {metadata} - {e}"
|
||||
)
|
||||
|
||||
if payment_db_id_str and payment_db_id_str.isdigit():
|
||||
try:
|
||||
await payment_dal.update_payment_status_by_db_id(
|
||||
session, int(payment_db_id_str), "failed_metadata_error",
|
||||
payment_info_from_webhook.get("id"))
|
||||
except Exception as e_upd:
|
||||
logging.error(
|
||||
f"Failed to update payment status after metadata error: {e_upd}"
|
||||
)
|
||||
return
|
||||
|
||||
final_end_date_for_user: Optional[datetime] = None
|
||||
applied_referee_bonus_days: Optional[int] = None
|
||||
base_subscription_end_date: Optional[datetime] = None
|
||||
|
||||
async with get_db_connection_manager() as db:
|
||||
await _setup_db_connection(db)
|
||||
try:
|
||||
await db.execute(
|
||||
"UPDATE payments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE payment_id = ? AND (yookassa_payment_id = ? OR yookassa_payment_id IS NULL)",
|
||||
(payment_info_from_webhook.get("status", "succeeded"),
|
||||
payment_db_id, payment_info_from_webhook.get("id")))
|
||||
|
||||
new_sub_details = await subscription_service.activate_subscription(
|
||||
user_id,
|
||||
subscription_months,
|
||||
payment_value,
|
||||
payment_db_id,
|
||||
db_conn=db,
|
||||
promo_code_id=promo_code_id)
|
||||
|
||||
if new_sub_details and new_sub_details.get('end_date'):
|
||||
base_subscription_end_date = new_sub_details['end_date']
|
||||
final_end_date_for_user = base_subscription_end_date
|
||||
|
||||
referral_bonus_info = await referral_service.apply_referral_bonuses_for_payment(
|
||||
user_id, subscription_months, db_conn=db)
|
||||
|
||||
if referral_bonus_info and referral_bonus_info.get(
|
||||
"referee_new_end_date"):
|
||||
final_end_date_for_user = referral_bonus_info[
|
||||
"referee_new_end_date"]
|
||||
applied_referee_bonus_days = referral_bonus_info.get(
|
||||
"referee_bonus_applied_days")
|
||||
|
||||
await db.commit()
|
||||
|
||||
user_lang = await subscription_service.get_user_language(
|
||||
user_id)
|
||||
_ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs
|
||||
)
|
||||
|
||||
success_message = ""
|
||||
if applied_referee_bonus_days and final_end_date_for_user:
|
||||
referee_user_data = await get_user(user_id)
|
||||
inviter_name_for_msg = _("friend_placeholder")
|
||||
|
||||
if referee_user_data and referee_user_data[
|
||||
'referred_by_id'] is not None:
|
||||
inviter_user_data_for_msg = await get_user(
|
||||
referee_user_data['referred_by_id'])
|
||||
if inviter_user_data_for_msg and inviter_user_data_for_msg[
|
||||
'first_name']:
|
||||
inviter_name_for_msg = inviter_user_data_for_msg[
|
||||
'first_name']
|
||||
|
||||
success_message = _(
|
||||
"payment_successful_with_referral_bonus",
|
||||
months=subscription_months,
|
||||
base_end_date=base_subscription_end_date.strftime(
|
||||
'%Y-%m-%d')
|
||||
if base_subscription_end_date else "N/A",
|
||||
bonus_days=applied_referee_bonus_days,
|
||||
final_end_date=final_end_date_for_user.strftime(
|
||||
'%Y-%m-%d'),
|
||||
inviter_name=inviter_name_for_msg)
|
||||
elif final_end_date_for_user:
|
||||
success_message = _(
|
||||
"payment_successful",
|
||||
months=subscription_months,
|
||||
end_date=final_end_date_for_user.strftime('%Y-%m-%d'))
|
||||
else:
|
||||
logging.error(
|
||||
f"Critical error: final_end_date_for_user is None for user {user_id}"
|
||||
)
|
||||
success_message = _("payment_successful_error_details")
|
||||
|
||||
try:
|
||||
await bot.send_message(user_id, success_message)
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Failed to send final payment success message to user {user_id}: {e}"
|
||||
)
|
||||
else:
|
||||
logging.error(
|
||||
f"Failed to activate subscription for user {user_id} after payment {payment_info_from_webhook.get('id')}"
|
||||
)
|
||||
await db.rollback()
|
||||
except Exception as e:
|
||||
try:
|
||||
yk_payment_id_from_hook = payment_info_from_webhook.get("id")
|
||||
updated_payment_record = await payment_dal.update_payment_status_by_db_id(
|
||||
session,
|
||||
payment_db_id=payment_db_id,
|
||||
new_status=payment_info_from_webhook.get("status", "succeeded"),
|
||||
yk_payment_id=yk_payment_id_from_hook)
|
||||
if not updated_payment_record:
|
||||
logging.error(
|
||||
f"Error during process_successful_payment transaction for user {user_id}: {e}",
|
||||
exc_info=True)
|
||||
await db.rollback()
|
||||
try:
|
||||
user_lang_for_error = await subscription_service.get_user_language(
|
||||
user_id)
|
||||
_err = lambda key, **kwargs: i18n.gettext(
|
||||
user_lang_for_error, key, **kwargs)
|
||||
await bot.send_message(user_id,
|
||||
_err("error_processing_your_payment"))
|
||||
except Exception as notify_err:
|
||||
logging.error(
|
||||
f"Failed to send error notification to user {user_id}: {notify_err}"
|
||||
)
|
||||
f"Failed to update payment record {payment_db_id} for yk_id {yk_payment_id_from_hook}"
|
||||
)
|
||||
raise Exception(
|
||||
f"DB Error: Could not update payment record {payment_db_id}")
|
||||
|
||||
activation_details = await subscription_service.activate_subscription(
|
||||
session,
|
||||
user_id,
|
||||
subscription_months,
|
||||
payment_value,
|
||||
payment_db_id,
|
||||
promo_code_id_from_payment=promo_code_id)
|
||||
|
||||
if not activation_details or not activation_details.get('end_date'):
|
||||
logging.error(
|
||||
f"Failed to activate subscription for user {user_id} after payment {yk_payment_id_from_hook}"
|
||||
)
|
||||
raise Exception(
|
||||
f"Subscription Error: Failed to activate for user {user_id}")
|
||||
|
||||
base_subscription_end_date = activation_details['end_date']
|
||||
final_end_date_for_user = base_subscription_end_date
|
||||
applied_promo_bonus_days = activation_details.get(
|
||||
"applied_promo_bonus_days", 0)
|
||||
|
||||
referral_bonus_info = await referral_service.apply_referral_bonuses_for_payment(
|
||||
session, user_id, subscription_months)
|
||||
applied_referee_bonus_days_from_referral: Optional[int] = None
|
||||
if referral_bonus_info and referral_bonus_info.get(
|
||||
"referee_new_end_date"):
|
||||
final_end_date_for_user = referral_bonus_info[
|
||||
"referee_new_end_date"]
|
||||
applied_referee_bonus_days_from_referral = referral_bonus_info.get(
|
||||
"referee_bonus_applied_days")
|
||||
|
||||
user_lang = db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE
|
||||
_ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs)
|
||||
|
||||
success_message = ""
|
||||
if applied_referee_bonus_days_from_referral and final_end_date_for_user:
|
||||
inviter_name_display = _("friend_placeholder")
|
||||
if db_user and db_user.referred_by_id:
|
||||
inviter = await user_dal.get_user_by_id(
|
||||
session, db_user.referred_by_id)
|
||||
if inviter and inviter.first_name:
|
||||
inviter_name_display = inviter.first_name
|
||||
elif inviter and inviter.username:
|
||||
inviter_name_display = f"@{inviter.username}"
|
||||
|
||||
success_message = _(
|
||||
"payment_successful_with_referral_bonus",
|
||||
months=subscription_months,
|
||||
base_end_date=base_subscription_end_date.strftime('%Y-%m-%d'),
|
||||
bonus_days=applied_referee_bonus_days_from_referral,
|
||||
final_end_date=final_end_date_for_user.strftime('%Y-%m-%d'),
|
||||
inviter_name=inviter_name_display)
|
||||
elif applied_promo_bonus_days > 0 and final_end_date_for_user:
|
||||
success_message = _(
|
||||
"payment_successful_with_promo",
|
||||
months=subscription_months,
|
||||
bonus_days=applied_promo_bonus_days,
|
||||
end_date=final_end_date_for_user.strftime('%Y-%m-%d'))
|
||||
elif final_end_date_for_user:
|
||||
success_message = _(
|
||||
"payment_successful",
|
||||
months=subscription_months,
|
||||
end_date=final_end_date_for_user.strftime('%Y-%m-%d'))
|
||||
else:
|
||||
logging.error(
|
||||
f"Critical error: final_end_date_for_user is None for user {user_id} after successful payment logic."
|
||||
)
|
||||
success_message = _("payment_successful_error_details")
|
||||
|
||||
try:
|
||||
await bot.send_message(user_id, success_message)
|
||||
except Exception as e_notify:
|
||||
logging.error(
|
||||
f"Failed to send final payment success message to user {user_id}: {e_notify}"
|
||||
)
|
||||
|
||||
except Exception as e_process:
|
||||
logging.error(
|
||||
f"Error during process_successful_payment main try block for user {user_id}: {e_process}",
|
||||
exc_info=True)
|
||||
|
||||
raise
|
||||
|
||||
|
||||
async def process_cancelled_payment(bot: Bot, payment_info_from_webhook: dict,
|
||||
async def process_cancelled_payment(session: AsyncSession, bot: Bot,
|
||||
payment_info_from_webhook: dict,
|
||||
i18n: JsonI18n, settings: Settings):
|
||||
|
||||
metadata = payment_info_from_webhook.get("metadata", {})
|
||||
user_id_str = metadata.get("user_id")
|
||||
payment_db_id_str = metadata.get("payment_db_id")
|
||||
|
||||
if not user_id_str or not payment_db_id_str:
|
||||
logging.warning(
|
||||
f"Missing metadata in cancelled payment: {payment_info_from_webhook.get('id')}"
|
||||
f"Missing metadata in cancelled payment webhook: {payment_info_from_webhook.get('id')}"
|
||||
)
|
||||
return
|
||||
try:
|
||||
user_id = int(user_id_str)
|
||||
payment_db_id = int(payment_db_id_str)
|
||||
except ValueError:
|
||||
logging.error(f"Invalid metadata in cancelled payment: {metadata}")
|
||||
return
|
||||
async with get_db_connection_manager() as db:
|
||||
await _setup_db_connection(db)
|
||||
await db.execute(
|
||||
"UPDATE payments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE payment_id = ? AND (yookassa_payment_id = ? OR yookassa_payment_id IS NULL)",
|
||||
(payment_info_from_webhook.get("status", "canceled"),
|
||||
payment_db_id, payment_info_from_webhook.get("id")))
|
||||
await db.commit()
|
||||
user_lang = getattr(settings, 'DEFAULT_LANGUAGE', 'en')
|
||||
_ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs)
|
||||
try:
|
||||
await bot.send_message(user_id, _("payment_failed"))
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Failed to send payment cancellation message to user {user_id}: {e}"
|
||||
)
|
||||
f"Invalid metadata in cancelled payment webhook: {metadata}")
|
||||
return
|
||||
|
||||
try:
|
||||
updated_payment = await payment_dal.update_payment_status_by_db_id(
|
||||
session,
|
||||
payment_db_id=payment_db_id,
|
||||
new_status=payment_info_from_webhook.get("status", "canceled"),
|
||||
yk_payment_id=payment_info_from_webhook.get("id"))
|
||||
|
||||
if updated_payment:
|
||||
logging.info(
|
||||
f"Payment {payment_db_id} (YK: {payment_info_from_webhook.get('id')}) status updated to cancelled for user {user_id}."
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Could not find payment record {payment_db_id} to update status to cancelled for user {user_id}."
|
||||
)
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
user_lang = settings.DEFAULT_LANGUAGE
|
||||
if db_user and db_user.language_code: user_lang = db_user.language_code
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs)
|
||||
await bot.send_message(user_id, _("payment_failed"))
|
||||
|
||||
except Exception as e_process_cancel:
|
||||
logging.error(
|
||||
f"Error processing cancelled payment for user {user_id}, payment_db_id {payment_db_id}: {e_process_cancel}",
|
||||
exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
async def yookassa_webhook_route(request: web.Request):
|
||||
logging.info(
|
||||
f"YooKassa Webhook Route: Available keys in request.app: {list(request.app.keys())}"
|
||||
)
|
||||
|
||||
try:
|
||||
bot: Bot = request.app['bot']
|
||||
i18n_instance: JsonI18n = request.app['i18n']
|
||||
settings: Settings = request.app['settings']
|
||||
yk_service: YooKassaService = request.app['yookassa_service']
|
||||
panel_service: PanelApiService = request.app['panel_service']
|
||||
subscription_service: SubscriptionService = request.app[
|
||||
'subscription_service']
|
||||
referral_service: ReferralService = request.app['referral_service']
|
||||
except KeyError as e:
|
||||
async_session_factory: sessionmaker = request.app[
|
||||
'async_session_factory']
|
||||
except KeyError as e_app_ctx:
|
||||
logging.error(
|
||||
f"KeyError accessing app context in yookassa_webhook_route: {e}.",
|
||||
f"KeyError accessing app context in yookassa_webhook_route: {e_app_ctx}.",
|
||||
exc_info=True)
|
||||
return web.Response(status=500,
|
||||
text="Internal Server Error: Missing app context")
|
||||
return web.Response(
|
||||
status=500,
|
||||
text="Internal Server Error: Missing app context component")
|
||||
|
||||
try:
|
||||
event_json = await request.json()
|
||||
|
||||
notification_object = WebhookNotification(event_json)
|
||||
payment_data_from_notification = notification_object.object
|
||||
|
||||
logging.info(
|
||||
f"YooKassa Webhook Parsed: Event='{notification_object.event}', PaymentId='{payment_data_from_notification.id}', Status='{payment_data_from_notification.status}'"
|
||||
f"YooKassa Webhook Parsed: Event='{notification_object.event}', "
|
||||
f"PaymentId='{payment_data_from_notification.id}', Status='{payment_data_from_notification.status}'"
|
||||
)
|
||||
|
||||
if not payment_data_from_notification or not hasattr(
|
||||
payment_data_from_notification,
|
||||
'metadata') or payment_data_from_notification.metadata is None:
|
||||
logging.error(
|
||||
f"YooKassa webhook payment {payment_data_from_notification.id} lacks metadata."
|
||||
f"YooKassa webhook payment {payment_data_from_notification.id} lacks metadata. Cannot process."
|
||||
)
|
||||
return web.Response(status=200, text="ok_error_no_metadata")
|
||||
payment_dict_for_processing = {}
|
||||
if hasattr(payment_data_from_notification, 'model_dump'):
|
||||
payment_dict_for_processing = payment_data_from_notification.model_dump(
|
||||
exclude_none=True)
|
||||
if 'amount' in payment_dict_for_processing and isinstance(
|
||||
payment_dict_for_processing['amount'], Amount):
|
||||
amount_obj = payment_dict_for_processing['amount']
|
||||
payment_dict_for_processing['amount'] = {
|
||||
"value": str(amount_obj.value),
|
||||
"currency": str(amount_obj.currency)
|
||||
}
|
||||
elif 'amount' in payment_dict_for_processing and not isinstance(
|
||||
payment_dict_for_processing['amount'], dict):
|
||||
amount_obj_original = payment_data_from_notification.amount
|
||||
payment_dict_for_processing['amount'] = {
|
||||
"value": str(amount_obj_original.value),
|
||||
"currency": str(amount_obj_original.currency)
|
||||
} if hasattr(amount_obj_original, 'value') and hasattr(
|
||||
amount_obj_original, 'currency') else {
|
||||
"value": "0.0",
|
||||
"currency": "RUB"
|
||||
}
|
||||
elif hasattr(payment_data_from_notification, 'amount') and hasattr(
|
||||
payment_data_from_notification.amount, 'value') and hasattr(
|
||||
payment_data_from_notification.amount, 'currency'):
|
||||
amount_obj = payment_data_from_notification.amount
|
||||
payment_dict_for_processing = {
|
||||
"id":
|
||||
str(payment_data_from_notification.id),
|
||||
"status":
|
||||
str(payment_data_from_notification.status),
|
||||
"paid":
|
||||
bool(payment_data_from_notification.paid),
|
||||
"amount": {
|
||||
"value": str(amount_obj.value),
|
||||
"currency": str(amount_obj.currency)
|
||||
},
|
||||
"metadata":
|
||||
dict(payment_data_from_notification.metadata)
|
||||
if payment_data_from_notification.metadata else {},
|
||||
"description":
|
||||
str(payment_data_from_notification.description)
|
||||
if payment_data_from_notification.description else None
|
||||
}
|
||||
else:
|
||||
logging.error(
|
||||
f"Could not serialize payment_data for payment {payment_data_from_notification.id}"
|
||||
)
|
||||
return web.Response(status=200, text="ok_error_serialization")
|
||||
|
||||
payment_dict_for_processing = {
|
||||
"id":
|
||||
str(payment_data_from_notification.id),
|
||||
"status":
|
||||
str(payment_data_from_notification.status),
|
||||
"paid":
|
||||
bool(payment_data_from_notification.paid),
|
||||
"amount": {
|
||||
"value": str(payment_data_from_notification.amount.value),
|
||||
"currency": str(payment_data_from_notification.amount.currency)
|
||||
} if payment_data_from_notification.amount else {},
|
||||
"metadata":
|
||||
dict(payment_data_from_notification.metadata),
|
||||
"description":
|
||||
str(payment_data_from_notification.description)
|
||||
if payment_data_from_notification.description else None,
|
||||
}
|
||||
|
||||
async with payment_processing_lock:
|
||||
if notification_object.event == YOOKASSA_EVENT_PAYMENT_SUCCEEDED:
|
||||
if payment_dict_for_processing.get(
|
||||
"paid") and payment_dict_for_processing.get(
|
||||
"status") == "succeeded":
|
||||
await process_successful_payment(
|
||||
bot, payment_dict_for_processing, i18n_instance,
|
||||
settings, panel_service, yk_service,
|
||||
subscription_service, referral_service)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Payment Succeeded event for {payment_dict_for_processing.get('id')} but data not ok: status='{payment_dict_for_processing.get('status')}', paid='{payment_dict_for_processing.get('paid')}'"
|
||||
)
|
||||
elif notification_object.event == YOOKASSA_EVENT_PAYMENT_CANCELED:
|
||||
await process_cancelled_payment(bot,
|
||||
payment_dict_for_processing,
|
||||
i18n_instance, settings)
|
||||
async with async_session_factory() as session:
|
||||
try:
|
||||
if notification_object.event == YOOKASSA_EVENT_PAYMENT_SUCCEEDED:
|
||||
if payment_dict_for_processing.get(
|
||||
"paid") and payment_dict_for_processing.get(
|
||||
"status") == "succeeded":
|
||||
await process_successful_payment(
|
||||
session, bot, payment_dict_for_processing,
|
||||
i18n_instance, settings, panel_service,
|
||||
subscription_service, referral_service)
|
||||
await session.commit()
|
||||
else:
|
||||
logging.warning(
|
||||
f"Payment Succeeded event for {payment_dict_for_processing.get('id')} "
|
||||
f"but data not as expected: status='{payment_dict_for_processing.get('status')}', "
|
||||
f"paid='{payment_dict_for_processing.get('paid')}'"
|
||||
)
|
||||
elif notification_object.event == YOOKASSA_EVENT_PAYMENT_CANCELED:
|
||||
await process_cancelled_payment(
|
||||
session, bot, payment_dict_for_processing,
|
||||
i18n_instance, settings)
|
||||
await session.commit()
|
||||
except Exception as e_webhook_db_processing:
|
||||
await session.rollback()
|
||||
logging.error(
|
||||
f"Error processing YooKassa webhook event '{notification_object.event}' "
|
||||
f"for YK Payment ID {payment_dict_for_processing.get('id')} in DB transaction: {e_webhook_db_processing}",
|
||||
exc_info=True)
|
||||
return web.Response(
|
||||
status=200, text="ok_internal_processing_error_logged")
|
||||
|
||||
return web.Response(status=200, text="ok")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logging.error("YooKassa Webhook: Invalid JSON.")
|
||||
return web.Response(status=200, text="ok_invalid_json")
|
||||
except KeyError as e:
|
||||
logging.error("YooKassa Webhook: Invalid JSON received.")
|
||||
return web.Response(status=400, text="bad_request_invalid_json")
|
||||
except Exception as e_general_webhook:
|
||||
logging.error(
|
||||
f"KeyError in yookassa_webhook_route after initial context access: {e}.",
|
||||
f"YooKassa Webhook general processing error: {e_general_webhook}",
|
||||
exc_info=True)
|
||||
return web.Response(
|
||||
status=500,
|
||||
text="Internal Server Error: Context error post-access")
|
||||
except Exception as e:
|
||||
logging.error(f"YooKassa Webhook processing error: {e}", exc_info=True)
|
||||
return web.Response(status=200, text="ok_internal_error")
|
||||
return web.Response(status=200,
|
||||
text="ok_general_internal_error_logged")
|
||||
|
||||
@@ -3,6 +3,8 @@ import re
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from aiogram.utils.markdown import hcode
|
||||
|
||||
from config.settings import Settings
|
||||
from bot.states.user_states import UserPromoStates
|
||||
@@ -10,26 +12,26 @@ from bot.services.promo_code_service import PromoCodeService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.keyboards.inline.user_keyboards import get_back_to_main_menu_markup
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from aiogram.utils.markdown import hcode
|
||||
|
||||
from .start import send_main_menu
|
||||
|
||||
router = Router(name="user_promo_router")
|
||||
|
||||
SUSPICIOUS_SQL_KEYWORDS_REGEX = re.compile(
|
||||
r"\b(DROP\s*TABLE|DELETE\s*FROM|ALTER\s*TABLE|TRUNCATE\s*TABLE|UNION\s*SELECT|;\s*SELECT|;\s*INSERT|;\s*UPDATE|;\s*DELETE|xp_cmdshell|sysdatabases|sysobjects|INFORMATION_SCHEMA)\b",
|
||||
r"\b(DROP\s*TABLE|DELETE\s*FROM|ALTER\s*TABLE|TRUNCATE\s*TABLE|UNION\s*SELECT|"
|
||||
r";\s*SELECT|;\s*INSERT|;\s*UPDATE|;\s*DELETE|xp_cmdshell|sysdatabases|sysobjects|INFORMATION_SCHEMA)\b",
|
||||
re.IGNORECASE)
|
||||
SUSPICIOUS_CHARS_REGEX = re.compile(r"(--|#\s|;|\*\/|\/\*)")
|
||||
MAX_PROMO_CODE_INPUT_LENGTH = 100
|
||||
|
||||
|
||||
async def prompt_promo_code_input(callback: types.CallbackQuery,
|
||||
state: FSMContext, i18n_data: dict,
|
||||
settings: Settings):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
settings: Settings, session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
await callback.answer("Language service error.", show_alert=True)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
@@ -44,8 +46,10 @@ async def prompt_promo_code_input(callback: types.CallbackQuery,
|
||||
await callback.message.edit_text(
|
||||
text=_(key="promo_code_prompt"),
|
||||
reply_markup=get_back_to_main_menu_markup(current_lang, i18n))
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to edit message for promo prompt: {e}")
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Failed to edit message for promo prompt: {e_edit}. Sending new one."
|
||||
)
|
||||
await callback.message.answer(
|
||||
text=_(key="promo_code_prompt"),
|
||||
reply_markup=get_back_to_main_menu_markup(current_lang, i18n))
|
||||
@@ -53,73 +57,96 @@ async def prompt_promo_code_input(callback: types.CallbackQuery,
|
||||
await callback.answer()
|
||||
await state.set_state(UserPromoStates.waiting_for_promo_code)
|
||||
logging.info(
|
||||
f"User {callback.from_user.id} entered state UserPromoStates.waiting_for_promo_code. FSM state: {await state.get_state()}"
|
||||
)
|
||||
f"User {callback.from_user.id} entered state UserPromoStates.waiting_for_promo_code. "
|
||||
f"FSM state: {await state.get_state()}")
|
||||
|
||||
|
||||
@router.message(UserPromoStates.waiting_for_promo_code, F.text)
|
||||
async def process_promo_code_input(message: types.Message, state: FSMContext,
|
||||
settings: Settings, i18n_data: dict,
|
||||
promo_code_service: PromoCodeService,
|
||||
bot: Bot):
|
||||
bot: Bot, session: AsyncSession):
|
||||
logging.info(
|
||||
f"Processing promo code input from user {message.from_user.id} in state {await state.get_state()}: '{message.text}'"
|
||||
)
|
||||
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
|
||||
if not i18n or not promo_code_service:
|
||||
logging.error("Deps missing in process_promo_code_input")
|
||||
await message.reply("Service error. Please try again.")
|
||||
logging.error(
|
||||
"Dependencies (i18n or PromoCodeService) missing in process_promo_code_input"
|
||||
)
|
||||
await message.reply("Service error. Please try again later.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
code_input = message.text.strip()
|
||||
code_input = message.text.strip() if message.text else ""
|
||||
user = message.from_user
|
||||
|
||||
is_suspicious = False
|
||||
if SUSPICIOUS_SQL_KEYWORDS_REGEX.search(
|
||||
code_input) or SUSPICIOUS_CHARS_REGEX.search(code_input) or len(
|
||||
code_input) > 100:
|
||||
if not code_input:
|
||||
is_suspicious = True
|
||||
logging.warning(f"Empty promo code input by user {user.id}.")
|
||||
elif len(
|
||||
code_input
|
||||
) > MAX_PROMO_CODE_INPUT_LENGTH or SUSPICIOUS_SQL_KEYWORDS_REGEX.search(
|
||||
code_input) or SUSPICIOUS_CHARS_REGEX.search(code_input):
|
||||
is_suspicious = True
|
||||
logging.warning(
|
||||
f"Suspicious input for promo by user {user.id} (len: {len(code_input)}): '{code_input}'"
|
||||
f"Suspicious input for promo code by user {user.id} (len: {len(code_input)}): '{code_input}'"
|
||||
)
|
||||
|
||||
response_to_user_text = ""
|
||||
if is_suspicious:
|
||||
admin_notify_key = "admin_suspicious_promo_attempt_notification_no_username" if not user.username else "admin_suspicious_promo_attempt_notification"
|
||||
admin_lang = settings.DEFAULT_LANGUAGE
|
||||
_admin = lambda k, **kw: i18n.gettext(admin_lang, k, **kw)
|
||||
admin_notification_text = _admin(admin_notify_key,
|
||||
user_id=user.id,
|
||||
user_username=user.username or "N/A",
|
||||
user_first_name=user.first_name
|
||||
or "N/A",
|
||||
promo_code_input=hcode(code_input))
|
||||
try:
|
||||
await bot.send_message(settings.ADMIN_ID,
|
||||
admin_notification_text,
|
||||
parse_mode="HTML")
|
||||
except Exception as e_admin_notify:
|
||||
logging.error(
|
||||
f"Failed to send suspicious promo notification to admin: {e_admin_notify}"
|
||||
)
|
||||
|
||||
if settings.ADMIN_IDS:
|
||||
admin_notify_key = "admin_suspicious_promo_attempt_notification" if user.username else "admin_suspicious_promo_attempt_notification_no_username"
|
||||
|
||||
admin_lang = settings.DEFAULT_LANGUAGE
|
||||
_admin = lambda k, **kw: i18n.gettext(admin_lang, k, **kw)
|
||||
admin_notification_text = _admin(
|
||||
admin_notify_key,
|
||||
user_id=user.id,
|
||||
user_username=user.username or "N/A",
|
||||
user_first_name=user.first_name or "N/A",
|
||||
promo_code_input=hcode(code_input))
|
||||
for admin_id in settings.ADMIN_IDS:
|
||||
try:
|
||||
await bot.send_message(admin_id,
|
||||
admin_notification_text,
|
||||
parse_mode="HTML")
|
||||
except Exception as e_admin_notify:
|
||||
logging.error(
|
||||
f"Failed to send suspicious promo notification to admin {admin_id}: {e_admin_notify}"
|
||||
)
|
||||
|
||||
response_to_user_text = _("promo_code_not_found",
|
||||
code=code_input.upper())
|
||||
code=hcode(code_input.upper()))
|
||||
else:
|
||||
|
||||
success, response_text_from_service = await promo_code_service.apply_promo_code(
|
||||
user.id, code_input, current_lang)
|
||||
session, user.id, code_input, current_lang)
|
||||
response_to_user_text = response_text_from_service
|
||||
if success:
|
||||
await session.commit()
|
||||
logging.info(
|
||||
f"Promo code '{code_input}' successfully applied for user {user.id}."
|
||||
)
|
||||
else:
|
||||
await session.rollback()
|
||||
logging.info(
|
||||
f"Promo code '{code_input}' application failed for user {user.id}. Reason: {response_text_from_service}"
|
||||
)
|
||||
|
||||
await message.answer(response_to_user_text,
|
||||
reply_markup=get_back_to_main_menu_markup(
|
||||
current_lang, i18n))
|
||||
current_lang, i18n),
|
||||
parse_mode="HTML")
|
||||
await state.clear()
|
||||
logging.info(
|
||||
f"Promo code '{code_input}' processing finished for user {message.from_user.id}. State cleared."
|
||||
f"Promo code input '{code_input}' processing finished for user {message.from_user.id}. State cleared."
|
||||
)
|
||||
|
||||
|
||||
@@ -127,9 +154,9 @@ async def process_promo_code_input(message: types.Message, state: FSMContext,
|
||||
UserPromoStates.waiting_for_promo_code)
|
||||
async def cancel_promo_input_via_button(
|
||||
callback: types.CallbackQuery, state: FSMContext, settings: Settings,
|
||||
i18n_data: dict, subscription_service: SubscriptionService):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
i18n_data: dict, subscription_service: SubscriptionService,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing in cancel_promo_input_via_button")
|
||||
@@ -140,21 +167,17 @@ async def cancel_promo_input_via_button(
|
||||
f"User {callback.from_user.id} cancelled promo code input via button from state {await state.get_state()}. Clearing state."
|
||||
)
|
||||
await state.clear()
|
||||
logging.info(
|
||||
f"State after clear for user {callback.from_user.id}: {await state.get_state()}"
|
||||
)
|
||||
|
||||
if callback.message:
|
||||
show_trial_button_on_back = False
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
callback.from_user.id):
|
||||
show_trial_button_on_back = True
|
||||
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=show_trial_button_on_back,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
else:
|
||||
|
||||
await callback.answer("Promo code input cancelled.", show_alert=False)
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
await callback.answer(_("promo_input_cancelled_short"),
|
||||
show_alert=False)
|
||||
|
||||
@@ -1,67 +1,88 @@
|
||||
import logging
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.filters import Command
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional, Union
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from bot.services.referral_service import ReferralService
|
||||
from db.database import get_db_connection_manager
|
||||
from bot.keyboards.inline.user_keyboards import get_referral_link_keyboard, get_back_to_main_menu_markup
|
||||
|
||||
from bot.keyboards.inline.user_keyboards import get_back_to_main_menu_markup
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
router = Router(name="user_referral_router")
|
||||
|
||||
|
||||
async def referral_command_handler(event: types.Message | types.CallbackQuery,
|
||||
async def referral_command_handler(event: Union[types.Message,
|
||||
types.CallbackQuery],
|
||||
settings: Settings, i18n_data: dict,
|
||||
referral_service: ReferralService,
|
||||
bot: Bot):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
referral_service: ReferralService, bot: Bot,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
|
||||
target_message = event.message if isinstance(
|
||||
target_message_obj = event.message if isinstance(
|
||||
event, types.CallbackQuery) else event
|
||||
if not target_message:
|
||||
if not target_message_obj:
|
||||
logging.error(
|
||||
"Target message is None in referral_command_handler from callback."
|
||||
"Target message is None in referral_command_handler (possibly from callback without message)."
|
||||
)
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer("Error displaying referral info.")
|
||||
await event.answer("Error displaying referral info.",
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
if not i18n or not referral_service:
|
||||
logging.error("Deps missing in referral_command_handler")
|
||||
await target_message.answer("Service error." if isinstance(
|
||||
event, types.Message) else "Service error.",
|
||||
parse_mode=None)
|
||||
logging.error(
|
||||
"Dependencies (i18n or ReferralService) missing in referral_command_handler"
|
||||
)
|
||||
await target_message_obj.answer(
|
||||
"Service error. Please try again later.")
|
||||
if isinstance(event, types.CallbackQuery): await event.answer()
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
bot_info = await bot.get_me()
|
||||
bot_username = bot_info.username
|
||||
try:
|
||||
bot_info = await bot.get_me()
|
||||
bot_username = bot_info.username
|
||||
except Exception as e_bot_info:
|
||||
logging.error(
|
||||
f"Failed to get bot info for referral link: {e_bot_info}")
|
||||
await target_message_obj.answer(_("error_generating_referral_link"))
|
||||
if isinstance(event, types.CallbackQuery): await event.answer()
|
||||
return
|
||||
|
||||
if not bot_username:
|
||||
logging.error("Bot username is None, cannot generate referral link.")
|
||||
await target_message_obj.answer(_("error_generating_referral_link"))
|
||||
if isinstance(event, types.CallbackQuery): await event.answer()
|
||||
return
|
||||
|
||||
inviter_user_id = event.from_user.id
|
||||
referral_link = referral_service.generate_referral_link(
|
||||
bot_username, inviter_user_id)
|
||||
bonus_info_parts = []
|
||||
|
||||
if hasattr(settings,
|
||||
'subscription_options') and settings.subscription_options:
|
||||
for months_period in sorted(settings.subscription_options.keys()):
|
||||
inv_bonus = settings.referral_bonus_inviter.get(months_period)
|
||||
ref_bonus = settings.referral_bonus_referee.get(months_period)
|
||||
bonus_info_parts = []
|
||||
if settings.subscription_options:
|
||||
|
||||
for months_period_key, _price in sorted(
|
||||
settings.subscription_options.items()):
|
||||
|
||||
inv_bonus = settings.referral_bonus_inviter.get(months_period_key)
|
||||
ref_bonus = settings.referral_bonus_referee.get(months_period_key)
|
||||
if inv_bonus is not None or ref_bonus is not None:
|
||||
bonus_info_parts.append(
|
||||
_("referral_bonus_per_period",
|
||||
months=months_period,
|
||||
months=months_period_key,
|
||||
inviter_bonus_days=inv_bonus
|
||||
if inv_bonus is not None else _("no_bonus_days"),
|
||||
if inv_bonus is not None else _("no_bonus_placeholder"),
|
||||
referee_bonus_days=ref_bonus
|
||||
if ref_bonus is not None else _("no_bonus_days")))
|
||||
if ref_bonus is not None else _("no_bonus_placeholder")))
|
||||
|
||||
bonus_details_str = "\n".join(bonus_info_parts) if bonus_info_parts else _(
|
||||
"referral_no_bonuses_configured")
|
||||
|
||||
text = _("referral_program_info_new",
|
||||
referral_link=referral_link,
|
||||
bonus_details=bonus_details_str)
|
||||
@@ -72,29 +93,16 @@ async def referral_command_handler(event: types.Message | types.CallbackQuery,
|
||||
await event.answer(text,
|
||||
reply_markup=reply_markup_val,
|
||||
disable_web_page_preview=True)
|
||||
elif isinstance(event, types.CallbackQuery):
|
||||
elif isinstance(event, types.CallbackQuery) and event.message:
|
||||
try:
|
||||
await event.message.edit_text(text,
|
||||
reply_markup=reply_markup_val,
|
||||
disable_web_page_preview=True)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to edit message for referral info: {e}")
|
||||
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Failed to edit message for referral info: {e_edit}. Sending new one."
|
||||
)
|
||||
await event.message.answer(text,
|
||||
reply_markup=reply_markup_val,
|
||||
disable_web_page_preview=True)
|
||||
await event.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "copy_referral_link_ack")
|
||||
async def copy_referral_link_ack_callback_handler(
|
||||
callback: types.CallbackQuery, i18n_data: dict, settings: Settings):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
return await callback.answer("Language service error.",
|
||||
show_alert=True)
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
await callback.answer(text=_(key="referral_link_for_copying_reminder"),
|
||||
show_alert=False)
|
||||
|
||||
+224
-174
@@ -2,10 +2,12 @@ import logging
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.filters import CommandStart, Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from typing import Optional, Dict, Any, Callable, Awaitable
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional, Union
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from db.dal import user_dal
|
||||
|
||||
from db.database import add_user_if_not_exists, update_user_language_code
|
||||
from bot.keyboards.inline.user_keyboards import get_main_menu_inline_keyboard, get_language_selection_keyboard
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
@@ -13,228 +15,272 @@ from bot.services.referral_service import ReferralService
|
||||
from bot.services.promo_code_service import PromoCodeService
|
||||
from config.settings import Settings
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
|
||||
router = Router(name="user_start_router")
|
||||
|
||||
|
||||
async def send_main_menu(message_or_callback: types.Message
|
||||
| types.CallbackQuery,
|
||||
async def send_main_menu(target_event: Union[types.Message,
|
||||
types.CallbackQuery],
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
show_trial_button_flag: bool,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
is_edit: bool = False):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
answered_callback_internally = False
|
||||
|
||||
user_id = target_event.from_user.id
|
||||
user_full_name = target_event.from_user.full_name
|
||||
|
||||
if not i18n:
|
||||
logging.error("i18n_instance missing in send_main_menu")
|
||||
target_mc_for_error = message_or_callback if isinstance(
|
||||
message_or_callback,
|
||||
types.Message) else message_or_callback.message
|
||||
error_text_fallback = "Error: Language service unavailable."
|
||||
if target_mc_for_error:
|
||||
logging.error(
|
||||
f"i18n_instance missing in send_main_menu for user {user_id}")
|
||||
err_msg_fallback = "Error: Language service unavailable. Please try again later."
|
||||
if isinstance(target_event, types.CallbackQuery):
|
||||
try:
|
||||
await target_mc_for_error.answer(error_text_fallback)
|
||||
except Exception as e_ans:
|
||||
logging.error(
|
||||
f"Failed to send error message in send_main_menu: {e_ans}")
|
||||
if isinstance(message_or_callback, types.CallbackQuery):
|
||||
await message_or_callback.answer()
|
||||
answered_callback_internally = True
|
||||
await target_event.answer(err_msg_fallback, show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
elif isinstance(target_event, types.Message) and hasattr(
|
||||
target_event, 'chat') and target_event.chat:
|
||||
try:
|
||||
await target_event.chat.send_message(err_msg_fallback)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
user_full_name = message_or_callback.from_user.full_name
|
||||
text = _(key="main_menu_greeting", user_name=user_full_name)
|
||||
reply_markup: Optional[
|
||||
InlineKeyboardMarkup] = get_main_menu_inline_keyboard(
|
||||
current_lang, i18n, settings, show_trial_button_flag)
|
||||
target_message: Optional[types.Message] = None
|
||||
if isinstance(message_or_callback, types.Message):
|
||||
target_message = message_or_callback
|
||||
elif isinstance(message_or_callback, types.CallbackQuery):
|
||||
target_message = message_or_callback.message
|
||||
|
||||
if not target_message:
|
||||
show_trial_button_in_menu = False
|
||||
if settings.TRIAL_ENABLED:
|
||||
if hasattr(
|
||||
subscription_service, 'has_had_any_subscription') and callable(
|
||||
getattr(subscription_service, 'has_had_any_subscription')):
|
||||
if not await subscription_service.has_had_any_subscription(
|
||||
session, user_id):
|
||||
show_trial_button_in_menu = True
|
||||
else:
|
||||
logging.error(
|
||||
"Method has_had_any_subscription is missing in SubscriptionService for send_main_menu!"
|
||||
)
|
||||
|
||||
text = _(key="main_menu_greeting", user_name=user_full_name)
|
||||
reply_markup = get_main_menu_inline_keyboard(current_lang, i18n, settings,
|
||||
show_trial_button_in_menu)
|
||||
|
||||
target_message_obj: Optional[types.Message] = None
|
||||
if isinstance(target_event, types.Message):
|
||||
target_message_obj = target_event
|
||||
elif isinstance(target_event,
|
||||
types.CallbackQuery) and target_event.message:
|
||||
target_message_obj = target_event.message
|
||||
|
||||
if not target_message_obj:
|
||||
logging.error(
|
||||
f"send_main_menu: target_message is None for event from user {message_or_callback.from_user.id}."
|
||||
f"send_main_menu: target_message_obj is None for event from user {user_id}."
|
||||
)
|
||||
if isinstance(
|
||||
message_or_callback,
|
||||
types.CallbackQuery) and not answered_callback_internally:
|
||||
await message_or_callback.answer("Error displaying menu.")
|
||||
answered_callback_internally = True
|
||||
if isinstance(target_event, types.CallbackQuery):
|
||||
await target_event.answer(_("error_displaying_menu"),
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
if is_edit:
|
||||
await target_message.edit_text(text, reply_markup=reply_markup)
|
||||
await target_message_obj.edit_text(text, reply_markup=reply_markup)
|
||||
else:
|
||||
await target_message.answer(text, reply_markup=reply_markup)
|
||||
await target_message_obj.answer(text, reply_markup=reply_markup)
|
||||
|
||||
if isinstance(
|
||||
message_or_callback,
|
||||
types.CallbackQuery) and not answered_callback_internally:
|
||||
await message_or_callback.answer()
|
||||
answered_callback_internally = True
|
||||
if isinstance(target_event, types.CallbackQuery):
|
||||
await target_event.answer()
|
||||
except Exception as e_send_edit:
|
||||
logging.warning(
|
||||
f"Failed to send/edit main menu (user: {message_or_callback.from_user.id}): {e_send_edit}."
|
||||
f"Failed to send/edit main menu (user: {user_id}, is_edit: {is_edit}): {type(e_send_edit).__name__} - {e_send_edit}."
|
||||
)
|
||||
if is_edit:
|
||||
if is_edit and target_message_obj and hasattr(
|
||||
target_message_obj, 'chat') and target_message_obj.chat:
|
||||
try:
|
||||
await target_message.answer(text, reply_markup=reply_markup)
|
||||
await target_message_obj.chat.send_message(
|
||||
text, reply_markup=reply_markup)
|
||||
except Exception as e_send_new:
|
||||
logging.error(
|
||||
f"Also failed to send new main menu message: {e_send_new}")
|
||||
if isinstance(
|
||||
message_or_callback,
|
||||
types.CallbackQuery) and not answered_callback_internally:
|
||||
await message_or_callback.answer()
|
||||
answered_callback_internally = True
|
||||
|
||||
if isinstance(message_or_callback,
|
||||
types.CallbackQuery) and not answered_callback_internally:
|
||||
logging.warning(
|
||||
f"Callback {message_or_callback.id} was not answered in send_main_menu main logic paths."
|
||||
)
|
||||
await message_or_callback.answer()
|
||||
f"Also failed to send new main menu message for user {user_id}: {e_send_new}"
|
||||
)
|
||||
if isinstance(target_event, types.CallbackQuery):
|
||||
await target_event.answer(
|
||||
_("error_occurred_try_again") if is_edit else None)
|
||||
|
||||
|
||||
@router.message(CommandStart())
|
||||
async def start_command_handler(message: types.Message, state: FSMContext,
|
||||
settings: Settings, i18n_data: dict,
|
||||
async def start_command_handler(message: types.Message,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
bot: Bot):
|
||||
|
||||
session: AsyncSession,
|
||||
command: Optional[CommandStart] = None):
|
||||
await state.clear()
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n_instance not found")
|
||||
await message.answer("Language service error.")
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
user_id = message.from_user.id
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
user = message.from_user
|
||||
user_id = user.id
|
||||
|
||||
referred_by_user_id: Optional[int] = None
|
||||
args = message.text.split()
|
||||
if len(args) > 1 and args[0] == "/start":
|
||||
if command and command.args:
|
||||
arg_payload = command.args
|
||||
if arg_payload.startswith("ref_"):
|
||||
try:
|
||||
potential_referrer_id_str = arg_payload.split("_")[1]
|
||||
if potential_referrer_id_str.isdigit():
|
||||
potential_referrer_id = int(potential_referrer_id_str)
|
||||
if potential_referrer_id != user_id:
|
||||
referred_by_user_id = potential_referrer_id
|
||||
except (IndexError, ValueError) as e:
|
||||
logging.warning(
|
||||
f"Could not parse referral from /start args '{arg_payload}': {e}"
|
||||
)
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
if not db_user:
|
||||
user_data_to_create = {
|
||||
"user_id": user_id,
|
||||
"username": user.username,
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"language_code": current_lang,
|
||||
"referred_by_id": referred_by_user_id,
|
||||
"registration_date": datetime.now(timezone.utc)
|
||||
}
|
||||
try:
|
||||
referral_param = args[1]
|
||||
if referral_param.startswith("ref_") and referral_param.split(
|
||||
"_")[1].isdigit():
|
||||
potential_referrer_id = int(referral_param.split("_")[1])
|
||||
if potential_referrer_id != user_id:
|
||||
referred_by_user_id = potential_referrer_id
|
||||
except (ValueError, IndexError) as e:
|
||||
logging.warning(f"Could not parse referral: '{args[1]}' - {e}")
|
||||
db_op_success, was_new_bot_user = await add_user_if_not_exists(
|
||||
user_id=user_id,
|
||||
username=message.from_user.username,
|
||||
first_name=message.from_user.first_name,
|
||||
last_name=message.from_user.last_name,
|
||||
lang_code=current_lang,
|
||||
referred_by_id=referred_by_user_id)
|
||||
if not db_op_success:
|
||||
await message.answer(_("error_occurred_processing_request"))
|
||||
return
|
||||
if referred_by_user_id:
|
||||
logging.info(
|
||||
f"User {user_id} started with referral from {referred_by_user_id}."
|
||||
)
|
||||
await message.answer(
|
||||
_(key="welcome", user_name=message.from_user.full_name))
|
||||
show_trial_button_in_menu = False
|
||||
if settings.TRIAL_ENABLED:
|
||||
if not await subscription_service.has_had_any_subscription(user_id):
|
||||
show_trial_button_in_menu = True
|
||||
logging.info(f"User {user_id} is eligible for a trial button.")
|
||||
db_user = await user_dal.create_user(session, user_data_to_create)
|
||||
|
||||
else:
|
||||
logging.info(
|
||||
f"User {user_id} not eligible for trial button (already had a subscription)."
|
||||
f"New user {user_id} added to session. Referred by: {referred_by_user_id or 'N/A'}."
|
||||
)
|
||||
except Exception as e_create:
|
||||
|
||||
logging.error(
|
||||
f"Failed to add new user {user_id} to session: {e_create}",
|
||||
exc_info=True)
|
||||
await message.answer(_("error_occurred_processing_request"))
|
||||
return
|
||||
else:
|
||||
logging.info(f"Trial period is disabled in settings. No trial button.")
|
||||
update_payload = {}
|
||||
if db_user.language_code != current_lang:
|
||||
update_payload["language_code"] = current_lang
|
||||
if referred_by_user_id and db_user.referred_by_id is None:
|
||||
update_payload["referred_by_id"] = referred_by_user_id
|
||||
if user.username != db_user.username:
|
||||
update_payload["username"] = user.username
|
||||
if user.first_name != db_user.first_name:
|
||||
update_payload["first_name"] = user.first_name
|
||||
if user.last_name != db_user.last_name:
|
||||
update_payload["last_name"] = user.last_name
|
||||
|
||||
if update_payload:
|
||||
try:
|
||||
await user_dal.update_user(session, user_id, update_payload)
|
||||
|
||||
logging.info(
|
||||
f"Updated existing user {user_id} in session: {update_payload}"
|
||||
)
|
||||
except Exception as e_update:
|
||||
|
||||
logging.error(
|
||||
f"Failed to update existing user {user_id} in session: {e_update}",
|
||||
exc_info=True)
|
||||
|
||||
await message.answer(_(key="welcome", user_name=user.full_name))
|
||||
await send_main_menu(message,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=show_trial_button_in_menu)
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=False)
|
||||
|
||||
|
||||
@router.message(Command("language"))
|
||||
@router.callback_query(F.data == "main_action:language")
|
||||
async def language_command_handler(event: types.Message | types.CallbackQuery,
|
||||
i18n_data: dict, settings: Settings):
|
||||
|
||||
async def language_command_handler(
|
||||
event: Union[types.Message, types.CallbackQuery],
|
||||
i18n_data: dict,
|
||||
settings: Settings,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
target_message_for_reply: Optional[types.Message] = None
|
||||
is_callback = isinstance(event, types.CallbackQuery)
|
||||
answered_callback = False
|
||||
if is_callback:
|
||||
await event.answer()
|
||||
answered_callback = True
|
||||
target_message_for_reply = event.message
|
||||
else:
|
||||
target_message_for_reply = event
|
||||
if not i18n:
|
||||
logging.error("i18n instance is missing in language_command_handler.")
|
||||
error_message_text = "Language service error."
|
||||
if target_message_for_reply:
|
||||
await target_message_for_reply.answer(error_message_text)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
text_to_send = _(key="choose_language")
|
||||
reply_markup_to_send = get_language_selection_keyboard(i18n, current_lang)
|
||||
if not target_message_for_reply:
|
||||
logging.warning("language_command_handler: No target message.")
|
||||
reply_markup = get_language_selection_keyboard(i18n, current_lang)
|
||||
|
||||
target_message_obj = event.message if isinstance(
|
||||
event, types.CallbackQuery) else event
|
||||
if not target_message_obj:
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer(_("error_occurred_try_again"), show_alert=True)
|
||||
return
|
||||
if is_callback:
|
||||
try:
|
||||
await target_message_for_reply.edit_text(
|
||||
text_to_send, reply_markup=reply_markup_to_send)
|
||||
except Exception as e:
|
||||
logging.info(
|
||||
f"Could not edit for lang selection: {e}. Sending new.")
|
||||
await target_message_for_reply.answer(
|
||||
text_to_send, reply_markup=reply_markup_to_send)
|
||||
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
if event.message:
|
||||
try:
|
||||
await event.message.edit_text(text_to_send,
|
||||
reply_markup=reply_markup)
|
||||
except Exception:
|
||||
await target_message_obj.answer(text_to_send,
|
||||
reply_markup=reply_markup)
|
||||
await event.answer()
|
||||
else:
|
||||
await target_message_for_reply.answer(
|
||||
text_to_send, reply_markup=reply_markup_to_send)
|
||||
await target_message_obj.answer(text_to_send,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("set_lang_"))
|
||||
async def select_language_callback_handler(
|
||||
callback: types.CallbackQuery, i18n_data: dict, settings: Settings,
|
||||
subscription_service: SubscriptionService):
|
||||
subscription_service: SubscriptionService, session: AsyncSession):
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Language service error.", show_alert=True)
|
||||
await callback.answer("Service error or message context lost.",
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
lang_code = callback.data.split("_")[2]
|
||||
except IndexError:
|
||||
await callback.answer("Error processing language selection.",
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
lang_code = callback.data.split("_")[2]
|
||||
user_id = callback.from_user.id
|
||||
await update_user_language_code(user_id, lang_code)
|
||||
i18n_data["current_language"] = lang_code
|
||||
_ = lambda key, **kwargs: i18n.gettext(lang_code, key, **kwargs)
|
||||
try:
|
||||
updated = await user_dal.update_user_language(session, user_id,
|
||||
lang_code)
|
||||
if updated:
|
||||
|
||||
await callback.answer(_(key="language_set_alert"))
|
||||
i18n_data["current_language"] = lang_code
|
||||
_ = lambda key, **kwargs: i18n.gettext(lang_code, key, **kwargs)
|
||||
await callback.answer(_(key="language_set_alert"))
|
||||
logging.info(
|
||||
f"User {user_id} language updated to {lang_code} in session.")
|
||||
else:
|
||||
await callback.answer("Could not set language.", show_alert=True)
|
||||
return
|
||||
except Exception as e_lang_update:
|
||||
|
||||
show_trial_button_after_lang_change = False
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
user_id):
|
||||
show_trial_button_after_lang_change = True
|
||||
|
||||
await send_main_menu(
|
||||
callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=show_trial_button_after_lang_change,
|
||||
is_edit=True)
|
||||
logging.error(
|
||||
f"Error updating lang for user {user_id}: {e_lang_update}",
|
||||
exc_info=True)
|
||||
await callback.answer("Error setting language.", show_alert=True)
|
||||
return
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("main_action:"))
|
||||
@@ -242,44 +288,48 @@ async def main_action_callback_handler(
|
||||
callback: types.CallbackQuery, state: FSMContext, settings: Settings,
|
||||
i18n_data: dict, bot: Bot, subscription_service: SubscriptionService,
|
||||
referral_service: ReferralService, panel_service: PanelApiService,
|
||||
promo_code_service: PromoCodeService):
|
||||
|
||||
promo_code_service: PromoCodeService, session: AsyncSession):
|
||||
action = callback.data.split(":")[1]
|
||||
user_id = callback.from_user.id
|
||||
|
||||
from . import subscription as user_subscription_handlers
|
||||
from . import referral as user_referral_handlers
|
||||
from . import promo_user as user_promo_handlers
|
||||
from . import trial_handler as user_trial_handlers
|
||||
|
||||
if not callback.message:
|
||||
logging.error(f"Callback {callback.id} no message for {action}")
|
||||
await callback.answer("Error.")
|
||||
await callback.answer("Error: message context lost.", show_alert=True)
|
||||
return
|
||||
|
||||
if action == "subscribe":
|
||||
await user_subscription_handlers.display_subscription_options(
|
||||
callback, i18n_data, settings)
|
||||
callback, i18n_data, settings, session)
|
||||
elif action == "my_subscription":
|
||||
|
||||
await user_subscription_handlers.my_subscription_command_handler(
|
||||
callback, i18n_data, settings, panel_service, subscription_service)
|
||||
callback, i18n_data, settings, panel_service, subscription_service,
|
||||
session, bot)
|
||||
elif action == "referral":
|
||||
await user_referral_handlers.referral_command_handler(
|
||||
callback, settings, i18n_data, referral_service, bot)
|
||||
callback, settings, i18n_data, referral_service, bot, session)
|
||||
elif action == "apply_promo":
|
||||
await user_promo_handlers.prompt_promo_code_input(
|
||||
callback, state, i18n_data, settings)
|
||||
callback, state, i18n_data, settings, session)
|
||||
elif action == "request_trial":
|
||||
await user_trial_handlers.request_trial_confirmation_handler(
|
||||
callback, settings, i18n_data, subscription_service)
|
||||
callback, settings, i18n_data, subscription_service, session)
|
||||
elif action == "language":
|
||||
|
||||
await language_command_handler(callback, i18n_data, settings)
|
||||
elif action == "back_to_main":
|
||||
show_trial_button_on_back = False
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
callback.from_user.id):
|
||||
show_trial_button_on_back = True
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=show_trial_button_on_back,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
else:
|
||||
await callback.answer("Unknown action.", show_alert=True)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
_ = lambda key, **kwargs: i18n.gettext(
|
||||
i18n_data.get("current_language"), key, **kw) if i18n else key
|
||||
await callback.answer(_("main_menu_unknown_action"), show_alert=True)
|
||||
|
||||
+259
-224
@@ -1,15 +1,12 @@
|
||||
import logging
|
||||
import aiosqlite
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Optional, Dict, Any, Union
|
||||
from datetime import datetime, timezone
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from db.database import add_payment_record, get_db_connection_manager, _setup_db_connection
|
||||
from db.dal import payment_dal
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_subscription_options_keyboard, get_confirm_subscription_keyboard,
|
||||
get_payment_url_keyboard, get_back_to_main_menu_markup)
|
||||
@@ -21,307 +18,345 @@ from bot.middlewares.i18n import JsonI18n
|
||||
router = Router(name="user_subscription_router")
|
||||
|
||||
|
||||
async def display_subscription_options(message_or_callback: types.Message
|
||||
| types.CallbackQuery, i18n_data: dict,
|
||||
settings: Settings):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
async def display_subscription_options(event: Union[types.Message,
|
||||
types.CallbackQuery],
|
||||
i18n_data: dict, settings: Settings,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing in display_subscription_options")
|
||||
|
||||
target_msg = message_or_callback.message if isinstance(
|
||||
message_or_callback, types.CallbackQuery) else message_or_callback
|
||||
if target_msg: await target_msg.answer("Language service error.")
|
||||
if isinstance(message_or_callback, types.CallbackQuery):
|
||||
await message_or_callback.answer()
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
if not i18n:
|
||||
err_msg = "Language service error."
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer(err_msg, show_alert=True)
|
||||
elif isinstance(event, types.Message):
|
||||
await event.answer(err_msg)
|
||||
return
|
||||
|
||||
get_translation = lambda key, **kwargs: i18n.gettext(
|
||||
current_lang, key, **kwargs)
|
||||
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
|
||||
text_content = get_text("select_subscription_period"
|
||||
) if settings.subscription_options else get_text(
|
||||
"no_subscription_options_available")
|
||||
|
||||
text = get_translation(
|
||||
"select_subscription_period"
|
||||
) if settings.subscription_options else get_translation(
|
||||
"no_subscription_options_available")
|
||||
reply_markup = get_subscription_options_keyboard(
|
||||
settings.subscription_options, currency_symbol_val, current_lang,
|
||||
i18n) if settings.subscription_options else None
|
||||
settings.subscription_options, currency_symbol_val, current_lang, i18n
|
||||
) if settings.subscription_options else get_back_to_main_menu_markup(
|
||||
current_lang, i18n)
|
||||
|
||||
target_message = message_or_callback.message if isinstance(
|
||||
message_or_callback, types.CallbackQuery) else message_or_callback
|
||||
answered_callback = False
|
||||
target_message_obj = event.message if isinstance(
|
||||
event, types.CallbackQuery) else event
|
||||
if not target_message_obj:
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer(get_text("error_occurred_try_again"),
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
if isinstance(message_or_callback, types.CallbackQuery):
|
||||
|
||||
await message_or_callback.answer()
|
||||
answered_callback = True
|
||||
|
||||
if target_message:
|
||||
if isinstance(message_or_callback, types.CallbackQuery):
|
||||
try:
|
||||
await target_message.edit_text(text, reply_markup=reply_markup)
|
||||
except Exception:
|
||||
await target_message.answer(text, reply_markup=reply_markup)
|
||||
else:
|
||||
await target_message.answer(text, reply_markup=reply_markup)
|
||||
elif isinstance(message_or_callback, types.Message):
|
||||
await message_or_callback.answer(text, reply_markup=reply_markup)
|
||||
|
||||
if isinstance(message_or_callback,
|
||||
types.CallbackQuery) and not answered_callback:
|
||||
await message_or_callback.answer()
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
try:
|
||||
await target_message_obj.edit_text(text_content,
|
||||
reply_markup=reply_markup)
|
||||
except Exception:
|
||||
await target_message_obj.answer(text_content,
|
||||
reply_markup=reply_markup)
|
||||
await event.answer()
|
||||
else:
|
||||
await target_message_obj.answer(text_content,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("subscribe_period:"))
|
||||
async def select_subscription_period_callback_handler(
|
||||
callback: types.CallbackQuery, state: FSMContext, settings: Settings,
|
||||
i18n_data: dict):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error(
|
||||
"i18n missing in select_subscription_period_callback_handler")
|
||||
await callback.answer("Service error. Please try again.",
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer(get_text("error_occurred_try_again"),
|
||||
show_alert=True)
|
||||
return
|
||||
get_translation = lambda key, **kwargs: i18n.gettext(
|
||||
current_lang, key, **kwargs)
|
||||
|
||||
try:
|
||||
months = int(callback.data.split(":")[-1])
|
||||
except ValueError:
|
||||
logging.error(f"Invalid sub period: {callback.data}")
|
||||
await callback.answer(get_translation("error_try_again"),
|
||||
show_alert=True)
|
||||
except (ValueError, IndexError):
|
||||
logging.error(
|
||||
f"Invalid subscription period in callback_data: {callback.data}")
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
price = settings.subscription_options.get(months)
|
||||
if price is None:
|
||||
logging.error(f"Price not found for {months} months subscription.")
|
||||
await callback.answer(get_translation("error_try_again"),
|
||||
show_alert=True)
|
||||
price_rub = settings.subscription_options.get(months)
|
||||
if price_rub is None:
|
||||
logging.error(
|
||||
f"Price not found for {months} months subscription period in settings.subscription_options."
|
||||
)
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
|
||||
confirmation_text = get_translation("confirm_subscription_prompt",
|
||||
months=months,
|
||||
price=price,
|
||||
currency_symbol=currency_symbol_val)
|
||||
reply_markup = get_confirm_subscription_keyboard(months, price,
|
||||
confirmation_text_content = get_text("confirm_subscription_prompt",
|
||||
months=months,
|
||||
price=f"{price_rub:.2f}",
|
||||
currency_symbol=currency_symbol_val)
|
||||
reply_markup = get_confirm_subscription_keyboard(months, price_rub,
|
||||
currency_symbol_val,
|
||||
current_lang, i18n)
|
||||
|
||||
if callback.message:
|
||||
try:
|
||||
await callback.message.edit_text(confirmation_text,
|
||||
reply_markup=reply_markup)
|
||||
except Exception as e:
|
||||
logging.warning(f"Edit failed: {e}")
|
||||
await callback.message.answer(confirmation_text,
|
||||
reply_markup=reply_markup)
|
||||
try:
|
||||
await callback.message.edit_text(confirmation_text_content,
|
||||
reply_markup=reply_markup)
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Edit message for subscription confirmation failed: {e_edit}. Sending new one."
|
||||
)
|
||||
await callback.message.answer(confirmation_text_content,
|
||||
reply_markup=reply_markup)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("confirm_sub:"))
|
||||
async def confirm_subscription_callback_handler(
|
||||
callback: types.CallbackQuery, state: FSMContext, settings: Settings,
|
||||
i18n_data: dict, yookassa_service: YooKassaService):
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict,
|
||||
yookassa_service: YooKassaService, session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
if not i18n:
|
||||
logging.error("i18n missing")
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
return
|
||||
get_translation = lambda key, **kwargs: i18n.gettext(
|
||||
current_lang, key, **kwargs)
|
||||
if not yookassa_service or not yookassa_service.configured:
|
||||
logging.error("YooKassa service missing or not configured")
|
||||
await callback.message.edit_text(
|
||||
get_translation("payment_service_unavailable")
|
||||
) if callback.message else None
|
||||
await callback.answer(get_translation("payment_service_unavailable"),
|
||||
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
if not i18n or not callback.message:
|
||||
|
||||
await callback.answer(get_text("error_occurred_try_again"),
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
if not yookassa_service or not yookassa_service.configured:
|
||||
logging.error("YooKassa service is not configured or unavailable.")
|
||||
target_msg_edit = callback.message
|
||||
await target_msg_edit.edit_text(get_text("payment_service_unavailable")
|
||||
)
|
||||
await callback.answer(get_text("payment_service_unavailable_alert"),
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
_, data_payload = callback.data.split(":", 1)
|
||||
months_str, price_str = data_payload.split(":")
|
||||
months = int(months_str)
|
||||
price = float(price_str)
|
||||
except ValueError:
|
||||
logging.error(f"Invalid confirm data: {callback.data}")
|
||||
await callback.answer(get_translation("error_try_again"),
|
||||
show_alert=True)
|
||||
price_rub = float(price_str)
|
||||
except (ValueError, IndexError):
|
||||
logging.error(
|
||||
f"Invalid confirmation data in callback: {callback.data}")
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
user_id = callback.from_user.id
|
||||
description = get_translation("payment_description_subscription",
|
||||
months=months)
|
||||
currency = settings.DEFAULT_CURRENCY_SYMBOL
|
||||
payment_metadata = {
|
||||
|
||||
payment_description = get_text("payment_description_subscription",
|
||||
months=months)
|
||||
currency_code_for_yk = "RUB"
|
||||
|
||||
payment_record_data = {
|
||||
"user_id": user_id,
|
||||
"amount": price_rub,
|
||||
"currency": currency_code_for_yk,
|
||||
"status": "pending_yookassa",
|
||||
"description": payment_description,
|
||||
"subscription_duration_months": months,
|
||||
}
|
||||
db_payment_record = None
|
||||
try:
|
||||
db_payment_record = await payment_dal.create_payment_record(
|
||||
session, payment_record_data)
|
||||
await session.commit()
|
||||
logging.info(
|
||||
f"Payment record {db_payment_record.payment_id} created for user {user_id} with status 'pending_yookassa'."
|
||||
)
|
||||
except Exception as e_db_payment:
|
||||
await session.rollback()
|
||||
logging.error(
|
||||
f"Failed to create payment record in DB for user {user_id}: {e_db_payment}",
|
||||
exc_info=True)
|
||||
await callback.message.edit_text(
|
||||
get_text("error_creating_payment_record"))
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
if not db_payment_record:
|
||||
await callback.message.edit_text(
|
||||
get_text("error_creating_payment_record"))
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
yookassa_metadata = {
|
||||
"user_id": str(user_id),
|
||||
"subscription_months": str(months),
|
||||
"description": description
|
||||
"payment_db_id": str(db_payment_record.payment_id),
|
||||
}
|
||||
payment_db_id = await add_payment_record(user_id, None, None, price,
|
||||
currency, "pending_creation",
|
||||
description, months, None)
|
||||
if not payment_db_id:
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
get_translation("error_creating_payment_record"))
|
||||
await callback.answer(show_alert=True)
|
||||
return
|
||||
payment_metadata["payment_db_id"] = str(payment_db_id)
|
||||
payment_response = await yookassa_service.create_payment(
|
||||
price, currency, description, payment_metadata)
|
||||
receipt_email_for_yk = settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL
|
||||
|
||||
if callback.message:
|
||||
if payment_response and payment_response.get("confirmation_url"):
|
||||
async with get_db_connection_manager() as db:
|
||||
await _setup_db_connection(db)
|
||||
await db.execute(
|
||||
"UPDATE payments SET yookassa_payment_id = ?, idempotence_key = ?, status = ? WHERE payment_id = ?",
|
||||
(payment_response["id"],
|
||||
payment_response.get("idempotence_key"),
|
||||
payment_response["status"], payment_db_id))
|
||||
await db.commit()
|
||||
payment_response_yk = await yookassa_service.create_payment(
|
||||
amount=price_rub,
|
||||
currency=currency_code_for_yk,
|
||||
description=payment_description,
|
||||
metadata=yookassa_metadata,
|
||||
receipt_email=receipt_email_for_yk)
|
||||
|
||||
if payment_response_yk and payment_response_yk.get("confirmation_url"):
|
||||
try:
|
||||
await payment_dal.update_payment_status_by_db_id(
|
||||
session,
|
||||
payment_db_id=db_payment_record.payment_id,
|
||||
new_status=payment_response_yk.get("status", "pending"),
|
||||
yk_payment_id=payment_response_yk.get("id"))
|
||||
await session.commit()
|
||||
except Exception as e_db_update_ykid:
|
||||
await session.rollback()
|
||||
logging.error(
|
||||
f"Failed to update payment record {db_payment_record.payment_id} with YK ID: {e_db_update_ykid}",
|
||||
exc_info=True)
|
||||
await callback.message.edit_text(
|
||||
get_translation(key="payment_link_message", months=months),
|
||||
reply_markup=get_payment_url_keyboard(
|
||||
payment_response["confirmation_url"], current_lang, i18n),
|
||||
disable_web_page_preview=False)
|
||||
else:
|
||||
async with get_db_connection_manager() as db:
|
||||
await _setup_db_connection(db)
|
||||
await db.execute(
|
||||
"UPDATE payments SET status = ? WHERE payment_id = ?",
|
||||
("failed_creation", payment_db_id))
|
||||
await db.commit()
|
||||
await callback.message.edit_text(
|
||||
get_translation("error_payment_gateway"))
|
||||
get_text("error_payment_gateway_link_failed"))
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
get_text(key="payment_link_message", months=months),
|
||||
reply_markup=get_payment_url_keyboard(
|
||||
payment_response_yk["confirmation_url"], current_lang, i18n),
|
||||
disable_web_page_preview=False)
|
||||
else:
|
||||
try:
|
||||
await payment_dal.update_payment_status_by_db_id(
|
||||
session, db_payment_record.payment_id, "failed_creation")
|
||||
await session.commit()
|
||||
except Exception as e_db_fail_create:
|
||||
await session.rollback()
|
||||
logging.error(
|
||||
f"Additionally failed to update payment record to 'failed_creation': {e_db_fail_create}",
|
||||
exc_info=True)
|
||||
|
||||
logging.error(
|
||||
f"Failed to create payment in YooKassa for user {user_id}, payment_db_id {db_payment_record.payment_id}. Response: {payment_response_yk}"
|
||||
)
|
||||
await callback.message.edit_text(get_text("error_payment_gateway"))
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "main_action:subscribe")
|
||||
async def reshow_subscription_options_callback(callback: types.CallbackQuery,
|
||||
i18n_data: dict,
|
||||
settings: Settings):
|
||||
await display_subscription_options(callback, i18n_data, settings)
|
||||
settings: Settings,
|
||||
session: AsyncSession):
|
||||
await display_subscription_options(callback, i18n_data, settings, session)
|
||||
|
||||
|
||||
async def my_subscription_command_handler(
|
||||
message_event: types.Message | types.CallbackQuery, i18n_data: dict,
|
||||
event: Union[types.Message, types.CallbackQuery], i18n_data: dict,
|
||||
settings: Settings, panel_service: PanelApiService,
|
||||
subscription_service: SubscriptionService):
|
||||
target_message = message_event.message if isinstance(
|
||||
message_event, types.CallbackQuery) else message_event
|
||||
user = message_event.from_user
|
||||
if isinstance(message_event, types.CallbackQuery):
|
||||
await message_event.answer()
|
||||
subscription_service: SubscriptionService, session: AsyncSession,
|
||||
bot: Bot):
|
||||
target_message_obj = event.message if isinstance(
|
||||
event, types.CallbackQuery) else event
|
||||
user = event.from_user
|
||||
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer()
|
||||
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing")
|
||||
await target_message.answer("Lang error")
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
if not i18n or not target_message_obj:
|
||||
if isinstance(event, types.Message):
|
||||
await event.answer(get_text("error_occurred_try_again"))
|
||||
return
|
||||
get_translation = lambda key, **kwargs: i18n.gettext(
|
||||
current_lang, key, **kwargs)
|
||||
|
||||
if not panel_service or not subscription_service:
|
||||
logging.error("Services missing")
|
||||
await target_message.answer(
|
||||
get_translation("error_service_unavailable"))
|
||||
logging.error(
|
||||
"PanelService or SubscriptionService is missing in my_subscription_command_handler."
|
||||
)
|
||||
await target_message_obj.answer(get_text("error_service_unavailable"))
|
||||
return
|
||||
|
||||
active_sub = await subscription_service.get_active_subscription(user.id)
|
||||
sub_info_text = ""
|
||||
if active_sub:
|
||||
end_date_obj = active_sub.get('end_date')
|
||||
if isinstance(end_date_obj, str):
|
||||
try:
|
||||
end_date_obj = datetime.fromisoformat(
|
||||
end_date_obj.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
logging.warning(
|
||||
f"Could not parse date string '{end_date_obj}'.")
|
||||
end_date_obj = datetime.now(timezone.utc)
|
||||
active_sub_details = await subscription_service.get_active_subscription_details(
|
||||
session, user.id)
|
||||
|
||||
if not isinstance(end_date_obj, datetime):
|
||||
end_date_obj = datetime.now(timezone.utc)
|
||||
if end_date_obj.tzinfo is None:
|
||||
end_date_obj = end_date_obj.replace(tzinfo=timezone.utc)
|
||||
sub_info_text_content = ""
|
||||
if active_sub_details:
|
||||
end_date_obj = active_sub_details.get('end_date')
|
||||
days_left = 0
|
||||
if end_date_obj:
|
||||
if end_date_obj.tzinfo is None:
|
||||
end_date_obj = end_date_obj.replace(tzinfo=timezone.utc)
|
||||
days_left = (end_date_obj.date() - datetime.now().date()).days
|
||||
|
||||
today_date_utc = datetime.now(timezone.utc).date()
|
||||
end_date_only = end_date_obj.date()
|
||||
days_left = (end_date_only - today_date_utc).days
|
||||
actual_config_link = active_sub_details.get('config_link') or get_text(
|
||||
"config_link_not_available")
|
||||
|
||||
actual_config_link = get_translation("config_link_not_available")
|
||||
panel_user_uuid = active_sub.get('panel_user_uuid')
|
||||
if panel_user_uuid:
|
||||
panel_user_data = await panel_service.get_user_by_uuid(
|
||||
panel_user_uuid)
|
||||
if panel_user_data:
|
||||
if panel_user_data.get('subscriptionUrl'):
|
||||
actual_config_link = panel_user_data['subscriptionUrl']
|
||||
elif panel_user_data.get('shortUuid'):
|
||||
link = await panel_service.get_subscription_link(
|
||||
panel_user_data['shortUuid'])
|
||||
if link: actual_config_link = link
|
||||
traffic_limit_bytes = active_sub_details.get('traffic_limit_bytes')
|
||||
traffic_used_bytes = active_sub_details.get('traffic_used_bytes')
|
||||
|
||||
traffic_limit_gb = get_translation("traffic_unlimited")
|
||||
traffic_used_gb = get_translation("traffic_na")
|
||||
if active_sub.get('traffic_limit_bytes'
|
||||
) and active_sub['traffic_limit_bytes'] > 0:
|
||||
traffic_limit_gb = f"{active_sub['traffic_limit_bytes'] / (1024**3):.2f} GB"
|
||||
if active_sub.get('traffic_used_bytes') is not None:
|
||||
traffic_used_gb = f"{active_sub['traffic_used_bytes'] / (1024**3):.2f} GB"
|
||||
traffic_limit_gb_str = get_text("traffic_unlimited")
|
||||
if traffic_limit_bytes and traffic_limit_bytes > 0:
|
||||
traffic_limit_gb_str = f"{traffic_limit_bytes / (1024**3):.2f} GB"
|
||||
|
||||
sub_info_text = get_translation(
|
||||
traffic_used_gb_str = get_text("traffic_na")
|
||||
if traffic_used_bytes is not None:
|
||||
traffic_used_gb_str = f"{traffic_used_bytes / (1024**3):.2f} GB"
|
||||
|
||||
sub_info_text_content = get_text(
|
||||
"my_subscription_details",
|
||||
end_date=end_date_obj.strftime("%Y-%m-%d"),
|
||||
end_date=end_date_obj.strftime("%Y-%m-%d")
|
||||
if end_date_obj else "N/A",
|
||||
days_left=max(0, days_left),
|
||||
status=active_sub.get(
|
||||
'status_from_panel',
|
||||
get_translation('status_active')).capitalize(),
|
||||
status=active_sub_details.get(
|
||||
'status_from_panel', get_text('status_active')).capitalize(),
|
||||
config_link=actual_config_link,
|
||||
traffic_limit=traffic_limit_gb,
|
||||
traffic_used=traffic_used_gb)
|
||||
traffic_limit=traffic_limit_gb_str,
|
||||
traffic_used=traffic_used_gb_str)
|
||||
else:
|
||||
sub_info_text = get_translation("subscription_not_active")
|
||||
sub_info_text_content = get_text("subscription_not_active")
|
||||
logging.info(
|
||||
f"User {user.id} no active sub details for 'my_subscription'.")
|
||||
|
||||
reply_markup_val = get_back_to_main_menu_markup(current_lang, i18n)
|
||||
if isinstance(message_event,
|
||||
types.CallbackQuery) and message_event.message:
|
||||
|
||||
if isinstance(event, types.CallbackQuery) and event.message:
|
||||
try:
|
||||
await message_event.message.edit_text(
|
||||
sub_info_text,
|
||||
reply_markup=reply_markup_val,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True)
|
||||
except Exception as e:
|
||||
logging.warning(f"Edit 'my_sub' failed: {e}")
|
||||
await target_message.answer(sub_info_text,
|
||||
await event.message.edit_text(sub_info_text_content,
|
||||
reply_markup=reply_markup_val,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True)
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Edit 'my_subscription' failed: {e_edit}. Sending new message to chat {target_message_obj.chat.id}."
|
||||
)
|
||||
await bot.send_message(chat_id=target_message_obj.chat.id,
|
||||
text=sub_info_text_content,
|
||||
reply_markup=reply_markup_val,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True)
|
||||
else:
|
||||
await target_message_obj.answer(sub_info_text_content,
|
||||
reply_markup=reply_markup_val,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True)
|
||||
else:
|
||||
await target_message.answer(sub_info_text,
|
||||
reply_markup=reply_markup_val,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True)
|
||||
|
||||
|
||||
@router.message(Command("connect"))
|
||||
async def connect_command_handler(message: types.Message, i18n_data: dict,
|
||||
settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
subscription_service: SubscriptionService):
|
||||
"""Handles the /connect command, showing subscription info."""
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession, bot: Bot):
|
||||
logging.info(f"User {message.from_user.id} used /connect command.")
|
||||
await my_subscription_command_handler(message, i18n_data, settings,
|
||||
panel_service, subscription_service)
|
||||
panel_service, subscription_service,
|
||||
session, bot)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import logging
|
||||
from aiogram import Router, F, types, Bot
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import datetime
|
||||
|
||||
from config.settings import Settings
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
@@ -16,37 +16,44 @@ router = Router(name="user_trial_router")
|
||||
|
||||
async def request_trial_confirmation_handler(
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict,
|
||||
subscription_service: SubscriptionService):
|
||||
subscription_service: SubscriptionService, session: AsyncSession):
|
||||
user_id = callback.from_user.id
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error.", show_alert=True)
|
||||
await callback.answer(_("error_occurred_try_again"), show_alert=True)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
show_trial_btn_if_fail = False
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
user_id):
|
||||
show_trial_btn_if_fail = True
|
||||
|
||||
show_trial_btn_in_menu_if_fail = False
|
||||
if settings.TRIAL_ENABLED:
|
||||
if not await subscription_service.has_had_any_subscription(
|
||||
session, user_id):
|
||||
show_trial_btn_in_menu_if_fail = True
|
||||
|
||||
if not settings.TRIAL_ENABLED:
|
||||
await callback.message.edit_text(
|
||||
_("trial_feature_disabled"),
|
||||
reply_markup=get_main_menu_inline_keyboard(current_lang, i18n,
|
||||
settings,
|
||||
show_trial_btn_if_fail))
|
||||
settings, False))
|
||||
await callback.answer()
|
||||
return
|
||||
if await subscription_service.has_had_any_subscription(user_id):
|
||||
|
||||
if await subscription_service.has_had_any_subscription(session, user_id):
|
||||
await callback.message.edit_text(
|
||||
_("trial_already_had_subscription_or_trial"),
|
||||
reply_markup=get_main_menu_inline_keyboard(current_lang, i18n,
|
||||
settings, False))
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
traffic_gb_display = str(
|
||||
settings.TRIAL_TRAFFIC_LIMIT_GB
|
||||
) if settings.TRIAL_TRAFFIC_LIMIT_GB and settings.TRIAL_TRAFFIC_LIMIT_GB > 0 else _(
|
||||
"traffic_unlimited")
|
||||
|
||||
await callback.message.edit_text(
|
||||
text=_("trial_confirm_prompt",
|
||||
days=settings.TRIAL_DURATION_DAYS,
|
||||
@@ -59,55 +66,58 @@ async def request_trial_confirmation_handler(
|
||||
async def confirm_activate_trial_handler(
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
panel_service: PanelApiService, bot: Bot):
|
||||
panel_service: PanelApiService, session: AsyncSession):
|
||||
user_id = callback.from_user.id
|
||||
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error.", show_alert=True)
|
||||
await callback.answer(_("error_occurred_try_again"), show_alert=True)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
show_trial_button_after_action = False
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
user_id):
|
||||
show_trial_button_after_action = True
|
||||
|
||||
if not settings.TRIAL_ENABLED:
|
||||
await callback.answer(_("trial_feature_disabled"), show_alert=True)
|
||||
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=False,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
return
|
||||
if await subscription_service.has_had_any_subscription(user_id):
|
||||
if await subscription_service.has_had_any_subscription(session, user_id):
|
||||
await callback.answer(_("trial_already_had_subscription_or_trial"),
|
||||
show_alert=True)
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=False,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
return
|
||||
|
||||
activation_result = await subscription_service.activate_trial_subscription(
|
||||
user_id)
|
||||
session, user_id)
|
||||
|
||||
final_message_text_in_chat = ""
|
||||
show_trial_button_after_action = False
|
||||
|
||||
if activation_result and activation_result.get("activated"):
|
||||
await callback.answer(_("trial_activated_alert"), show_alert=True)
|
||||
|
||||
end_date = activation_result.get("end_date")
|
||||
config_link_for_trial = _("config_link_not_available")
|
||||
end_date_obj = activation_result.get("end_date")
|
||||
config_link_for_trial = activation_result.get("subscription_url") or _(
|
||||
"config_link_not_available")
|
||||
|
||||
if activation_result.get("subscription_url"):
|
||||
config_link_for_trial = activation_result["subscription_url"]
|
||||
elif activation_result.get("panel_short_uuid"):
|
||||
link = await panel_service.get_subscription_link(
|
||||
if config_link_for_trial == _(
|
||||
"config_link_not_available") and activation_result.get(
|
||||
"panel_short_uuid"):
|
||||
generated_link = await panel_service.get_subscription_link(
|
||||
activation_result["panel_short_uuid"])
|
||||
if link: config_link_for_trial = link
|
||||
if generated_link: config_link_for_trial = generated_link
|
||||
|
||||
traffic_gb_val = activation_result.get("traffic_gb",
|
||||
settings.TRIAL_TRAFFIC_LIMIT_GB)
|
||||
@@ -117,30 +127,34 @@ async def confirm_activate_trial_handler(
|
||||
final_message_text_in_chat = _(
|
||||
"trial_activated_details_message",
|
||||
days=activation_result.get("days", settings.TRIAL_DURATION_DAYS),
|
||||
end_date=end_date.strftime('%Y-%m-%d') if isinstance(
|
||||
end_date, datetime) else "N/A",
|
||||
end_date=end_date_obj.strftime('%Y-%m-%d') if isinstance(
|
||||
end_date_obj, datetime) else "N/A",
|
||||
config_link=config_link_for_trial,
|
||||
traffic_gb=traffic_display)
|
||||
show_trial_button_after_action = False
|
||||
else:
|
||||
message_key = activation_result.get(
|
||||
message_key_from_service = activation_result.get(
|
||||
"message_key", "trial_activation_failed"
|
||||
) if activation_result else "trial_activation_failed"
|
||||
final_message_text_in_chat = _(message_key)
|
||||
final_message_text_in_chat = _(message_key_from_service)
|
||||
await callback.answer(final_message_text_in_chat, show_alert=True)
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
session, user_id):
|
||||
show_trial_button_after_action = True
|
||||
|
||||
if callback.message:
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
final_message_text_in_chat,
|
||||
parse_mode="HTML",
|
||||
reply_markup=get_main_menu_inline_keyboard(
|
||||
current_lang, i18n, settings,
|
||||
show_trial_button_after_action),
|
||||
disable_web_page_preview=True)
|
||||
except Exception as e_edit:
|
||||
logging.warning(f"Could not edit trial result message: {e_edit}")
|
||||
await callback.message.answer(
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
final_message_text_in_chat,
|
||||
parse_mode="HTML",
|
||||
reply_markup=get_main_menu_inline_keyboard(
|
||||
current_lang, i18n, settings, show_trial_button_after_action),
|
||||
disable_web_page_preview=True)
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Could not edit trial result message: {e_edit}. Sending new one.")
|
||||
|
||||
if callback.message and hasattr(callback.message,
|
||||
'chat') and callback.message.chat:
|
||||
await callback.message.chat.send_message(
|
||||
final_message_text_in_chat,
|
||||
parse_mode="HTML",
|
||||
reply_markup=get_main_menu_inline_keyboard(
|
||||
@@ -152,13 +166,11 @@ async def confirm_activate_trial_handler(
|
||||
@router.callback_query(F.data == "main_action:cancel_trial")
|
||||
async def cancel_trial_activation(callback: types.CallbackQuery,
|
||||
settings: Settings, i18n_data: dict,
|
||||
subscription_service: SubscriptionService):
|
||||
show_trial_button_on_back = False
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
callback.from_user.id):
|
||||
show_trial_button_on_back = True
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession):
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=show_trial_button_on_back,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
|
||||
Reference in New Issue
Block a user