Files
bdbot/bot/handlers/group_handlers.py
2026-01-28 15:53:27 +03:00

229 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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()