Migrating to Postgres from sqlite3
This commit is contained in:
+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)
|
||||
|
||||
Reference in New Issue
Block a user