Add Docker build workflow and enhance admin functionalities

- Introduced a new GitHub Actions workflow for building and pushing the development Docker image.
- Added inline mode handling for user interactions, allowing users to share referral links and view statistics.
- Enhanced admin functionalities with new sections for user management, statistics, and promo code management.
- Implemented CSV export for logs and improved notification services for various events, including new user registrations and payment notifications.
- Updated localization files to support new features and commands.
This commit is contained in:
machka-pasla
2025-08-03 15:59:37 +03:00
parent 35d798a429
commit f0bbe90f80
23 changed files with 2270 additions and 549 deletions
+17 -1
View File
@@ -20,7 +20,7 @@ from bot.services.panel_api_service import PanelApiService
from bot.services.yookassa_service import YooKassaService
from bot.middlewares.i18n import JsonI18n
from config.settings import Settings
from bot.services.notification_service import notify_admin_new_payment
from bot.services.notification_service import notify_admin_new_payment, NotificationService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
payment_processing_lock = asyncio.Lock()
@@ -195,6 +195,22 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
f"Failed to send payment details message to user {user_id}: {e_notify}"
)
# Send notification about payment
try:
notification_service = NotificationService(bot, settings, i18n)
user = await user_dal.get_user_by_id(session, user_id)
await notification_service.notify_payment_received(
user_id=user_id,
amount=payment_value,
currency=settings.DEFAULT_CURRENCY_SYMBOL,
months=subscription_months,
payment_provider="yookassa", # This is specifically for YooKassa webhook
username=user.username if user else None
)
except Exception as e:
logging.error(f"Failed to send payment notification: {e}")
# Legacy notification for backwards compatibility
await notify_admin_new_payment(
bot,
settings,
+36 -1
View File
@@ -87,7 +87,8 @@ async def referral_command_handler(event: Union[types.Message,
referral_link=referral_link,
bonus_details=bonus_details_str)
reply_markup_val = get_back_to_main_menu_markup(current_lang, i18n)
from bot.keyboards.inline.user_keyboards import get_referral_link_keyboard
reply_markup_val = get_referral_link_keyboard(current_lang, i18n)
if isinstance(event, types.Message):
await event.answer(text,
@@ -106,3 +107,37 @@ async def referral_command_handler(event: Union[types.Message,
reply_markup=reply_markup_val,
disable_web_page_preview=True)
await event.answer()
@router.callback_query(F.data.startswith("referral_action:"))
async def referral_action_handler(callback: types.CallbackQuery, settings: Settings,
i18n_data: dict, referral_service: ReferralService,
bot: Bot, session: AsyncSession):
action = callback.data.split(":")[1]
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n = i18n_data.get("i18n_instance")
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
if action == "share_message":
try:
bot_info = await bot.get_me()
bot_username = bot_info.username
if not bot_username:
await callback.answer("Ошибка получения имени бота", show_alert=True)
return
inviter_user_id = callback.from_user.id
referral_link = referral_service.generate_referral_link(bot_username, inviter_user_id)
friend_message = _("referral_friend_message", referral_link=referral_link)
await callback.message.answer(
friend_message,
disable_web_page_preview=True
)
except Exception as e:
logging.error(f"Error in referral share message: {e}")
await callback.answer("Произошла ошибка", show_alert=True)
await callback.answer()
+70
View File
@@ -129,6 +129,8 @@ async def start_command_handler(message: types.Message,
user_id = user.id
referred_by_user_id: Optional[int] = None
promo_code_to_apply: Optional[str] = None
if command and command.args:
arg_payload = command.args
if arg_payload.startswith("ref_"):
@@ -142,6 +144,14 @@ async def start_command_handler(message: types.Message,
logging.warning(
f"Could not parse referral from /start args '{arg_payload}': {e}"
)
elif arg_payload.startswith("promo_"):
try:
promo_code_to_apply = arg_payload.split("_")[1]
logging.info(f"User {user_id} started with promo code: {promo_code_to_apply}")
except (IndexError, ValueError) as e:
logging.warning(
f"Could not parse promo code from /start args '{arg_payload}': {e}"
)
db_user = await user_dal.get_user_by_id(session, user_id)
if not db_user:
@@ -160,6 +170,19 @@ async def start_command_handler(message: types.Message,
logging.info(
f"New user {user_id} added to session. Referred by: {referred_by_user_id or 'N/A'}."
)
# Send notification about new user registration
try:
from bot.services.notification_service import NotificationService
notification_service = NotificationService(message.bot, settings, i18n)
await notification_service.notify_new_user_registration(
user_id=user_id,
username=user.username,
first_name=user.first_name,
referred_by_id=referred_by_user_id
)
except Exception as e:
logging.error(f"Failed to send new user notification: {e}")
except Exception as e_create:
logging.error(
@@ -194,6 +217,53 @@ async def start_command_handler(message: types.Message,
exc_info=True)
await message.answer(_(key="welcome", user_name=hd.quote(user.full_name)))
# Auto-apply promo code if provided via start parameter
if promo_code_to_apply:
try:
from bot.services.promo_code_service import PromoCodeService
promo_code_service = PromoCodeService()
success, result = await promo_code_service.apply_promo_code(
session, user_id, promo_code_to_apply, current_lang
)
if success:
await session.commit()
logging.info(f"Auto-applied promo code '{promo_code_to_apply}' for user {user_id}")
# Get updated subscription details
active = await subscription_service.get_active_subscription_details(session, user_id)
config_link = active.get("config_link") if active else None
config_link = config_link or _("config_link_not_available")
from datetime import datetime
new_end_date = result if isinstance(result, datetime) else None
promo_success_text = _(
"promo_code_applied_success_full",
end_date=(new_end_date.strftime("%d.%m.%Y %H:%M:%S") if new_end_date else "N/A"),
config_link=config_link,
)
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
await message.answer(
promo_success_text,
reply_markup=get_connect_and_main_keyboard(current_lang, i18n, settings, config_link),
parse_mode="HTML"
)
# Don't show main menu if promo was successfully applied
return
else:
await session.rollback()
logging.warning(f"Failed to auto-apply promo code '{promo_code_to_apply}' for user {user_id}: {result}")
# Continue to show main menu if promo failed
except Exception as e:
logging.error(f"Error auto-applying promo code '{promo_code_to_apply}' for user {user_id}: {e}")
await session.rollback()
await send_main_menu(message,
settings,
i18n_data,
+86 -13
View File
@@ -59,21 +59,94 @@ async def request_trial_confirmation_handler(
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")
# Directly activate trial without confirmation
activation_result = await subscription_service.activate_trial_subscription(
session, user_id
)
await callback.message.edit_text(
text=_(
"trial_confirm_prompt",
days=settings.TRIAL_DURATION_DAYS,
traffic_gb=traffic_gb_display,
),
reply_markup=get_trial_confirmation_keyboard(current_lang, i18n),
)
await callback.answer()
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_obj = activation_result.get("end_date")
config_link_for_trial = activation_result.get("subscription_url") or _(
"config_link_not_available"
)
traffic_gb_val = activation_result.get(
"traffic_gb", settings.TRIAL_TRAFFIC_LIMIT_GB
)
traffic_display = (
f"{traffic_gb_val} GB"
if traffic_gb_val and traffic_gb_val > 0
else _("traffic_unlimited")
)
final_message_text_in_chat = _(
"trial_activated_details_message",
days=activation_result.get("days", settings.TRIAL_DURATION_DAYS),
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,
)
# Send notification to admin about new trial
await notify_admin_new_trial(
callback.bot,
settings,
i18n,
user_id,
end_date_obj,
)
else:
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_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
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(
current_lang, i18n, settings, show_trial_button_after_action
),
disable_web_page_preview=True,
)
@router.callback_query(F.data == "trial_action:confirm_activate")