From e05a4229f185256b57f44f163dee6d1a4b8d749d Mon Sep 17 00:00:00 2001 From: J-Rios Date: Sun, 30 May 2021 17:08:58 +0200 Subject: [PATCH] Bot API 5.2 support (fix new member joins in large groups) --- README.md | 2 +- requirements.txt | 2 +- sources/constants.py | 6 +- sources/join_captcha_bot.py | 152 +++++++++++++++++------------------- sources/tlgbotutils.py | 45 ++++++++++- 5 files changed, 119 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index effca19..1ff57ff 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ BTC: ## Installation -Note: Use Python 3 to install and run the Bot, Python 2 support could be broken. +Note: Use Python 3.6 or above to install and run the Bot, previous version are unsupported. To generate Captchas, the Bot uses [multicolor_captcha_generator library](https://github.com/J-Rios/multicolor_captcha_generator), wich uses Pillow to generate the images. diff --git a/requirements.txt b/requirements.txt index 227143f..2eacd9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -python_telegram_bot==12.8.0 +python_telegram_bot==13.5 Pillow==8.1.1 multicolorcaptcha==1.0.0 diff --git a/sources/constants.py b/sources/constants.py index 2fa0ed2..50f4da3 100644 --- a/sources/constants.py +++ b/sources/constants.py @@ -10,9 +10,9 @@ Author: Creation date: 09/09/2018 Last modified date: - 27/05/2021 + 30/05/2021 Version: - 1.19.3 + 1.20.0 ''' ############################################################################### @@ -263,7 +263,7 @@ CONST = { "DEV_BTC": "3N9wf3FunR6YNXonquBeWammaBZVzTXTyR", # Bot version - "VERSION": "1.19.3 (27/05/2021)" + "VERSION": "1.20.0 (30/05/2021)" } diff --git a/sources/join_captcha_bot.py b/sources/join_captcha_bot.py index 7e6ca93..8eab92b 100644 --- a/sources/join_captcha_bot.py +++ b/sources/join_captcha_bot.py @@ -13,9 +13,9 @@ Author: Creation date: 09/09/2018 Last modified date: - 15/04/2021 + 30/05/2021 Version: - 1.19.2 + 1.20.0 ''' ############################################################################### @@ -45,8 +45,8 @@ from telegram import ( from telegram.ext import ( CallbackContext, Updater, CommandHandler, - MessageHandler, Filters, CallbackQueryHandler, - PollAnswerHandler, Defaults + ChatMemberHandler, MessageHandler, Filters, + CallbackQueryHandler, PollAnswerHandler, Defaults ) from telegram.ext.dispatcher import ( @@ -72,7 +72,7 @@ from tlgbotutils import ( tlg_answer_callback_query, tlg_delete_msg, tlg_edit_msg_media, tlg_ban_user, tlg_kick_user, tlg_user_is_admin, tlg_leave_chat, tlg_restrict_user, tlg_is_valid_user_id_or_alias, tlg_is_valid_group, - tlg_alias_in_string + tlg_alias_in_string, tlg_extract_members_status_change ) from constants import ( @@ -463,55 +463,52 @@ def is_group_in_banned_list(chat_id): ############################################################################### ### Received Telegram not-command messages handlers -@run_async def new_member_join(update: Update, context: CallbackContext): '''New member join the group event handler''' global new_users bot = context.bot - # Get message data - update_msg = getattr(update, "message", None) - if update_msg is None: - printts("Warning: Received an unexpected new user update.") - printts(update) + # Check members changes + result = tlg_extract_members_status_change(update.chat_member) + if result is None: return - chat_id = getattr(update_msg, "chat_id", None) - if chat_id is None: - printts("Warning: Received an unexpected new user update without chat id.") - printts(update) - return - chat = getattr(update_msg, "chat", None) - if chat is None: - printts("Warning: Received an unexpected new user update without chat.") - printts(update) - return - # Leave the chat if it is a channel - if chat.type == "channel": - printts("Bot try to be added to a channel") - tlg_send_msg(bot, chat_id, CONST["BOT_LEAVE_CHANNEL"]) - tlg_leave_chat(bot, chat_id) - return - # Check if Group is allowed to be used by the Bot - if not is_group_in_allowed_list(chat_id): - printts("Warning: Bot added to not allowed group.") - from_user_name = "" - if update_msg.from_user.name is not None: - from_user_name = update_msg.from_user.name - else: - from_user_name = update_msg.from_user.full_name - chat_link = "" - if chat.username: - chat_link = "@{}".format(chat.username) - printts("{}, {}, {}, {}".format(chat_id, from_user_name, chat.title, chat_link)) - msg_text = CONST["NOT_ALLOW_GROUP"].format(CONST["BOT_OWNER"], chat_id, CONST["REPOSITORY"]) - tlg_send_msg(bot, chat_id, msg_text) - tlg_leave_chat(bot, chat_id) - return - if is_group_in_banned_list(chat_id): - printts("Warning: Bot added to banned group: {}".format(chat_id)) - tlg_leave_chat(bot, chat_id) - return - # For each new user that join or has been added - for join_user in update_msg.new_chat_members: + was_member, is_member = result + # Ignore member leave group + #if not is_member and was_member: + # return + # Check if it is a new member join + if not was_member and is_member: + chat = update.chat_member.chat + member_added_by = update.chat_member.from_user + join_user = update.chat_member.new_chat_member.user + chat_id = chat.id + chat_type = chat.type + # Leave the chat if it is a channel + if chat_type == "channel": + printts("Bot try to be added to a channel") + tlg_send_msg(bot, chat_id, CONST["BOT_LEAVE_CHANNEL"]) + tlg_leave_chat(bot, chat_id) + return + # Check if Group is allowed to be used by the Bot + if not is_group_in_allowed_list(chat_id): + printts("Warning: Bot added to not allowed group.") + from_user_name = "" + if member_added_by.name is not None: + from_user_name = member_added_by.name + else: + from_user_name = member_added_by.full_name + chat_link = "" + if chat.username: + chat_link = "@{}".format(chat.username) + printts("{}, {}, {}, {}".format(chat_id, from_user_name, chat.title, chat_link)) + msg_text = CONST["NOT_ALLOW_GROUP"].format(CONST["BOT_OWNER"], chat_id, CONST["REPOSITORY"]) + tlg_send_msg(bot, chat_id, msg_text) + tlg_leave_chat(bot, chat_id) + return + if is_group_in_banned_list(chat_id): + printts("Warning: Bot added to banned group: {}".format(chat_id)) + tlg_leave_chat(bot, chat_id) + return + # Get User ID join_user_id = join_user.id # Get user name if join_user.name is not None: @@ -528,11 +525,9 @@ def new_member_join(update: Update, context: CallbackContext): # Get the language of the Telegram client software the Admin that has added the Bot # has, to assume this is the chat language and configure Bot language of this chat admin_language = "" - msg_from_user = getattr(update_msg, "from_user", None) - if msg_from_user: - language_code = getattr(msg_from_user, "language_code", None) - if language_code: - admin_language = language_code[0:2].upper() + language_code = getattr(member_added_by, "language_code", None) + if language_code: + admin_language = language_code[0:2].upper() if admin_language not in TEXT: admin_language = CONST["INIT_LANG"] save_config_property(chat_id, "Language", admin_language) @@ -564,29 +559,27 @@ def new_member_join(update: Update, context: CallbackContext): if tlg_user_is_admin(bot, join_user_id, chat_id): printts("[{}] User is an administrator.".format(chat_id)) printts("Skipping the captcha process.") - continue + return # Ignore Members added by an Admin - join_by = getattr(update_msg, "from_user", None) - if join_by: - join_by_id = update_msg.from_user.id - if tlg_user_is_admin(bot, join_by_id, chat_id): - printts("[{}] User has been added by an administrator.".format(chat_id)) - printts("Skipping the captcha process.") - continue + join_by_id = member_added_by.id + if tlg_user_is_admin(bot, join_by_id, chat_id): + printts("[{}] User has been added by an administrator.".format(chat_id)) + printts("Skipping the captcha process.") + return # Ignore if the member that has been join the group is a Bot if join_user.is_bot: printts("[{}] User is a Bot.".format(chat_id)) printts("Skipping the captcha process.") - continue + return # Ignore if the member that has joined is in chat ignore list if is_user_in_ignored_list(chat_id, join_user): printts("[{}] User is in ignore list.".format(chat_id)) printts("Skipping the captcha process.") - continue + return if is_user_in_allowed_list(join_user): printts("[{}] User is in global allowed list.".format(chat_id)) printts("Skipping the captcha process.") - continue + return # Check and remove previous join messages of that user (if any) if chat_id in new_users: if join_user_id in new_users[chat_id]: @@ -598,7 +591,7 @@ def new_member_join(update: Update, context: CallbackContext): captcha_enable = get_chat_config(chat_id, "Enabled") if not captcha_enable: printts("[{}] Captcha is not enabled in this chat".format(chat_id)) - continue + return # Determine configured language and captcha settings lang = get_chat_config(chat_id, "Language") captcha_level = get_chat_config(chat_id, "Captcha_Difficulty_Level") @@ -628,7 +621,7 @@ def new_member_join(update: Update, context: CallbackContext): or (poll_correct_option == 0): tlg_send_selfdestruct_msg(bot, chat_id, TEXT[lang]["POLL_NEW_USER_NOT_CONFIG"]) - continue + return # Remove empty strings from options list poll_options = list(filter(None, poll_options)) # Send request to solve the poll text message @@ -709,7 +702,8 @@ def new_member_join(update: Update, context: CallbackContext): join_data["join_retries"] = new_users[chat_id][join_user_id]["join_data"]["join_retries"] # Add new user join data and messages to be removed new_users[chat_id][join_user_id]["join_data"] = join_data - new_users[chat_id][join_user_id]["join_msg"] = update_msg.message_id + if update.message: + new_users[chat_id][join_user_id]["join_msg"] = update.message.message_id if sent_result["msg"]: new_users[chat_id][join_user_id]["msg_to_rm"].append(sent_result["msg"].message_id) if (captcha_mode == "poll") and (solve_poll_request_msg_id is not None): @@ -773,7 +767,6 @@ def msg_notext(update: Update, context: CallbackContext): tlg_send_selfdestruct_msg(bot, chat_id, bot_msg) -@run_async def msg_nocmd(update: Update, context: CallbackContext): '''Non-command text messages handler''' global new_users @@ -939,7 +932,6 @@ def msg_nocmd(update: Update, context: CallbackContext): printts(" ") -@run_async def receive_poll_answer(update: Update, context: CallbackContext): '''User poll vote received''' global new_users @@ -2304,7 +2296,9 @@ def th_time_to_kick_not_verify_users(bot): new_users[chat_id][user_id]["join_data"]["join_retries"] = join_retries # Remove join messages printts("[{}] Removing messages from user {}...".format(chat_id, user_name)) - tlg_delete_msg(bot, chat_id, new_users[chat_id][user_id]["join_msg"]) + join_msg = new_users[chat_id][user_id]["join_msg"] + if join_msg is not None: + tlg_delete_msg(bot, chat_id, join_msg) for msg in new_users[chat_id][user_id]["msg_to_rm"]: tlg_delete_msg(bot, chat_id, msg) new_users[chat_id][user_id]["msg_to_rm"].clear() @@ -2362,7 +2356,7 @@ def main(): # Set messages to be sent silently by default msgs_defaults = Defaults(disable_notification=True) # Create an event handler (updater) for a Bot with the given Token and get the dispatcher - updater = Updater(CONST["TOKEN"], workers=12, use_context=True, defaults=msgs_defaults) + updater = Updater(CONST["TOKEN"], workers=12, defaults=msgs_defaults) dp = updater.dispatcher # Set Telegram errors handler dp.add_error_handler(tlg_error_callback) @@ -2393,32 +2387,32 @@ def main(): if (CONST["BOT_OWNER"] != "XXXXXXXXX") and CONST["BOT_PRIVATE"]: dp.add_handler(CommandHandler("allowgroup", cmd_allowgroup, pass_args=True)) # Set to dispatcher a not-command text messages handler - dp.add_handler(MessageHandler(Filters.text, msg_nocmd)) + dp.add_handler(MessageHandler(Filters.text, msg_nocmd, run_async=True)) # Set to dispatcher not text messages handler dp.add_handler(MessageHandler(Filters.photo | Filters.audio | Filters.voice | Filters.video | Filters.sticker | Filters.document | Filters.location | Filters.contact, msg_notext)) # Set to dispatcher a new member join the group and member left the group events handlers - dp.add_handler(MessageHandler(Filters.status_update.new_chat_members, new_member_join)) + dp.add_handler(ChatMemberHandler(new_member_join, ChatMemberHandler.ANY_CHAT_MEMBER, run_async=True)) # Set to dispatcher inline keyboard callback handler for new captcha request and # button captcha challenge dp.add_handler(CallbackQueryHandler(key_inline_keyboard)) # Set to dispatcher users poll vote handler - dp.add_handler(PollAnswerHandler(receive_poll_answer)) + dp.add_handler(PollAnswerHandler(receive_poll_answer, run_async=True)) # Launch the Bot ignoring pending messages (clean=True) and get all updates (allowed_uptades=[]) if CONST["WEBHOOK_HOST"] == "None": printts("Setup Bot for Polling.") updater.start_polling( - clean=True, - allowed_updates=[] + drop_pending_updates=True, + allowed_updates=Update.ALL_TYPES ) else: printts("Setup Bot for Webhook.") updater.start_webhook( - clean=True, listen="0.0.0.0", port=CONST["WEBHOOK_PORT"], url_path=CONST["TOKEN"], + drop_pending_updates=True, listen="0.0.0.0", port=CONST["WEBHOOK_PORT"], url_path=CONST["TOKEN"], key=CONST["WEBHOOK_CERT_PRIV_KEY"], cert=CONST["WEBHOOK_CERT"], webhook_url="https://{}:{}/{}".format(CONST["WEBHOOK_HOST"], CONST["WEBHOOK_PORT"], - CONST["TOKEN"]) + CONST["TOKEN"]), allowed_updates=Update.ALL_TYPES ) printts("Bot setup completed. Bot is now running.") # Launch delete mesages and kick users threads diff --git a/sources/tlgbotutils.py b/sources/tlgbotutils.py index 7525a45..d081883 100644 --- a/sources/tlgbotutils.py +++ b/sources/tlgbotutils.py @@ -10,16 +10,19 @@ Author: Creation date: 02/11/2020 Last modified date: - 29/03/2021 + 30/05/2021 Version: - 1.0.4 + 1.1.0 ''' ############################################################################### ### Imported modules +from typing import Tuple, Optional + from telegram import ( - ChatPermissions, TelegramError, ParseMode, Poll + ChatPermissions, TelegramError, ParseMode, Poll, ChatMemberUpdated, + ChatMember ) from telegram.utils.helpers import DEFAULT_NONE @@ -311,7 +314,7 @@ def tlg_get_chat_type(bot, chat_id_or_alias, timeout=None): def tlg_is_valid_user_id_or_alias(user_id_alias): '''Check if given telegram ID or alias has a valid expected format.''' - # Check if it is a valid alias (start with a @ and have 5 characters or more) + # Check if it is a valid alias (start with @ and have 5 characters or more) if user_id_alias[0] == '@': if len(user_id_alias) > 5: return True @@ -346,3 +349,37 @@ def tlg_alias_in_string(str_text): if (len(word) > 1) and (word[0] == '@'): return True return False + + +def tlg_extract_members_status_change( + chat_member_update: ChatMemberUpdated, +) -> Optional[Tuple[bool, bool]]: + '''Takes a ChatMemberUpdated instance and extracts whether the + "old_chat_member" was a member of the chat and whether the + "new_chat_member" is a member of the chat. Returns None, if the status + didn't change.''' + members_diff = chat_member_update.difference() + status_change = members_diff.get("status") + if status_change is None: + return None + old_status, new_status = status_change + old_is_member, new_is_member = members_diff.get("is_member", (None, None)) + was_member = ( + old_status + in [ + ChatMember.MEMBER, + ChatMember.CREATOR, + ChatMember.ADMINISTRATOR, + ] + or (old_status == ChatMember.RESTRICTED and old_is_member is True) + ) + is_member = ( + new_status + in [ + ChatMember.MEMBER, + ChatMember.CREATOR, + ChatMember.ADMINISTRATOR, + ] + or (new_status == ChatMember.RESTRICTED and new_is_member is True) + ) + return was_member, is_member