Migrating to Postgres from sqlite3

This commit is contained in:
machka
2025-05-20 18:41:32 +00:00
parent ac2f83c061
commit a56d804b58
38 changed files with 5017 additions and 4389 deletions
+238 -214
View File
@@ -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")
+76 -53
View File
@@ -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)
+53 -45
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+67 -55
View File
@@ -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)