FileToLink / Thunder /utils /broadcast.py
Fyaz Mohammed
2.0.0
17cac08 unverified
Raw
History Blame Contribute Delete
7.42 kB
# Thunder/utils/broadcast.py
import asyncio
import os
import time
from pyrogram.client import Client
from pyrogram.enums import ParseMode
from pyrogram.errors import (ChatWriteForbidden, FloodWait, PeerIdInvalid, UserDeactivated,
UserIsBlocked, ChannelInvalid, InputUserDeactivated)
from pyrogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
Message)
from Thunder.utils.database import db
from Thunder.utils.logger import logger
from Thunder.utils.messages import (
MSG_INVALID_BROADCAST_CMD,
MSG_BROADCAST_START,
MSG_BUTTON_CANCEL_BROADCAST,
MSG_BROADCAST_COMPLETE
)
from Thunder.utils.time_format import get_readable_time
broadcast_ids = {}
async def broadcast_message(client: Client, message: Message, mode: str = "all"):
if not message.reply_to_message:
try:
await message.reply_text(MSG_INVALID_BROADCAST_CMD)
except FloodWait as e:
await asyncio.sleep(e.value)
await message.reply_text(MSG_INVALID_BROADCAST_CMD)
except Exception as e:
logger.error(f"Error sending invalid broadcast message: {e}", exc_info=True)
return
broadcast_id = os.urandom(3).hex()
stats = {"total": 0, "success": 0, "failed": 0, "deleted": 0, "cancelled": False}
broadcast_ids[broadcast_id] = stats
try:
status_msg = await message.reply_text(
MSG_BROADCAST_START,
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton(MSG_BUTTON_CANCEL_BROADCAST, callback_data=f"cancel_{broadcast_id}")
]])
)
except FloodWait as e:
await asyncio.sleep(e.value)
status_msg = await message.reply_text(
MSG_BROADCAST_START,
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton(MSG_BUTTON_CANCEL_BROADCAST, callback_data=f"cancel_{broadcast_id}")
]])
)
except Exception as e:
logger.error(f"Error starting broadcast: {e}", exc_info=True)
del broadcast_ids[broadcast_id]
return
start_time = time.time()
try:
if mode == "authorized":
stats["total"] = await db.get_authorized_users_count()
cursor = await db.get_authorized_users_cursor()
elif mode == "regular":
stats["total"] = await db.get_regular_users_count()
cursor = await db.get_regular_users_cursor()
else:
stats["total"] = await db.total_users_count()
cursor = await db.get_all_users()
except Exception as e:
logger.error(f"Error getting user cursor for mode '{mode}': {e}", exc_info=True)
try:
await status_msg.edit_text(f"❌ **Broadcast Failed:** Unable to fetch users for mode '{mode}'.")
except Exception:
pass
del broadcast_ids[broadcast_id]
return
if stats["total"] == 0:
try:
await status_msg.edit_text(f"ℹ️ **No users found for broadcast mode:** `{mode}`")
except Exception:
pass
del broadcast_ids[broadcast_id]
return
async def do_broadcast():
async for user in cursor:
if stats["cancelled"]:
break
user_id = user.get('id') or user.get('user_id')
if not user_id:
logger.warning(f"Skipping user with no ID: {user}")
continue
try:
success = False
for attempt in range(3):
try:
await message.reply_to_message.copy(user_id)
stats["success"] += 1
success = True
break
except FloodWait as e:
if attempt < 2:
await asyncio.sleep(e.value)
else:
logger.warning(f"FloodWait persisted for user {user_id} after 3 attempts, last wait: {e.value}s")
stats["failed"] += 1
break
except (UserDeactivated, UserIsBlocked, PeerIdInvalid, ChatWriteForbidden, ChannelInvalid, InputUserDeactivated) as e:
if isinstance(e, ChannelInvalid):
recipient_type = "Channel"
reason = "invalid channel"
elif isinstance(e, InputUserDeactivated):
recipient_type = "User"
reason = "deactivated account"
elif isinstance(e, UserIsBlocked):
recipient_type = "User"
reason = "blocked the bot"
elif isinstance(e, UserDeactivated):
recipient_type = "User"
reason = "deactivated account"
elif isinstance(e, PeerIdInvalid):
recipient_type = "Recipient"
reason = "invalid ID"
elif isinstance(e, ChatWriteForbidden):
recipient_type = "Chat"
reason = "write forbidden"
else:
recipient_type = "Recipient"
reason = f"error: {type(e).__name__}"
logger.warning(f"{recipient_type} {user_id} removed due to {reason}")
is_authorized = await db.is_user_authorized(user_id)
if not is_authorized:
await db.delete_user(user_id)
stats["deleted"] += 1
else:
stats["failed"] += 1
except Exception as e:
logger.error(f"Error copying message to user {user_id}: {e}", exc_info=True)
stats["failed"] += 1
try:
await status_msg.delete()
except FloodWait as e:
await asyncio.sleep(e.value)
try:
await status_msg.delete()
except Exception:
pass
except Exception as e:
logger.debug(f"Could not delete status message: {e}")
completion_msg = MSG_BROADCAST_COMPLETE.format(
elapsed_time=get_readable_time(int(time.time() - start_time)),
total_users=stats["total"],
successes=stats["success"],
failures=stats["failed"],
deleted_accounts=stats["deleted"]
)
if stats["cancelled"]:
completion_msg = "🛑 **Broadcast Cancelled**\n\n" + completion_msg
try:
await message.reply_text(completion_msg, parse_mode=ParseMode.MARKDOWN)
except FloodWait as e:
await asyncio.sleep(e.value)
try:
await message.reply_text(completion_msg, parse_mode=ParseMode.MARKDOWN)
except Exception as e:
logger.error(f"Failed to send completion message after FloodWait: {e}", exc_info=True)
except Exception as e:
logger.error(f"Failed to send broadcast completion message: {e}", exc_info=True)
if broadcast_id in broadcast_ids:
del broadcast_ids[broadcast_id]
asyncio.create_task(do_broadcast())