"""Async handlers for group events.""" from telebot.async_telebot import AsyncTeleBot from telebot import types from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from bot.database import get_db_session, User, Chat, UserChat from bot.logger import get_logger logger = get_logger(__name__) def register_group_handlers(bot: AsyncTeleBot) -> None: """Register all group event handlers.""" @bot.message_handler(content_types=['new_chat_members']) async def handle_new_member(message: types.Message) -> None: """Handle bot being added to a chat.""" # Check if bot was added bot_me = await bot.get_me() if not bot_me: return for member in message.new_chat_members: if member.id == bot_me.id: chat_id = message.chat.id chat_title: str = message.chat.title or "Unknown" logger.info(f"Bot added to chat: {chat_title} (ID: {chat_id})") async for db in get_db_session(): try: # Check if chat exists result = await db.execute(select(Chat).filter(Chat.chat_id == chat_id)) chat = result.scalar_one_or_none() if not chat: chat = Chat(chat_id=chat_id, chat_title=chat_title, bot_is_admin=False) db.add(chat) await db.commit() logger.info(f"Created new chat record: {chat_title} (ID: {chat_id})") # Check if bot is admin try: bot_me = await bot.get_me() if not bot_me: return bot_member = await bot.get_chat_member(chat_id, bot_me.id) is_admin = bot_member.status in ['administrator', 'creator'] chat.bot_is_admin = is_admin await db.commit() if not is_admin: logger.warning(f"Bot is not admin in chat: {chat_title} (ID: {chat_id})") # Request admin rights await bot.reply_to( message, "Привет! Мне нужны права администратора, чтобы видеть список участников чата. " "Пожалуйста, выдайте мне права администратора." ) else: logger.info(f"Bot is admin in chat: {chat_title} (ID: {chat_id})") # Sync users and show statistics await sync_chat_users(bot, db, chat_id) await show_statistics(bot, message.chat) except Exception as e: logger.error(f"Error checking admin status in chat {chat_id}: {e}", exc_info=True) # Bot might not have permission to check chat.bot_is_admin = False await db.commit() await bot.reply_to( message, "Привет! Мне нужны права администратора, чтобы видеть список участников чата. " "Пожалуйста, выдайте мне права администратора." ) finally: await db.close() break # NOTE: # Updates about the bot itself (когда бота повышают до админа / понижают) # приходят в поле `my_chat_member`, а не `chat_member`. # Для их обработки в pyTelegramBotAPI нужно использовать my_chat_member_handler. @bot.my_chat_member_handler() async def handle_chat_member_update(message: types.ChatMemberUpdated) -> None: """Handle my_chat_member updates (bot role changes, e.g. becoming admin).""" bot_me = await bot.get_me() if not bot_me: return if message.new_chat_member.user.id == bot_me.id: # Bot's status changed chat_id = message.chat.id chat_title: str = message.chat.title or "Unknown" new_status = message.new_chat_member.status logger.info(f"Bot status changed in chat {chat_title} (ID: {chat_id}): {new_status}") async for db in get_db_session(): try: result = await db.execute(select(Chat).filter(Chat.chat_id == chat_id)) chat = result.scalar_one_or_none() if not chat: chat = Chat(chat_id=chat_id, chat_title=chat_title, bot_is_admin=False) db.add(chat) # Check if bot became admin is_admin = message.new_chat_member.status in ['administrator', 'creator'] was_admin = chat.bot_is_admin if chat else False if chat: chat.bot_is_admin = is_admin chat.chat_title = chat_title await db.commit() if is_admin and not was_admin: logger.info(f"Bot promoted to admin in chat: {chat_title} (ID: {chat_id})") # Bot just became admin - sync users and show statistics await sync_chat_users(bot, db, chat_id) await show_statistics(bot, message.chat) elif not is_admin and was_admin: logger.warning(f"Bot demoted from admin in chat: {chat_title} (ID: {chat_id})") finally: await db.close() async def sync_chat_users(bot: AsyncTeleBot, db: AsyncSession, chat_id: int) -> None: """Sync users from chat with database.""" try: logger.debug(f"Syncing users for chat ID: {chat_id}") # Get all users who already shared birthday result = await db.execute(select(User)) existing_users = result.scalars().all() logger.debug(f"Found {len(existing_users)} users with birthdays to sync") synced_count = 0 # Try to check if they're in this chat and add relationships for user in existing_users: try: member = await bot.get_chat_member(chat_id, user.user_id) if member.status not in ['left', 'kicked']: # User is in chat - ensure relationship exists user_chat_result = await db.execute( select(UserChat).filter( UserChat.user_id == user.user_id, UserChat.chat_id == chat_id ) ) user_chat = user_chat_result.scalar_one_or_none() if not user_chat: user_chat = UserChat(user_id=user.user_id, chat_id=chat_id) db.add(user_chat) synced_count += 1 except Exception as e: # User might have blocked bot or is not in chat logger.debug(f"Could not sync user {user.user_id} in chat {chat_id}: {e}") await db.commit() logger.info(f"Synced {synced_count} users to chat ID: {chat_id}") except Exception as e: logger.error(f"Error syncing users for chat {chat_id}: {e}", exc_info=True) # If sync fails, continue anyway pass async def show_statistics(bot: AsyncTeleBot, chat: types.Chat) -> None: """Show statistics about users who shared their birthday.""" async for db in get_db_session(): try: chat_id = chat.id logger.debug(f"Showing statistics for chat ID: {chat_id}") # Get all chat members (exclude bots where possible) try: members_count_raw = await bot.get_chat_member_count(chat_id) # Try to subtract all bots (including this bot) using admin list human_members = members_count_raw try: admins = await bot.get_chat_administrators(chat_id) bots_in_admins = sum(1 for m in admins if getattr(m.user, "is_bot", False)) human_members = max(members_count_raw - bots_in_admins, 0) except Exception: human_members = members_count_raw members_count = human_members except Exception as e: logger.warning(f"Could not get member count for chat {chat_id}: {e}") members_count = 0 # Get users from this chat who shared birthday from sqlalchemy import func result = await db.execute( select(func.count(User.user_id.distinct())).select_from(User).join(UserChat).filter( UserChat.chat_id == chat_id ) ) users_with_birthday = result.scalar() or 0 logger.info(f"Chat {chat_id} statistics: {users_with_birthday} users with birthdays out of {members_count} total") # Create message if members_count > 0: percentage = (users_with_birthday / members_count) * 100 message_text = ( f"Отлично! Теперь я админ этого чата.\n\n" f"📊 Статистика:\n" f"• Поделились днем рождения: {users_with_birthday} из {members_count} участников\n" f"• Процент: {percentage:.1f}%" ) else: message_text = ( f"Отлично! Теперь я админ этого чата.\n\n" f"📊 Поделились днем рождения: {users_with_birthday} участников" ) # Create inline keyboard with button to start private chat bot_me = await bot.get_me() if not bot_me or not bot_me.username: await bot.send_message(chat_id, message_text) return keyboard = types.InlineKeyboardMarkup() start_button = types.InlineKeyboardButton( text="Поделиться днем рождения", url=f"https://t.me/{bot_me.username}?start=share_birthday" ) keyboard.add(start_button) await bot.send_message(chat_id, message_text, reply_markup=keyboard) except Exception as e: logger.error(f"Error showing statistics for chat {chat_id}: {e}", exc_info=True) finally: await db.close()