diff --git a/.gitignore b/.gitignore index f529a2ed9b..d67488e57b 100644 --- a/.gitignore +++ b/.gitignore @@ -153,8 +153,11 @@ dmypy.json .vscode # Others +backups/ *.mp3 *.mp4 *.otf *.png *.ttf +shell_output.txt +error.txt diff --git a/AstrakoBot/__init__.py b/AstrakoBot/__init__.py index c667abbaea..830553c8c5 100644 --- a/AstrakoBot/__init__.py +++ b/AstrakoBot/__init__.py @@ -17,6 +17,10 @@ ) LOGGER = logging.getLogger(__name__) +log_warning_only = ["telethon.client.updates", "apscheduler.scheduler", "apscheduler.executors.default", "telethon.network.mtprotosender", "telethon.client"] + +for i in log_warning_only: + logging.getLogger(i).setLevel(logging.WARNING) # if version < 3.6, stop bot. if sys.version_info[0] < 3 or sys.version_info[1] < 6: @@ -83,6 +87,8 @@ WEATHER_API = os.environ.get("WEATHER_API", None) ALLOW_CHATS = os.environ.get("ALLOW_CHATS", True) + BACKUP_PASS = os.environ.get("BACKUP_PASS", True) + DROP_UPDATES = os.environ.get("DROP_UPDATES", False) try: BL_CHATS = set(int(x) for x in os.environ.get("BL_CHATS", "").split()) @@ -129,6 +135,7 @@ API_HASH = Config.API_HASH DB_URI = Config.SQLALCHEMY_DATABASE_URI + DB_NAME = Config.DB_NAME DONATION_LINK = Config.DONATION_LINK LOAD = Config.LOAD NO_LOAD = Config.NO_LOAD @@ -146,6 +153,8 @@ SPAMWATCH_API = Config.SPAMWATCH_API INFOPIC = Config.INFOPIC WEATHER_API = Config.WEATHER_API + BACKUP_PASS = Config.BACKUP_PASS + DROP_UPDATES = Config.DROP_UPDATES try: BL_CHATS = set(int(x) for x in Config.BL_CHATS or []) diff --git a/AstrakoBot/__main__.py b/AstrakoBot/__main__.py index b83be07733..421fb889a3 100644 --- a/AstrakoBot/__main__.py +++ b/AstrakoBot/__main__.py @@ -20,12 +20,13 @@ StartTime, telethn, updater, + DROP_UPDATES, ) # needed to dynamically load modules # NOTE: Module order is not guaranteed, specify that in the config file! from AstrakoBot.modules import ALL_MODULES -from AstrakoBot.modules.helper_funcs.chat_status import is_user_admin +from AstrakoBot.modules.helper_funcs.admin_status import user_is_admin from AstrakoBot.modules.helper_funcs.misc import paginate_modules from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode, Update from telegram.error import ( @@ -169,13 +170,6 @@ def send_help(chat_id, text, keyboard=None): ) -def test(update: Update, context: CallbackContext): - # pprint(eval(str(update))) - # update.effective_message.reply_text("Hola tester! _I_ *have* `markdown`", parse_mode=ParseMode.MARKDOWN) - update.effective_message.reply_text("This person edited a message") - print(update.effective_message) - - def start(update: Update, context: CallbackContext): args = context.args uptime = get_readable_time((time.time() - StartTime)) @@ -202,7 +196,7 @@ def start(update: Update, context: CallbackContext): match = re.match("stngs_(.*)", args[0].lower()) chat = dispatcher.bot.getChat(match.group(1)) - if is_user_admin(chat, update.effective_user.id): + if user_is_admin(chat, update.effective_user.id): send_settings(match.group(1), update.effective_user.id, False) else: send_settings(match.group(1), update.effective_user.id, True) @@ -256,43 +250,23 @@ def start(update: Update, context: CallbackContext): ), ) else: - update.effective_message.reply_text( - "I'm awake already!\nHaven't slept since: {}".format( - uptime - ), - parse_mode=ParseMode.HTML, - ) - + try: + update.effective_message.reply_text( + "I'm awake already!\nHaven't slept since: {}".format( + uptime + ), + parse_mode=ParseMode.HTML, + ) + except BadRequest: + pass # chat_checker will take care of it, just don't error # for test purposes def error_callback(update: Update, context: CallbackContext): error = context.error try: raise error - except Unauthorized: - print("no nono1") - print(error) - # remove update.message.chat_id from conversation list - except BadRequest: - print("no nono2") - print("BadRequest caught") - print(error) - - # handle malformed requests - read more below! - except TimedOut: - print("no nono3") - # handle slow connection problems - except NetworkError: - print("no nono4") - # handle other connection problems - except ChatMigrated as err: - print("no nono5") - print(err) - # the chat_id of a group has changed, use e.new_chat_id instead - except TelegramError: + except: print(error) - # handle all other telegram related errors - def help_button(update: Update, context: CallbackContext): query = update.callback_query @@ -301,8 +275,6 @@ def help_button(update: Update, context: CallbackContext): next_match = re.match(r"help_next\((.+?)\)", query.data) back_match = re.match(r"help_back", query.data) - print(query.message.chat.id) - try: if mod_match: module = mod_match.group(1) @@ -548,7 +520,7 @@ def get_settings(update: Update, context: CallbackContext): # ONLY send settings in PM if chat.type != chat.PRIVATE: - if is_user_admin(chat, user.id): + if user_is_admin(chat, user.id): text = "Click here to get this chat's settings, as well as yours." msg.reply_text( text, @@ -626,7 +598,6 @@ def migrate_chats(update: Update, context: CallbackContext): def main(): - test_handler = CommandHandler("test", test, run_async=True) start_handler = CommandHandler("start", start, run_async=True) help_handler = CommandHandler("help", get_help, run_async=True) @@ -638,7 +609,6 @@ def main(): donate_handler = CommandHandler("donate", donate, run_async=True) migrate_handler = MessageHandler(Filters.status_update.migrate, migrate_chats) - # dispatcher.add_handler(test_handler) dispatcher.add_handler(start_handler) dispatcher.add_handler(help_handler) dispatcher.add_handler(settings_handler) @@ -660,12 +630,11 @@ def main(): else: LOGGER.info("Using long polling.") - updater.start_polling(timeout=15, read_latency=4, drop_pending_updates=True) + allowed_updates = ['message', 'edited_message', 'callback_query', 'callback_query', 'my_chat_member', + 'chat_member', 'chat_join_request', 'channel_post', 'edited_channel_post', 'inline_query'] + updater.start_polling(timeout=15, read_latency=4, drop_pending_updates=DROP_UPDATES, allowed_updates = allowed_updates) - if len(argv) not in (1, 3, 4): - telethn.disconnect() - else: - telethn.run_until_disconnected() + telethn.run_until_disconnected() updater.idle() diff --git a/AstrakoBot/modules/admin.py b/AstrakoBot/modules/admin.py index a3493d462a..56269ae46f 100644 --- a/AstrakoBot/modules/admin.py +++ b/AstrakoBot/modules/admin.py @@ -15,7 +15,7 @@ user_admin, ADMIN_CACHE, ) - +from AstrakoBot.modules.helper_funcs.admin_status import get_bot_member from AstrakoBot.modules.helper_funcs.extraction import ( extract_user, extract_user_and_text, @@ -68,7 +68,7 @@ def promote(update: Update, context: CallbackContext) -> str: return # set same perms as bot - bot can't assign higher perms than itself! - bot_member = chat.get_member(bot.id) + bot_member = get_bot_member(chat.id) try: bot.promoteChatMember( @@ -358,7 +358,7 @@ def invite(update: Update, context: CallbackContext): if chat.username: update.effective_message.reply_text(f"https://t.me/{chat.username}") elif chat.type in [chat.SUPERGROUP, chat.CHANNEL]: - bot_member = chat.get_member(bot.id) + bot_member = get_bot_member(chat.id) if bot_member.can_invite_users: invitelink = bot.exportChatInviteLink(chat.id) update.effective_message.reply_text(invitelink) @@ -399,14 +399,12 @@ def adminlist(update: Update, context: CallbackContext): administrators = bot.getChatAdministrators(chat_id) text = "Admins in {}:".format(html.escape(update.effective_chat.title)) - bot_admin_list = [] - for admin in administrators: user = admin.user status = admin.status custom_title = admin.custom_title - if user.first_name == "": + if not user.first_name: name = "☠ Deleted Account" else: name = "{}".format( @@ -415,19 +413,12 @@ def adminlist(update: Update, context: CallbackContext): ) ) - if user.is_bot: - bot_admin_list.append(name) - administrators.remove(admin) - continue - - # if user.username: - # name = escape_markdown("@" + user.username) if status == "creator": text += "\n šŸ‘‘ Creator:" text += "\n • {}\n".format(name) - if custom_title: - text += f" ┗━ {html.escape(custom_title)}\n" + # if user.username: + # name = escape_markdown("@" + user.username) text += "\nšŸ”± Admins:" @@ -439,7 +430,7 @@ def adminlist(update: Update, context: CallbackContext): status = admin.status custom_title = admin.custom_title - if user.first_name == "": + if not user.first_name: name = "☠ Deleted Account" else: name = "{}".format( @@ -475,10 +466,6 @@ def adminlist(update: Update, context: CallbackContext): text += "\n • {}".format(admin) text += "\n" - text += "\nšŸ¤– Bots:" - for each_bot in bot_admin_list: - text += "\n • {}".format(each_bot) - try: msg.edit_text(text, parse_mode=ParseMode.HTML) except BadRequest: # if original message is deleted diff --git a/AstrakoBot/modules/afk.py b/AstrakoBot/modules/afk.py index 7255162b27..f59e8d01f5 100644 --- a/AstrakoBot/modules/afk.py +++ b/AstrakoBot/modules/afk.py @@ -180,7 +180,7 @@ def __gdpr__(user_id): AFK_HANDLER = DisableAbleCommandHandler("afk", afk, run_async=True) AFK_REGEX_HANDLER = DisableAbleMessageHandler( - Filters.regex(r"^(?i)brb(.*)$"), afk, friendly="afk" + Filters.regex(r"(?i)^brb(.*)$"), afk, friendly="afk" ) NO_AFK_HANDLER = MessageHandler(Filters.all & Filters.chat_type.groups, no_longer_afk, run_async=True) AFK_REPLY_HANDLER = MessageHandler(Filters.all & Filters.chat_type.groups, reply_afk, run_async=True) diff --git a/AstrakoBot/modules/android.py b/AstrakoBot/modules/android.py index b963f17d39..f66acb3b79 100644 --- a/AstrakoBot/modules/android.py +++ b/AstrakoBot/modules/android.py @@ -7,7 +7,7 @@ from bs4 import BeautifulSoup from requests import get from telegram import Bot, Update, ParseMode, InlineKeyboardMarkup, InlineKeyboardButton -from telegram.ext import Updater, CommandHandler, MessageHandler +from telegram.ext import Updater, MessageHandler from telegram.ext import CallbackContext, run_async from ujson import loads from yaml import load, Loader @@ -16,7 +16,11 @@ from AstrakoBot.modules.sql.clear_cmd_sql import get_clearcmd from AstrakoBot.modules.github import getphh from AstrakoBot.modules.helper_funcs.misc import delete +from AstrakoBot.modules.disable import DisableAbleCommandHandler +rget_headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36" +} def magisk(update: Update, context: CallbackContext): message = update.effective_message @@ -45,6 +49,52 @@ def magisk(update: Update, context: CallbackContext): if cleartime: context.dispatcher.run_async(delete, delmsg, cleartime.time) +def kernelsu(update: Update, context: CallbackContext): + message = update.effective_message + chat = update.effective_chat + repos = [ + ("KernelSU", "tiann/KernelSU"), + ("KernelSU-Next", "KernelSU-Next/KernelSU-Next") + ] + + msg = "*Latest KernelSU Releases:*\n\n" + + for repo_name, repo_path in repos: + try: + api_url = f"https://api.github.com/repos/{repo_path}/releases/latest" + response = get(api_url, headers=rget_headers) + response.raise_for_status() + data = response.json() + + msg += f"*{repo_name}:*\n" + msg += f'• Release - [{data["tag_name"]}]({data["html_url"]})\n' + + apk_assets = [asset for asset in data["assets"] if asset["name"].lower().endswith(".apk")] + if apk_assets: + for asset in apk_assets: + msg += f'• APK - [{asset["name"]}]({asset["browser_download_url"]})\n' + else: + msg += "• APK - No APK assets found\n" + + msg += "\n" + + except Exception as e: + msg += f"*{repo_name}:* Error fetching data ({str(e)})\n\n" + continue + + if "Error fetching data" in msg: + msg += "\nāš ļø Failed to fetch some releases, try again later." + + delmsg = message.reply_text( + text=msg, + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=True, + ) + + cleartime = get_clearcmd(chat.id, "kernelsu") + + if cleartime: + context.dispatcher.run_async(delete, delmsg, cleartime.time) def checkfw(update: Update, context: CallbackContext): args = context.args @@ -55,14 +105,15 @@ def checkfw(update: Update, context: CallbackContext): temp, csc = args model = f'sm-' + temp if not temp.upper().startswith('SM-') else temp fota = get( - f'http://fota-cloud-dn.ospserver.net/firmware/{csc.upper()}/{model.upper()}/version.xml' + f'https://fota-cloud-dn.ospserver.net/firmware/{csc.upper()}/{model.upper()}/version.xml', + headers=rget_headers ) if fota.status_code != 200: msg = f"Couldn't check for {temp.upper()} and {csc.upper()}, please refine your search or try again later!" else: - page = BeautifulSoup(fota.content, 'lxml') + page = BeautifulSoup(fota.content, 'xml') os = page.find("latest").get("o") if page.find("latest").text.strip(): @@ -102,7 +153,8 @@ def getfw(update: Update, context: CallbackContext): temp, csc = args model = f'sm-' + temp if not temp.upper().startswith('SM-') else temp fota = get( - f'http://fota-cloud-dn.ospserver.net/firmware/{csc.upper()}/{model.upper()}/version.xml' + f'https://fota-cloud-dn.ospserver.net/firmware/{csc.upper()}/{model.upper()}/version.xml', + headers=rget_headers ) if fota.status_code != 200: @@ -113,10 +165,7 @@ def getfw(update: Update, context: CallbackContext): url2 = f'https://www.sammobile.com/samsung/firmware/{model.upper()}/{csc.upper()}/' url3 = f'https://sfirmware.com/samsung-{model.lower()}/#tab=firmwares' url4 = f'https://samfw.com/firmware/{model.upper()}/{csc.upper()}/' - fota = get( - f'http://fota-cloud-dn.ospserver.net/firmware/{csc.upper()}/{model.upper()}/version.xml' - ) - page = BeautifulSoup(fota.content, 'lxml') + page = BeautifulSoup(fota.content, 'xml') os = page.find("latest").get("o") msg = "" if page.find("latest").text.strip(): @@ -129,10 +178,10 @@ def getfw(update: Update, context: CallbackContext): msg += f'• Android: `{os}`\n' msg += '\n' msg += f'*Downloads for {model.upper()} and {csc.upper()}*\n' - btn = [[InlineKeyboardButton(text=f"samfrew.com", url = url1)]] - btn += [[InlineKeyboardButton(text=f"sammobile.com", url = url2)]] - btn += [[InlineKeyboardButton(text=f"sfirmware.com", url = url3)]] - btn += [[InlineKeyboardButton(text=f"samfw.com", url = url4)]] + btn = [[InlineKeyboardButton(text=f"Samfrew", url = url1)]] + btn += [[InlineKeyboardButton(text=f"Sammobile", url = url2)]] + btn += [[InlineKeyboardButton(text=f"SFirmware", url = url3)]] + btn += [[InlineKeyboardButton(text=f"Samfw (Recommended)", url = url4)]] else: msg = 'Give me something to fetch, like:\n`/getfw SM-N975F DBT`' @@ -222,13 +271,13 @@ def orangefox(update: Update, context: CallbackContext): if device: link = get(f"https://api.orangefox.download/v3/releases/?codename={device}&sort=date_desc&limit=1") - if link.status_code == 404: + page = loads(link.content) + file_id = page["data"][0]["_id"] if "data" in page else "" + link = get(f"https://api.orangefox.download/v3/devices/get?codename={device}") + page = loads(link.content) + if "detail" in page and page["detail"] == "Not Found": msg = f"OrangeFox recovery is not avaliable for {device}" else: - page = loads(link.content) - file_id = page["data"][0]["_id"] - link = get(f"https://api.orangefox.download/v3/devices/get?codename={device}") - page = loads(link.content) oem = page["oem_name"] model = page["model_name"] full_name = page["full_name"] @@ -240,7 +289,7 @@ def orangefox(update: Update, context: CallbackContext): version = page["version"] changelog = page["changelog"][0] size = str(round(float(page["size"]) / 1024 / 1024, 1)) + "MB" - dl_link = page["mirrors"]["NL"] + dl_link = page["mirrors"][next(iter(page["mirrors"]))] date = datetime.fromtimestamp(page["date"]) md5 = page["md5"] msg = f"*Latest OrangeFox Recovery for the {full_name}*\n\n" @@ -315,6 +364,8 @@ def twrp(update: Update, context: CallbackContext): *Available commands:*\n *Magisk:* • `/magisk`, `/su`, `/root`: fetches latest magisk\n +*KernelSU:* +• `/kernelsu`: fetches latest kernelsu\n *OrangeFox Recovery Project:* • `/orangefox` ``: fetches lastest OrangeFox Recovery available for a given device codename\n *TWRP:* @@ -328,16 +379,17 @@ def twrp(update: Update, context: CallbackContext): • `/getfw ` - Samsung only - gets firmware download links from samfrew, sammobile and sfirmwares for the given device """ -MAGISK_HANDLER = CommandHandler(["magisk", "root", "su"], magisk, run_async=True) -ORANGEFOX_HANDLER = CommandHandler("orangefox", orangefox, run_async=True) -TWRP_HANDLER = CommandHandler("twrp", twrp, run_async=True) -GETFW_HANDLER = CommandHandler("getfw", getfw, run_async=True) -CHECKFW_HANDLER = CommandHandler("checkfw", checkfw, run_async=True) -PHH_HANDLER = CommandHandler("phh", phh, run_async=True) -MIUI_HANDLER = CommandHandler("miui", miui, run_async=True) - +MAGISK_HANDLER = DisableAbleCommandHandler(["magisk", "root", "su"], magisk, run_async=True) +KERNELSU_HANDLER = DisableAbleCommandHandler("kernelsu", kernelsu, run_async=True) +ORANGEFOX_HANDLER = DisableAbleCommandHandler("orangefox", orangefox, run_async=True) +TWRP_HANDLER = DisableAbleCommandHandler("twrp", twrp, run_async=True) +GETFW_HANDLER = DisableAbleCommandHandler("getfw", getfw, run_async=True) +CHECKFW_HANDLER = DisableAbleCommandHandler("checkfw", checkfw, run_async=True) +PHH_HANDLER = DisableAbleCommandHandler("phh", phh, run_async=True) +MIUI_HANDLER = DisableAbleCommandHandler("miui", miui, run_async=True) dispatcher.add_handler(MAGISK_HANDLER) +dispatcher.add_handler(KERNELSU_HANDLER) dispatcher.add_handler(ORANGEFOX_HANDLER) dispatcher.add_handler(TWRP_HANDLER) dispatcher.add_handler(GETFW_HANDLER) @@ -346,5 +398,5 @@ def twrp(update: Update, context: CallbackContext): dispatcher.add_handler(MIUI_HANDLER) __mod_name__ = "Android" -__command_list__ = ["magisk", "root", "su", "orangefox", "twrp", "checkfw", "getfw", "phh", "miui"] -__handlers__ = [MAGISK_HANDLER, ORANGEFOX_HANDLER, TWRP_HANDLER, GETFW_HANDLER, CHECKFW_HANDLER, PHH_HANDLER, MIUI_HANDLER] +__command_list__ = ["magisk", "kernelsu", "root", "su", "orangefox", "twrp", "checkfw", "getfw", "phh", "miui"] +__handlers__ = [MAGISK_HANDLER, KERNELSU_HANDLER, ORANGEFOX_HANDLER, TWRP_HANDLER, GETFW_HANDLER, CHECKFW_HANDLER, PHH_HANDLER, MIUI_HANDLER] diff --git a/AstrakoBot/modules/antiflood.py b/AstrakoBot/modules/antiflood.py index 1ae174ccf0..31a882c2d8 100644 --- a/AstrakoBot/modules/antiflood.py +++ b/AstrakoBot/modules/antiflood.py @@ -7,7 +7,6 @@ from AstrakoBot import WHITELIST_USERS, dispatcher from AstrakoBot.modules.helper_funcs.chat_status import ( bot_admin, - is_user_admin, user_admin, user_admin_no_reply, ) @@ -27,6 +26,7 @@ from AstrakoBot.modules.connection import connected from AstrakoBot.modules.helper_funcs.alternate import send_message from AstrakoBot.modules.sql.approve_sql import is_approved +from AstrakoBot.modules.helper_funcs.admin_status import user_is_admin FLOOD_GROUP = 3 @@ -39,8 +39,8 @@ def check_flood(update: Update, context: CallbackContext) -> str: if not user: # ignore channels return "" - # ignore admins and whitelists - if is_user_admin(chat, user.id) or user.id in WHITELIST_USERS: + # ignore admins, whitelists and telegram's native tech + if user_is_admin(chat, user.id) or user.id in WHITELIST_USERS or user.id == 777000: sql.update_flood(chat.id, None) return "" # ignore approved users @@ -54,11 +54,11 @@ def check_flood(update: Update, context: CallbackContext) -> str: try: getmode, getvalue = sql.get_flood_setting(chat.id) if getmode == 1: - chat.kick_member(user.id) + chat.ban_member(user.id) execstrings = "Banned" tag = "BANNED" elif getmode == 2: - chat.kick_member(user.id) + chat.ban_member(user.id) chat.unban_member(user.id) execstrings = "Kicked" tag = "KICKED" @@ -70,7 +70,7 @@ def check_flood(update: Update, context: CallbackContext) -> str: tag = "MUTED" elif getmode == 4: bantime = extract_time(msg, getvalue) - chat.kick_member(user.id, until_date=bantime) + chat.ban_member(user.id, until_date=bantime) execstrings = "Banned for {}".format(getvalue) tag = "TBAN" elif getmode == 5: diff --git a/AstrakoBot/modules/approve.py b/AstrakoBot/modules/approve.py index 3c081a3fcd..902925c58a 100644 --- a/AstrakoBot/modules/approve.py +++ b/AstrakoBot/modules/approve.py @@ -101,8 +101,11 @@ def approved(update: Update, context: CallbackContext): msg = "The following users are approved.\n" approved_users = sql.list_approved(message.chat_id) for i in approved_users: - member = chat.get_member(int(i.user_id)) - msg += f"- `{i.user_id}`: {member.user['first_name']}\n" + try: + member = chat.get_member(int(i.user_id)) + msg += f"- `{i.user_id}`: {member.user['first_name']}\n" + except BadRequest: + sql.disapprove(message.chat_id, i.user_id) if msg.endswith("approved.\n"): message.reply_text(f"No users are approved in {chat_title}.") return "" diff --git a/AstrakoBot/modules/backups.py b/AstrakoBot/modules/backups.py index fa7fce5445..9e08430858 100644 --- a/AstrakoBot/modules/backups.py +++ b/AstrakoBot/modules/backups.py @@ -57,7 +57,14 @@ def import_data(update: Update, context: CallbackContext): with BytesIO() as file: file_info.download(out=file) file.seek(0) - data = json.load(file) + try: + data = json.load(file) + except json.decoder.JSONDecodeError: + msg.reply_text("Backup seems to be invalid or corrupted!") + return + except UnicodeDecodeError: + msg.reply_text("This is not a backup file!") + return # only import one group if len(data) > 1 and str(chat.id) not in data: @@ -115,6 +122,8 @@ def import_data(update: Update, context: CallbackContext): else: text = "Backup fully restored" msg.reply_text(text, parse_mode="markdown") + else: + msg.reply_text("Reply to a valid backup file!") @user_admin diff --git a/AstrakoBot/modules/bans.py b/AstrakoBot/modules/bans.py index 8c0da91271..9f771d9fa6 100644 --- a/AstrakoBot/modules/bans.py +++ b/AstrakoBot/modules/bans.py @@ -19,13 +19,14 @@ bot_admin, can_restrict, connection_status, - is_user_admin, is_user_ban_protected, is_user_in_chat, user_admin, user_can_ban, can_delete, + is_bot_admin, ) +from AstrakoBot.modules.helper_funcs.admin_status import user_is_admin from AstrakoBot.modules.helper_funcs.extraction import extract_user_and_text from AstrakoBot.modules.helper_funcs.string_handling import extract_time from AstrakoBot.modules.log_channel import gloggable, loggable @@ -100,7 +101,7 @@ def ban(update: Update, context: CallbackContext) -> str: log += "\nReason: {}".format(reason) try: - chat.kick_member(user_id) + chat.ban_member(user_id) if silent: if message.reply_to_message: @@ -122,7 +123,7 @@ def ban(update: Update, context: CallbackContext) -> str: return log except BadRequest as excp: - if excp.message == "Reply message not found": + if excp.message == "Reply message not found" or excp.message == "Message can't be deleted": # Do not reply if silent: return log @@ -199,7 +200,7 @@ def temp_ban(update: Update, context: CallbackContext) -> str: log += "\nReason: {}".format(reason) try: - chat.kick_member(user_id, until_date=bantime) + chat.ban_member(user_id, until_date=bantime) bot.sendMessage( chat.id, f"Banned! User {mention_html(member.user.id, html.escape(member.user.first_name))} " @@ -294,7 +295,7 @@ def punch(update: Update, context: CallbackContext) -> str: @can_restrict def punchme(update: Update, context: CallbackContext): user_id = update.effective_message.from_user.id - if is_user_admin(update.effective_chat, user_id): + if user_is_admin(update.effective_chat, user_id): update.effective_message.reply_text("I wish I could... but you're an admin.") return @@ -353,11 +354,8 @@ def unban(update: Update, context: CallbackContext) -> str: return log -@connection_status -@bot_admin -@can_restrict @gloggable -def selfunban(context: CallbackContext, update: Update) -> str: +def selfunban(update: Update, context: CallbackContext) -> str: message = update.effective_message user = update.effective_user bot, args = context.bot, context.args @@ -370,7 +368,19 @@ def selfunban(context: CallbackContext, update: Update) -> str: message.reply_text("Give a valid chat ID.") return - chat = bot.getChat(chat_id) + try: + chat = bot.getChat(chat_id) + except: + message.reply_text("It doesn't look like I'm listed as a member of this chat.") + return + + if (chat.type == "private"): + message.reply_text("How am i going to unban you from a private chat?") + return + + if not is_bot_admin(chat, bot.id): + message.reply_text("I'm sorry but i am not an admin there!") + return try: member = chat.get_member(user.id) diff --git a/AstrakoBot/modules/blacklist.py b/AstrakoBot/modules/blacklist.py index 93967c8f4c..6634783bf7 100644 --- a/AstrakoBot/modules/blacklist.py +++ b/AstrakoBot/modules/blacklist.py @@ -387,7 +387,7 @@ def del_blacklist(update: Update, context: CallbackContext): return elif getmode == 5: message.delete() - chat.kick_member(user.id) + chat.ban_member(user.id) bot.sendMessage( chat.id, f"Banned {user.first_name} for using Blacklisted word: {trigger}", @@ -396,7 +396,7 @@ def del_blacklist(update: Update, context: CallbackContext): elif getmode == 6: message.delete() bantime = extract_time(message, value) - chat.kick_member(user.id, until_date=bantime) + chat.ban_member(user.id, until_date=bantime) bot.sendMessage( chat.id, f"Banned {user.first_name} until '{value}' for using Blacklisted word: {trigger}!", diff --git a/AstrakoBot/modules/blacklist_stickers.py b/AstrakoBot/modules/blacklist_stickers.py index 40de97a766..4ed78b0a1a 100644 --- a/AstrakoBot/modules/blacklist_stickers.py +++ b/AstrakoBot/modules/blacklist_stickers.py @@ -423,7 +423,7 @@ def del_blackliststicker(update: Update, context: CallbackContext): return elif getmode == 5: message.delete() - chat.kick_member(user.id) + chat.ban_member(user.id) bot.sendMessage( chat.id, "{} banned because using '{}' which in blacklist stickers".format( @@ -435,7 +435,7 @@ def del_blackliststicker(update: Update, context: CallbackContext): elif getmode == 6: message.delete() bantime = extract_time(message, value) - chat.kick_member(user.id, until_date=bantime) + chat.ban_member(user.id, until_date=bantime) bot.sendMessage( chat.id, "{} banned for {} because using '{}' which in blacklist stickers".format( diff --git a/AstrakoBot/modules/cleaner.py b/AstrakoBot/modules/cleaner.py index f94f4dab77..d0af505dac 100644 --- a/AstrakoBot/modules/cleaner.py +++ b/AstrakoBot/modules/cleaner.py @@ -8,6 +8,7 @@ dev_plus, user_admin, ) +from AstrakoBot.modules.helper_funcs.admin_status import get_bot_member from AstrakoBot.modules.sql import cleaner_sql as sql from telegram import ParseMode, Update from telegram.ext import ( @@ -46,7 +47,7 @@ def clean_blue_text_must_click(update: Update, context: CallbackContext): bot = context.bot chat = update.effective_chat message = update.effective_message - if chat.get_member(bot.id).can_delete_messages and sql.is_enabled(chat.id): + if get_bot_member(chat.id).can_delete_messages and sql.is_enabled(chat.id): fst_word = message.text.strip().split(None, 1)[0] if len(fst_word) > 1 and any( @@ -204,7 +205,7 @@ def bluetext_ignore_list(update: Update, context: CallbackContext): for x in local_ignore_list: text += f" - {x}\n" - if text == "": + if not text: text = "No commands are currently ignored from bluetext cleaning." message.reply_text(text) return diff --git a/AstrakoBot/modules/clear_cmd.py b/AstrakoBot/modules/clear_cmd.py index 75644a6729..63be30c39a 100644 --- a/AstrakoBot/modules/clear_cmd.py +++ b/AstrakoBot/modules/clear_cmd.py @@ -4,7 +4,7 @@ import AstrakoBot.modules.sql.clear_cmd_sql as sql from AstrakoBot import dispatcher from AstrakoBot.modules.helper_funcs.chat_status import user_admin, connection_status - +from AstrakoBot.modules.helper_funcs.string_handling import parse_to_seconds @user_admin @connection_status @@ -28,7 +28,7 @@ def clearcmd(update: Update, context: CallbackContext): "lyrics", "magisk", "miui", - "notes", + "notes", "orangefox", "phh", "ping", @@ -53,7 +53,7 @@ def clearcmd(update: Update, context: CallbackContext): if commands: msg += "*Command - Time*\n" for cmd in commands: - msg += f"`{cmd.cmd} - {cmd.time} secs`\n" + msg += f"`{cmd.cmd} - {cmd.time} secs`\n" else: msg = f"No deletion time has been set for any command in *{chat.title}*" @@ -83,14 +83,17 @@ def clearcmd(update: Update, context: CallbackContext): if time == "restore": sql.del_clearcmd(chat.id, cmd) msg = f"Removed `{cmd}` from list" - elif (5 <= int(time) <= 300): - sql.set_clearcmd(chat.id, cmd, time) - msg = f"`{cmd}` output will be deleted after *{time}* seconds in *{chat.title}*" else: - msg = "Time must be between 5 and 300 seconds" + time = parse_to_seconds(time) + if not time: + msg = "Time must be in seconds or minutes" + elif (5 <= time <= 300): + sql.set_clearcmd(chat.id, cmd, time) + msg = f"`{cmd}` output will be deleted after *{time}* seconds in *{chat.title}*" + else: + msg = "Time must be between 5 and 300 seconds" else: msg = "Specify a valid command. Use `/clearcmd list` to see available commands" - else: msg = "I don't understand what are you trying to do. Check module help for more details" diff --git a/AstrakoBot/modules/cron_jobs.py b/AstrakoBot/modules/cron_jobs.py new file mode 100644 index 0000000000..92c7e43888 --- /dev/null +++ b/AstrakoBot/modules/cron_jobs.py @@ -0,0 +1,117 @@ +import glob +import os +import shutil +import datetime +import subprocess +from time import sleep +from telegram.ext.commandhandler import CommandHandler + +from telegram.update import Update +from telegram.ext.callbackcontext import CallbackContext + +from AstrakoBot import DB_NAME, OWNER_ID, dispatcher, LOGGER as log, BACKUP_PASS +from AstrakoBot.modules.helper_funcs.chat_status import owner_plus + +@owner_plus +def backup_now(_: Update, ctx: CallbackContext): + cronjob.run(dispatcher=dispatcher) + +@owner_plus +def stop_jobs(update: Update, _: CallbackContext): + print(j.stop()) + update.effective_message.reply_text("Scheduler has been shut down") + +@owner_plus +def start_jobs(update: Update, _: CallbackContext): + print(j.start()) + update.effective_message.reply_text("Scheduler started") + +zip_pass = BACKUP_PASS + +def backup_db(_: CallbackContext): + bot = dispatcher.bot + tmpmsg = "Performing backup, Please wait..." + tmp = bot.send_message(OWNER_ID, tmpmsg) + datenow = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + dbbkpname = "db_{}_{}.tar".format(bot.username, datenow) + bkplocation = "backups/{}".format(datenow) + bkpcmd = "pg_dump {} --format=tar > {}/{}".format(DB_NAME, bkplocation, dbbkpname) + + if not os.path.exists(bkplocation): + os.makedirs(bkplocation) + log.info("performing db backup") + loginfo = "db backup" + term(bkpcmd, loginfo) + if not os.path.exists('{}/{}'.format(bkplocation, dbbkpname)): + bot.send_message(OWNER_ID, "An error occurred during the db backup") + tmp.edit_text("Backup Failed!") + sleep(8) + tmp.delete() + return + else: + log.info("copying config, and logs to backup location") + if os.path.exists('log.txt'): + print("logs copied") + shutil.copyfile('log.txt', '{}/log.txt'.format(bkplocation)) + if os.path.exists('AstrakoBot/config.py'): + print("config copied") + shutil.copyfile('AstrakoBot/config.py', '{}/config.py'.format(bkplocation)) + log.info("zipping the backup") + zipcmd = "zip -s 45m --password '{}' {} {}/*".format(zip_pass, bkplocation, bkplocation) + zipinfo = "zipping db backup" + log.info("zip started") + term(zipcmd, zipinfo) + log.info("zip done") + sleep(1) + for file in glob.glob('backups/{}'.format(f'{datenow}.z*')): + with open(file, 'rb') as bkp: + nm = "{} backup \n".format(bot.username) + datenow + try: + bot.send_document(OWNER_ID, + document=bkp, + caption=nm, + timeout=20 + ) + except Exception as err: + bot.send_message(OWNER_ID, "Failed to send backup:\n {}".format(err)) + log.info("removing zipped files") + shutil.rmtree("backups/{}".format(datenow)) + for file in glob.glob('backups/{}'.format(f'202*.z*')): + if os.path.isfile(file) \ + and os.path.getmtime(file) < (datetime.datetime.now() - datetime.timedelta(days=5)).timestamp(): + os.remove(file) + + log.info("backup done") + tmp.edit_text("Backup complete!") + sleep(5) + tmp.delete() + +@owner_plus +def del_bkp_fldr(update: Update, _: CallbackContext): + shutil.rmtree("backups") + update.effective_message.reply_text("'backups' directory has been purged!") + +def term(cmd, info): + process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True + ) + stdout, stderr = process.communicate() + stderr = stderr.decode() + stdout = stdout.decode() + if stdout: + log.info(f"{info} successful!") + log.info(f"{stdout}") + if stderr: + log.error(f"error while running {info}") + log.info(f"{stderr}") + +from AstrakoBot import updater as u +# run the backup daliy at 1:00 +twhen = datetime.datetime.strptime('01:00', '%H:%M').time() +j = u.job_queue +cronjob = j.run_daily(callback=backup_db, name="database backups", time=twhen) + +dispatcher.add_handler(CommandHandler("backupdb", backup_now, run_async=True)) +dispatcher.add_handler(CommandHandler("stopjobs", stop_jobs, run_async=True)) +dispatcher.add_handler(CommandHandler("startjobs", start_jobs, run_async=True)) +dispatcher.add_handler(CommandHandler("purgebackups", del_bkp_fldr, run_async=True)) diff --git a/AstrakoBot/modules/currency_converter.py b/AstrakoBot/modules/currency_converter.py index cd0acccafb..7453e72c90 100644 --- a/AstrakoBot/modules/currency_converter.py +++ b/AstrakoBot/modules/currency_converter.py @@ -5,7 +5,6 @@ from AstrakoBot.modules.sql.clear_cmd_sql import get_clearcmd from AstrakoBot.modules.helper_funcs.misc import delete - def convert(update: Update, context: CallbackContext): chat = update.effective_chat args = update.effective_message.text.split(" ") @@ -13,30 +12,52 @@ def convert(update: Update, context: CallbackContext): if len(args) == 4: try: orig_cur_amount = float(args[1]) - except ValueError: update.effective_message.reply_text("Invalid amount of currency") return orig_cur = args[2].upper() - new_cur = args[3].upper() + api_version = "v6" + data = None - request_url = ( - f"https://www.alphavantage.co/query" - f"?function=CURRENCY_EXCHANGE_RATE" - f"&from_currency={orig_cur}" - f"&to_currency={new_cur}" - f"&apikey={CASH_API_KEY}" - ) - response = requests.get(request_url).json() try: - current_rate = float( - response["Realtime Currency Exchange Rate"]["5. Exchange Rate"] - ) - except KeyError: - update.effective_message.reply_text("Currency not supported.") + request_url = f"https://v6.exchangerate-api.com/v6/{CASH_API_KEY}/latest/{orig_cur}" + response = requests.get(request_url) + data = response.json() + + if data.get('result') == 'error': + api_version = "v4" + request_url = f"https://api.exchangerate-api.com/v4/latest/{orig_cur}" + response = requests.get(request_url) + data = response.json() + + except Exception as e: + update.effective_message.reply_text(f"API Error: {str(e)}") + return + + if not data or 'result' in data and data['result'] == 'error': + error_key = 'error_type' if api_version == "v4" else 'error-type' + error_msg = data.get(error_key, 'Unknown API error') if data else 'Empty API response' + update.effective_message.reply_text(f"Error: {error_msg}") return + + if api_version == "v6": + if not data.get('conversion_rates'): + update.effective_message.reply_text("Invalid API response format") + return + rates = data['conversion_rates'] + else: + if not data.get('rates'): + update.effective_message.reply_text("Invalid API response format") + return + rates = data['rates'] + + if new_cur not in rates: + update.effective_message.reply_text(f"Currency {new_cur} is not supported.") + return + + current_rate = rates[new_cur] new_cur_amount = round(orig_cur_amount * current_rate, 5) delmsg = update.effective_message.reply_text( f"{orig_cur_amount} {orig_cur} = {new_cur_amount} {new_cur}" @@ -47,16 +68,13 @@ def convert(update: Update, context: CallbackContext): else: delmsg = update.effective_message.reply_text( - f"*Invalid Args!!:* Required 3 but passed {len(args) -1}", + f"*Invalid Arguments!* Required 3 parameters but got {len(args)-1}", parse_mode=ParseMode.MARKDOWN, ) cleartime = get_clearcmd(chat.id, "cash") - if cleartime: context.dispatcher.run_async(delete, delmsg, cleartime.time) - CONVERTER_HANDLER = CommandHandler("cash", convert, run_async=True) - dispatcher.add_handler(CONVERTER_HANDLER) diff --git a/AstrakoBot/modules/cust_filters.py b/AstrakoBot/modules/cust_filters.py index 145d572762..c6125eba3e 100644 --- a/AstrakoBot/modules/cust_filters.py +++ b/AstrakoBot/modules/cust_filters.py @@ -84,7 +84,7 @@ def list_handlers(update: Update, context: CallbackContext): if len(entry) + len(filter_list) > telegram.MAX_MESSAGE_LENGTH: deletion(update, context, send_message( update.effective_message, - filter_list.format(chat_name), + filter_list.replace("{}", chat_name, 1), parse_mode=telegram.ParseMode.MARKDOWN, )) filter_list = entry @@ -93,7 +93,7 @@ def list_handlers(update: Update, context: CallbackContext): deletion(update, context, send_message( update.effective_message, - filter_list.format(chat_name), + filter_list.replace("{}", chat_name, 1), parse_mode=telegram.ParseMode.MARKDOWN, )) diff --git a/AstrakoBot/modules/debug.py b/AstrakoBot/modules/debug.py index 7b56b33d7e..82d8806e8c 100644 --- a/AstrakoBot/modules/debug.py +++ b/AstrakoBot/modules/debug.py @@ -54,6 +54,10 @@ async def i_do_nothing_yes(event): @dev_plus def logs(update: Update, context: CallbackContext): user = update.effective_user + if os.path.getsize('log.txt') > (45 * 1024 * 1024): + update.effective_message.reply_text("Log file is too big to be sent!") + return + with open("log.txt", "rb") as f: context.bot.send_document(document=f, filename=f.name, chat_id=user.id) diff --git a/AstrakoBot/modules/disable.py b/AstrakoBot/modules/disable.py index f7f7f9af85..2921b1f432 100644 --- a/AstrakoBot/modules/disable.py +++ b/AstrakoBot/modules/disable.py @@ -22,9 +22,9 @@ from AstrakoBot.modules.helper_funcs.chat_status import ( connection_status, - is_user_admin, user_admin, ) + from AstrakoBot.modules.helper_funcs.admin_status import user_is_admin from AstrakoBot.modules.sql import disable_sql as sql from telegram.ext.dispatcher import run_async @@ -65,7 +65,7 @@ def check_update(self, update): return None chat = update.effective_chat user = update.effective_user - if user.id == 1087968824: + if not user.id or user.id == 1087968824: user_id = chat.id else: user_id = user.id @@ -78,7 +78,7 @@ def check_update(self, update): # check if command was disabled is_disabled = command[ 0 - ] in ADMIN_CMDS and is_user_admin(chat, user.id) + ] in ADMIN_CMDS and user_is_admin(chat, user.id) if not is_disabled: return None else: diff --git a/AstrakoBot/modules/feds.py b/AstrakoBot/modules/feds.py index cafe5fc48b..4fc667490c 100644 --- a/AstrakoBot/modules/feds.py +++ b/AstrakoBot/modules/feds.py @@ -6,6 +6,7 @@ import time import uuid from io import BytesIO +from html import escape import AstrakoBot.modules.sql.feds_sql as sql from AstrakoBot import ( @@ -18,9 +19,9 @@ dispatcher, ) from AstrakoBot.modules.disable import DisableAbleCommandHandler +from AstrakoBot.modules.helper_funcs.admin_status import get_bot_member, user_is_admin from AstrakoBot.modules.helper_funcs.alternate import send_message from AstrakoBot.modules.helper_funcs.chat_status import ( - is_user_admin, can_delete, ) from AstrakoBot.modules.helper_funcs.extraction import ( @@ -28,9 +29,10 @@ extract_user, extract_user_fban, ) +from AstrakoBot.modules.helper_funcs.admin_status import user_is_admin from AstrakoBot.modules.helper_funcs.string_handling import markdown_parser from telegram import ( - InlineKeyboardButton, + Chat, InlineKeyboardButton, InlineKeyboardMarkup, MessageEntity, ParseMode, @@ -41,7 +43,7 @@ CallbackContext, CallbackQueryHandler, CommandHandler, - run_async, + Filters, MessageHandler, run_async, ) from telegram.utils.helpers import mention_html, mention_markdown @@ -54,6 +56,7 @@ # Time spended on updating version to v2 = 26+ hours by @AyraHikari # Total spended for making this features is 68+ hours # LOGGER.info("Original federation module by MrYacha, reworked by Mizukito Akito (@peaktogoo) on Telegram.") +from AstrakoBot.modules.sql.users_sql import get_user_com_chats FBAN_ERRORS = { "User is an administrator of the chat", @@ -92,13 +95,13 @@ def new_fed(update: Update, context: CallbackContext): "Federations can only be created by privately messaging me." ) return - if len(message.text) == 1: + if not context.args: send_message( update.effective_message, "Please write the name of the federation!" ) return fednam = message.text.split(None, 1)[1] - if not fednam == "": + if fednam: fed_id = str(uuid.uuid4()) fed_name = fednam LOGGER.info(fed_id) @@ -125,7 +128,7 @@ def new_fed(update: Update, context: CallbackContext): try: bot.send_message( EVENT_LOGS, - "New Federation: {}\nID:
{}
".format(fed_name, fed_id), + "New Federation: {}\nID:
{}
".format(escape(str(fed_name)), fed_id), parse_mode=ParseMode.HTML, ) except: @@ -210,7 +213,7 @@ def fed_chat(update: Update, context: CallbackContext): fed_id = sql.get_fed_id(chat.id) user_id = update.effective_message.from_user.id - if not is_user_admin(update.effective_chat, user_id): + if not user_is_admin(update.effective_chat, user_id): update.effective_message.reply_text( "You must be an admin to execute this command" ) @@ -225,7 +228,7 @@ def fed_chat(update: Update, context: CallbackContext): info = sql.get_fed_info(fed_id) text = "This group is part of the following federation:" - text += "\n{} (ID: {})".format(info["fname"], fed_id) + text += "\n{} (ID: {})".format(escape(str(info["fname"])), fed_id) update.effective_message.reply_text(text, parse_mode=ParseMode.HTML) @@ -350,25 +353,18 @@ def user_join_fed(update: Update, context: CallbackContext): if is_user_fed_owner(fed_id, user.id) or user.id in SUDO_USERS: user_id = extract_user(msg, args) - if user_id: - user = bot.get_chat(user_id) - elif not msg.reply_to_message and not args: - user = msg.from_user - elif not msg.reply_to_message and ( - not args - or ( - len(args) >= 1 - and not args[0].startswith("@") - and not args[0].isdigit() - and not msg.parse_entities([MessageEntity.TEXT_MENTION]) - ) - ): - msg.reply_text("I cannot extract user from this message") + if not user_id: + msg.reply_text("Who are you trying to promote?") return - else: - LOGGER.warning("error") + user = bot.get_chat(user_id) + getuser = sql.search_user_in_fed(fed_id, user_id) fed_id = sql.get_fed_id(chat.id) + if not fed_id: + update.effective_message.reply_text( + "This group is not in any federation!" + ) + return info = sql.get_fed_info(fed_id) get_owner = ast.literal_eval(info["fusers"])["owner"] get_owner = bot.get_chat(get_owner).id @@ -410,28 +406,13 @@ def user_demote_fed(update: Update, context: CallbackContext): fed_id = sql.get_fed_id(chat.id) - if is_user_fed_owner(fed_id, user.id): + if is_user_fed_owner(fed_id, user.id) or user.id in SUDO_USERS: msg = update.effective_message user_id = extract_user(msg, args) - if user_id: - user = bot.get_chat(user_id) - - elif not msg.reply_to_message and not args: - user = msg.from_user - - elif not msg.reply_to_message and ( - not args - or ( - len(args) >= 1 - and not args[0].startswith("@") - and not args[0].isdigit() - and not msg.parse_entities([MessageEntity.TEXT_MENTION]) - ) - ): - msg.reply_text("I cannot extract user from this message") + if not user_id: + msg.reply_text("Who are you trying to demote?") return - else: - LOGGER.warning("error") + user = bot.get_chat(user_id) if user_id == bot.id: update.effective_message.reply_text( @@ -476,10 +457,6 @@ def fed_info(update: Update, context: CallbackContext): return info = sql.get_fed_info(fed_id) - if is_user_fed_admin(fed_id, user.id) is False: - update.effective_message.reply_text("Only a federation admin can do this!") - return - owner = bot.get_chat(info["owner"]) try: owner_name = owner.first_name + " " + owner.last_name @@ -494,7 +471,7 @@ def fed_info(update: Update, context: CallbackContext): text = "ā„¹ļø Federation Information:" text += "\nFedID: {}".format(fed_id) - text += "\nName: {}".format(info["fname"]) + text += "\nName: {}".format(escape(str(info["fname"]))) text += "\nCreator: {}".format(mention_html(owner.id, owner_name)) text += "\nAll Admins: {}".format(TotalAdminFed) getfban = sql.get_all_fban_users(fed_id) @@ -533,31 +510,45 @@ def fed_admin(update: Update, context: CallbackContext): chat = update.effective_chat info = sql.get_fed_info(fed_id) - text = "Federation Admin {}:\n\n".format(info["fname"]) + text = "Federation Admin {}:\n\n".format(escape(str(info["fname"]))) text += "šŸ‘‘ Owner:\n" owner = bot.get_chat(info["owner"]) try: owner_name = owner.first_name + " " + owner.last_name - except: - owner_name = owner.first_name - text += " • {}\n".format(mention_html(owner.id, owner_name)) + except BaseException: + owner_name = owner.first_name or 'Deleted' + text += " • {} ({})\n".format(mention_html(owner.id, owner_name), escape(str(owner.id))) members = sql.all_fed_members(fed_id) + zombies = [] if len(members) == 0: text += "\nšŸ”± There are no admins in this federation" else: text += "\nšŸ”± Admin:\n" for x in members: - user = bot.get_chat(x) - text += " • {}\n".format(mention_html(user.id, user.first_name)) + try: + user = bot.get_chat(x) + except BadRequest: + continue + + if not user.first_name: + zombies.append(user.id) + continue + text += " • {} ({})\n".format(mention_html(user.id, user.first_name), escape(str(user.id))) + + if zombies: + text += "\n🧟 Zombies:\n" + for user_id in zombies: + text += " • {}\n".format(escape(str(user_id))) update.effective_message.reply_text(text, parse_mode=ParseMode.HTML) def fed_ban(update: Update, context: CallbackContext): - bot, args = context.bot, context.args - chat = update.effective_chat - user = update.effective_user + bot, args = context.bot, context.args # type: telegram.Bot, List[str] + chat = update.effective_chat # Type: Optional[Chat] + user = update.effective_user # Type: Optional[User] + silent: bool = False if chat.type == "private": send_message( @@ -591,6 +582,12 @@ def fed_ban(update: Update, context: CallbackContext): message.reply_text("You don't seem to be referring to a user") return + if not reason: + message.reply_text( + "You must provide a reason!" + ) + return + if user_id == bot.id: message.reply_text( "What is funnier than kicking the group creator? Self sacrifice." @@ -632,7 +629,7 @@ def fed_ban(update: Update, context: CallbackContext): if not str(user_id).isdigit(): send_message(update.effective_message, excp.message) return - elif len(str(user_id)) != 9: + elif len(str(user_id)) not in [9, 10]: send_message(update.effective_message, "That's not a user!") return isvalid = False @@ -656,7 +653,7 @@ def fed_ban(update: Update, context: CallbackContext): # starting = "The reason fban is replaced for {} in the Federation {}.".format(user_target, fed_name) # send_message(update.effective_message, starting, parse_mode=ParseMode.HTML) - # if reason == "": + # if not reason: # reason = "No reason given." temp = sql.un_fban_user(fed_id, fban_user_id) @@ -688,7 +685,7 @@ def fed_ban(update: Update, context: CallbackContext): "\nUser: {}" "\nUser ID: {}" "\nReason: {}".format( - fed_name, + escape(str(fed_name)), mention_html(user.id, user.first_name), user_target, fban_user_id, @@ -698,33 +695,33 @@ def fed_ban(update: Update, context: CallbackContext): ) # Send message to owner if fednotif is enabled if getfednotif: - bot.send_message( - info["owner"], - "FedBan reason updated" - "\nFederation: {}" - "\nFederation Admin: {}" - "\nUser: {}" - "\nUser ID: {}" - "\nReason: {}".format( - fed_name, - mention_html(user.id, user.first_name), - user_target, - fban_user_id, - reason, - ), - parse_mode="HTML", - ) + try: + bot.send_message( + info["owner"], + "FedBan reason updated" + "\nFederation: {}" + "\nFederation Admin: {}" + "\nUser: {}" + "\nUser ID: {}" + "\nReason: {}".format( + escape(str(fed_name)), + mention_html(user.id, user.first_name), + user_target, + fban_user_id, + reason, + ), + parse_mode="HTML", + ) + except Unauthorized: + pass if message.text.startswith("/s"): silent = True if not can_delete(chat, context.bot.id): return "" - else: - silent = False # If fedlog is set, then send message, except fedlog is current chat - get_fedlog = sql.get_fed_log(fed_id) - if get_fedlog: + if get_fedlog := sql.get_fed_log(fed_id): if int(get_fedlog) != int(chat.id): bot.send_message( get_fedlog, @@ -734,7 +731,7 @@ def fed_ban(update: Update, context: CallbackContext): "\nUser: {}" "\nUser ID: {}" "\nReason: {}".format( - fed_name, + escape(str(fed_name)), mention_html(user.id, user.first_name), user_target, fban_user_id, @@ -742,7 +739,17 @@ def fed_ban(update: Update, context: CallbackContext): ), parse_mode="HTML", ) + common_chats = get_user_com_chats(fban_user_id) for fedschat in fed_chats: + if fedschat not in common_chats: + continue + if not get_bot_member(fedschat).can_restrict_members: + sql.chat_leave_fed(fedschat) + bot.send_message( + chat.id, + "I don't have rights to restrict users on this chat, left fed!", + ) + continue try: # Do not spam all fed chats """ @@ -751,9 +758,9 @@ def fed_ban(update: Update, context: CallbackContext): "\nFederation Admin: {}" \ "\nUser: {}" \ "\nUser ID: {}" \ - "\nReason: {}".format(fed_name, mention_html(user.id, user.first_name), user_target, fban_user_id, reason), parse_mode="HTML") + "\nReason: {}".format(escape(str(fed_name)), mention_html(user.id, user.first_name), user_target, fban_user_id, reason), parse_mode="HTML") """ - bot.kick_chat_member(fedschat, fban_user_id) + bot.ban_chat_member(fedschat, fban_user_id) except BadRequest as excp: if excp.message in FBAN_ERRORS: try: @@ -767,13 +774,25 @@ def fed_ban(update: Update, context: CallbackContext): ) continue elif excp.message == "User_id_invalid": + LOGGER.debug( + "fban error\nUnable to fban user {} in fed {} in chat {} \n\nreason: {}".format( + fban_user_id, fed_id, chat.id, str(excp)) + ) break else: LOGGER.warning( - "Could not fban on {} because: {}".format(chat, excp.message) + "Couldn't fban user {} in fed {} in chat {} \n\nreason: {}".format(fban_user_id, fed_id, chat.id, str(excp)) ) - except TelegramError: + except TelegramError as e: + LOGGER.debug( + "fban error, (TelegramError)\nUnable to fban user {} in fed {} in chat {} \n\nreason: {}".format( + fban_user_id, fed_id, chat.id, str(e)) + ) pass + except Exception as e: + LOGGER.warning( + "fban error\nUnable to fban user {} in fed {} in chat {} \n\nreason: {}".format(fban_user_id, fed_id, chat.id, str(e)) + ) # Also do not spam all fed admins """ send_to_list(bot, FEDADMIN, @@ -782,7 +801,7 @@ def fed_ban(update: Update, context: CallbackContext): "\nFederation Admin: {}" \ "\nUser: {}" \ "\nUser ID: {}" \ - "\nReason: {}".format(fed_name, mention_html(user.id, user.first_name), user_target, fban_user_id, reason), + "\nReason: {}".format(escape(str(fed_name)), mention_html(user.id, user.first_name), user_target, fban_user_id, reason), html=True) """ @@ -792,8 +811,17 @@ def fed_ban(update: Update, context: CallbackContext): for fedsid in subscriber: all_fedschat = sql.all_fed_chats(fedsid) for fedschat in all_fedschat: + if fedsid not in common_chats: + continue + if not get_bot_member(fedschat).can_restrict_members: + sql.chat_leave_fed(fedschat) + bot.send_message( + chat.id, + "I don't have rights to restrict users on this chat, left fed!", + ) + continue try: - bot.kick_chat_member(fedschat, fban_user_id) + bot.ban_chat_member(fedschat, fban_user_id) except BadRequest as excp: if excp.message in FBAN_ERRORS: try: @@ -808,6 +836,10 @@ def fed_ban(update: Update, context: CallbackContext): ) continue elif excp.message == "User_id_invalid": + LOGGER.debug( + "fban error\nUnable to fban user {} in fed {} in chat {} \n\nreason: {}".format( + fban_user_id, fed_id, chat.id, str(excp)) + ) break else: LOGGER.warning( @@ -815,9 +847,25 @@ def fed_ban(update: Update, context: CallbackContext): fedschat, excp.message ) ) - except TelegramError: + except TelegramError as e: + LOGGER.debug( + "fban error, (TelegramError)\nUnable to fban user {} in fed {} in chat {} \n\nreason: {}".format( + fban_user_id, fed_id, chat.id, str(e)) + ) pass + except Exception as e: + LOGGER.warning( + "fban error\nUnable to fban user {} in fed {} in chat {} \n\nreason: {}".format( + fban_user_id, fed_id, chat.id, str(e)) + ) # send_message(update.effective_message, "Fedban Reason has been updated.") + if silent: + try: + if message.reply_to_message: + message.reply_to_message.delete() + message.delete() + except: + pass return fed_name = info["fname"] @@ -826,7 +874,7 @@ def fed_ban(update: Update, context: CallbackContext): # user_target, fed_name) # update.effective_message.reply_text(starting, parse_mode=ParseMode.HTML) - # if reason == "": + # if not reason: # reason = "No reason given." x = sql.fban_user( @@ -854,7 +902,7 @@ def fed_ban(update: Update, context: CallbackContext): "\nUser: {}" "\nUser ID: {}" "\nReason: {}".format( - fed_name, + escape(str(fed_name)), mention_html(user.id, user.first_name), user_target, fban_user_id, @@ -867,30 +915,31 @@ def fed_ban(update: Update, context: CallbackContext): silent = True if not can_delete(chat, context.bot.id): return "" - else: - silent = False # Send message to owner if fednotif is enabled if getfednotif: - bot.send_message( - info["owner"], - "New FedBan" - "\nFederation: {}" - "\nFederation Admin: {}" - "\nUser: {}" - "\nUser ID: {}" - "\nReason: {}".format( - fed_name, - mention_html(user.id, user.first_name), - user_target, - fban_user_id, - reason, - ), - parse_mode="HTML", - ) + try: + bot.send_message( + info["owner"], + "New FedBan" + "\nFederation: {}" + "\nFederation Admin: {}" + "\nUser: {}" + "\nUser ID: {}" + "\nReason: {}".format( + escape(str(fed_name)), + mention_html(user.id, user.first_name), + user_target, + fban_user_id, + reason, + ), + parse_mode="HTML", + ) + except Unauthorized: + pass + # If fedlog is set, then send message, except fedlog is current chat - get_fedlog = sql.get_fed_log(fed_id) - if get_fedlog: + if get_fedlog:= sql.get_fed_log(fed_id): if int(get_fedlog) != int(chat.id): bot.send_message( get_fedlog, @@ -900,7 +949,7 @@ def fed_ban(update: Update, context: CallbackContext): "\nUser: {}" "\nUser ID: {}" "\nReason: {}".format( - fed_name, + escape(str(fed_name)), mention_html(user.id, user.first_name), user_target, fban_user_id, @@ -908,9 +957,19 @@ def fed_ban(update: Update, context: CallbackContext): ), parse_mode="HTML", ) - chats_in_fed = 0 + # chats_in_fed = 0 + common_chats = get_user_com_chats(fban_user_id) for fedschat in fed_chats: - chats_in_fed += 1 + if fedschat not in common_chats: + continue + if not get_bot_member(fedschat).can_restrict_members: + sql.chat_leave_fed(fedschat) + bot.send_message( + chat.id, + "I don't have rights to restrict users on this chat, left fed!", + ) + continue + # chats_in_fed += 1 try: # Do not spamming all fed chats """ @@ -919,20 +978,33 @@ def fed_ban(update: Update, context: CallbackContext): "\nFederation Admin: {}" \ "\nUser: {}" \ "\nUser ID: {}" \ - "\nReason: {}".format(fed_name, mention_html(user.id, user.first_name), user_target, fban_user_id, reason), parse_mode="HTML") + "\nReason: {}".format(escape(str(fed_name)), mention_html(user.id, user.first_name), user_target, fban_user_id, reason), parse_mode="HTML") """ - bot.kick_chat_member(fedschat, fban_user_id) + bot.ban_chat_member(fedschat, fban_user_id) except BadRequest as excp: if excp.message in FBAN_ERRORS: pass elif excp.message == "User_id_invalid": - break + LOGGER.debug( + "fban error\nUnable to fban user {} in fed {} in chat {} \n\nreason: {}".format( + fban_user_id, fed_id, chat.id, str(excp)) + ) + # break else: LOGGER.warning( - "Could not fban on {} because: {}".format(chat, excp.message) + "Couldn't fban user {} in fed {} in chat {} \n\nreason: {}".format(fban_user_id, fed_id, + chat.id, str(excp)) ) - except TelegramError: + except TelegramError as e: + LOGGER.debug( + "fban error, (TelegramError)\nUnable to fban user {} in fed {} in chat {} \n\nreason: {}".format( + fban_user_id, fed_id, chat.id, str(excp)) + ) pass + except Exception as e: + LOGGER.warning( + "fban error\nUnable to fban user {} in fed {} in chat {} \n\nreason: {}".format(fban_user_id, fed_id, chat.id, str(e)) + ) # Also do not spamming all fed admins """ @@ -942,51 +1014,76 @@ def fed_ban(update: Update, context: CallbackContext): "\nFederation Admin: {}" \ "\nUser: {}" \ "\nUser ID: {}" \ - "\nReason: {}".format(fed_name, mention_html(user.id, user.first_name), user_target, fban_user_id, reason), + "\nReason: {}".format(escape(str(fed_name)), mention_html(user.id, user.first_name), user_target, fban_user_id, reason), html=True) """ - # Fban for fed subscriber - subscriber = list(sql.get_subscriber(fed_id)) - if len(subscriber) != 0: - for fedsid in subscriber: - all_fedschat = sql.all_fed_chats(fedsid) - for fedschat in all_fedschat: - try: - bot.kick_chat_member(fedschat, fban_user_id) - except BadRequest as excp: - if excp.message in FBAN_ERRORS: - try: - dispatcher.bot.getChat(fedschat) - except Unauthorized: - targetfed_id = sql.get_fed_id(fedschat) - sql.unsubs_fed(fed_id, targetfed_id) - LOGGER.info( - "Chat {} has unsubbed from the fed {} because I was kicked".format( - fedschat, info["fname"] - ) - ) - continue - elif excp.message == "User_id_invalid": - break - else: - LOGGER.warning( - "Unable to execute fban on {} because: {}".format( - fedschat, excp.message + # Fban for fed subscriber + subscriber = list(sql.get_subscriber(fed_id)) + if len(subscriber) != 0: + for fedsid in subscriber: + all_fedschat = sql.all_fed_chats(fedsid) + for fedschat in all_fedschat: + if fedsid not in common_chats: + continue + if not get_bot_member(fedschat).can_restrict_members: + sql.chat_leave_fed(fedschat) + bot.send_message( + chat.id, + "I don't have rights to restrict users on this chat, left fed!", + ) + continue + try: + bot.ban_chat_member(fedschat, fban_user_id) + except BadRequest as excp: + if excp.message in FBAN_ERRORS: + try: + dispatcher.bot.getChat(fedschat) + except Unauthorized: + targetfed_id = sql.get_fed_id(fedschat) + sql.unsubs_fed(fed_id, targetfed_id) + LOGGER.info( + "Chat {} has unsubbed from the fed {} because I was kicked".format( + fedschat, info["fname"] ) ) - except TelegramError: - pass + continue + elif excp.message == "User_id_invalid": + LOGGER.debug( + "fban error\nUnable to fban user {} in fed {} in chat {} \n\nreason: {}".format( + fban_user_id, fed_id, chat.id, str(excp)) + ) + break + else: + LOGGER.warning( + "Unable to execute fban on {} because: {}".format( + fedschat, excp.message + ) + ) + except TelegramError as e: + LOGGER.debug( + "fban error, (TelegramError)\nUnable to fban user {} in fed {} in chat {} \n\nreason: {}".format( + fban_user_id, fed_id, chat.id, str(e)) + ) + pass + except Exception as e: + LOGGER.warning( + "fban error\nUnable to fban user {} in fed {} in chat {} \n\nreason: {}".format( + fban_user_id, fed_id, chat.id, str(e)) + ) # if chats_in_fed == 0: # send_message(update.effective_message, "Fedban affected 0 chats. ") # elif chats_in_fed > 0: # send_message(update.effective_message, # "Fedban affected {} chats. ".format(chats_in_fed)) - + if silent: - if message.reply_to_message: - message.reply_to_message.delete() - message.delete() + try: + if message.reply_to_message: + message.reply_to_message.delete() + message.delete() + except: + pass return @@ -1071,7 +1168,7 @@ def unfban(update: Update, context: CallbackContext): "\nFederation Admin: {}" "\nUser: {}" "\nUser ID: {}".format( - info["fname"], + escape(str(info["fname"])), mention_html(user.id, user.first_name), user_target, fban_user_id, @@ -1080,20 +1177,24 @@ def unfban(update: Update, context: CallbackContext): ) # Send message to owner if fednotif is enabled if getfednotif: - bot.send_message( - info["owner"], - "Un-FedBan" - "\nFederation: {}" - "\nFederation Admin: {}" - "\nUser: {}" - "\nUser ID: {}".format( - info["fname"], - mention_html(user.id, user.first_name), - user_target, - fban_user_id, - ), - parse_mode="HTML", - ) + try: + bot.send_message( + info["owner"], + "Un-FedBan" + "\nFederation: {}" + "\nFederation Admin: {}" + "\nUser: {}" + "\nUser ID: {}".format( + escape(str(info["fname"])), + mention_html(user.id, user.first_name), + user_target, + fban_user_id, + ), + parse_mode="HTML", + ) + except Unauthorized: + pass + # If fedlog is set, then send message, except fedlog is current chat get_fedlog = sql.get_fed_log(fed_id) if get_fedlog: @@ -1105,7 +1206,7 @@ def unfban(update: Update, context: CallbackContext): "\nFederation Admin: {}" "\nUser: {}" "\nUser ID: {}".format( - info["fname"], + escape(str(info["fname"])), mention_html(user.id, user.first_name), user_target, fban_user_id, @@ -1379,7 +1480,7 @@ def fed_ban_list(update: Update, context: CallbackContext): getfban = sql.get_all_fban_users(fed_id) if len(getfban) == 0: update.effective_message.reply_text( - "The federation ban list of {} is empty".format(info["fname"]), + "The federation ban list of {} is empty".format(escape(str(info["fname"]))), parse_mode=ParseMode.HTML, ) return @@ -1476,13 +1577,13 @@ def fed_ban_list(update: Update, context: CallbackContext): return text = "{} users have been banned from the federation {}:\n".format( - len(getfban), info["fname"] + len(getfban), escape(str(info["fname"])) ) for users in getfban: getuserinfo = sql.get_all_fban_users_target(fed_id, users) if getuserinfo is False: text = "There are no users banned from the federation {}".format( - info["fname"] + escape(str(info["fname"])) ) break user_name = getuserinfo["first_name"] @@ -1567,6 +1668,7 @@ def fed_chats(update: Update, context: CallbackContext): bot, args = context.bot, context.args chat = update.effective_chat user = update.effective_user + chat_name = "" if chat.type == "private": send_message( @@ -1591,12 +1693,12 @@ def fed_chats(update: Update, context: CallbackContext): getlist = sql.all_fed_chats(fed_id) if len(getlist) == 0: update.effective_message.reply_text( - "No users are fbanned from the federation {}".format(info["fname"]), + "No users are fbanned from the federation {}".format(escape(str(info["fname"]))), parse_mode=ParseMode.HTML, ) return - text = "New chat joined the federation {}:\n".format(info["fname"]) + text = "New chat joined the federation {}:\n".format(escape(str(info["fname"]))) for chats in getlist: try: chat_name = dispatcher.bot.getChat(chats).title @@ -1608,7 +1710,10 @@ def fed_chats(update: Update, context: CallbackContext): ) ) continue - text += " • {} ({})\n".format(chat_name, chats) + except BadRequest as excp: + if excp.message in FBAN_ERRORS: + pass + text += " • {} ({})\n".format(chat_name, chats) try: update.effective_message.reply_text(text, parse_mode=ParseMode.HTML) @@ -1701,7 +1806,7 @@ def fed_import_bans(update: Update, context: CallbackContext): reading = file.read().decode("UTF-8") splitting = reading.split("\n") for x in splitting: - if x == "": + if not x: continue try: data = json.loads(x) @@ -1897,7 +2002,7 @@ def fed_stat_user(update: Update, context: CallbackContext): parse_mode="markdown", ) return - if user_name == "" or user_name is None: + if not user_name: user_name = "He/she" if not reason: send_message( @@ -1911,12 +2016,12 @@ def fed_stat_user(update: Update, context: CallbackContext): send_message(update.effective_message, teks, parse_mode="markdown") return user_name, fbanlist = sql.get_user_fbanlist(str(user_id)) - if user_name == "": + if not user_name: try: user_name = bot.get_chat(user_id).first_name except BadRequest: user_name = "He/she" - if user_name == "" or user_name is None: + if not user_name: user_name = "He/she" if len(fbanlist) == 0: send_message( @@ -1934,7 +2039,7 @@ def fed_stat_user(update: Update, context: CallbackContext): elif not msg.reply_to_message and not args: user_id = msg.from_user.id user_name, fbanlist = sql.get_user_fbanlist(user_id) - if user_name == "": + if not user_name: user_name = msg.from_user.first_name if len(fbanlist) == 0: send_message( @@ -2273,21 +2378,54 @@ def is_user_fed_owner(fed_id, user_id): return False -# There's no handler for this yet, but updating for v12 in case its used -def welcome_fed(update: Update, context: CallbackContext): - bot, args = context.bot, context.args +def enforce_fed(update: Update, ctx: CallbackContext): chat = update.effective_chat user = update.effective_user + msg = update.effective_message fed_id = sql.get_fed_id(chat.id) - fban, fbanreason, fbantime = sql.get_fban_user(fed_id, user.id) - if fban: + + if fed_id and not get_bot_member(chat.id).can_restrict_members: + sql.chat_leave_fed(chat.id) + ctx.bot.send_message( + chat.id, + "I don't have rights to restrict users on this chat, left fed!", + ) + return + + if user and not user_is_admin(chat, user.id): + fban, fbanreason, fbantime = sql.get_fban_user(fed_id, user.id) + if fban: + check_and_ban(update, chat, user.id, fbanreason) + + if msg.new_chat_members: + new_members = update.effective_message.new_chat_members + for mem in new_members: + fban, fbanreason, fbantime = sql.get_fban_user(fed_id, mem.id) + if fban: + check_and_ban(update, chat, mem.id, fbanreason) + + # this is optional for now because it needs to check if user is in the chat + # and I rather not make more requests to tg on every reply + # if msg.reply_to_message: + # user = msg.reply_to_message.from_user + # mem = chat.get_member(user.id) + # if mem.status in ["administrator", "creator", "kicked"]: + # return + # fban, fbanreason, fbantime = sql.get_fban_user(fed_id, user.id) + # if fban and user and not user_is_admin(chat, user.id): + # check_and_ban(update, chat, user.id, fbanreason) + + +def check_and_ban(update, chat: Chat, user_id: int, reason: str): + try: + chat.ban_member(user_id) update.effective_message.reply_text( - "This user is banned in the current federation! I will remove him." + f"This user is banned in the current federation! I will remove him.\n{'Reason: {}'.format(reason) if reason else ''}", + allow_sending_without_reply = True ) - bot.kick_chat_member(chat.id, user.id) - return True - else: - return False + except: + pass + def __stats__(): @@ -2430,6 +2568,8 @@ def fed_user_help(update: Update, context: CallbackContext): FED_OWNER_HELP_HANDLER = CommandHandler("fedownerhelp", fed_owner_help, run_async=True) FED_ADMIN_HELP_HANDLER = CommandHandler("fedadminhelp", fed_admin_help, run_async=True) FED_USER_HELP_HANDLER = CommandHandler("feduserhelp", fed_user_help, run_async=True) +FBAN_ENFORCE = MessageHandler(Filters.all & Filters.chat_type.groups, enforce_fed, run_async=True) + dispatcher.add_handler(NEW_FED_HANDLER) dispatcher.add_handler(DEL_FED_HANDLER) @@ -2461,3 +2601,4 @@ def fed_user_help(update: Update, context: CallbackContext): dispatcher.add_handler(FED_OWNER_HELP_HANDLER) dispatcher.add_handler(FED_ADMIN_HELP_HANDLER) dispatcher.add_handler(FED_USER_HELP_HANDLER) +dispatcher.add_handler(FBAN_ENFORCE, 20) diff --git a/AstrakoBot/modules/fun.py b/AstrakoBot/modules/fun.py index 9087412b88..c953cc063a 100644 --- a/AstrakoBot/modules/fun.py +++ b/AstrakoBot/modules/fun.py @@ -5,7 +5,7 @@ import AstrakoBot.modules.fun_strings as fun_strings from AstrakoBot import dispatcher from AstrakoBot.modules.disable import DisableAbleCommandHandler -from AstrakoBot.modules.helper_funcs.chat_status import is_user_admin +from AstrakoBot.modules.helper_funcs.admin_status import user_is_admin from AstrakoBot.modules.helper_funcs.extraction import extract_user from AstrakoBot.modules.helper_funcs.misc import delete from AstrakoBot.modules.sql.clear_cmd_sql import get_clearcmd @@ -13,8 +13,6 @@ from telegram.error import BadRequest from telegram.ext import CallbackContext, run_async -GIF_ID = "CgACAgQAAx0CSVUvGgAC7KpfWxMrgGyQs-GUUJgt-TSO8cOIDgACaAgAAlZD0VHT3Zynpr5nGxsE" - def runs(update: Update, context: CallbackContext): deletion(update, context, update.effective_message.reply_text(random.choice(fun_strings.RUN_STRINGS))) @@ -27,12 +25,17 @@ def sanitize(update: Update, context: CallbackContext): if message.reply_to_message else message.from_user.first_name ) - reply_animation = ( - message.reply_to_message.reply_animation + + reply_msg = ( + message.reply_to_message if message.reply_to_message - else message.reply_animation + else message ) - deletion(update, context, reply_animation(random.choice(fun_strings.GIFS), caption=f"*Sanitizes {name}*")) + + try: + deletion(update, context, reply_msg.reply_animation(random.choice(fun_strings.GIFS), caption=f"*Sanitizes {name}*")) + except BadRequest: + deletion(update, context, reply_msg.reply_text(f"*Sanitizes {name}*")) def slap(update: Update, context: CallbackContext): @@ -54,7 +57,7 @@ def slap(update: Update, context: CallbackContext): if isinstance(temp, list): if temp[2] == "tmute": - if is_user_admin(chat, message.from_user.id): + if user_is_admin(chat, message.from_user.id): reply_text(temp[1]) return @@ -137,20 +140,6 @@ def roll(update: Update, context: CallbackContext): deletion(update, context, update.message.reply_text(random.choice(range(1, 7)))) -def shout(update: Update, context: CallbackContext): - args = context.args - text = " ".join(args) - result = [] - result.append(" ".join(list(text))) - for pos, symbol in enumerate(text[1:]): - result.append(symbol + " " + " " * pos + symbol) - result = list("\n".join(result)) - result[0] = text[0] - result = "".join(result) - msg = "```\n" + result + "```" - deletion(update, context, update.effective_message.reply_text(msg, parse_mode="MARKDOWN")) - - def toss(update: Update, context: CallbackContext): deletion(update, context, update.message.reply_text(random.choice(fun_strings.TOSS))) @@ -212,90 +201,6 @@ def table(update: Update, context: CallbackContext): deletion(update, context, reply_text(random.choice(fun_strings.TABLE))) -normiefont = [ - "a", - "b", - "c", - "d", - "e", - "f", - "g", - "h", - "i", - "j", - "k", - "l", - "m", - "n", - "o", - "p", - "q", - "r", - "s", - "t", - "u", - "v", - "w", - "x", - "y", - "z", -] -weebyfont = [ - "卂", - "乃", - "匚", - "刀", - "乇", - "äø‹", - "厶", - "卄", - "å·„", - "丁", - "长", - "乚", - "从", - "𠘨", - "口", - "å°ø", - "㔿", - "å°ŗ", - "äø‚", - "äø…", - "凵", - "リ", - "å±±", - "乂", - "äø«", - "乙", -] - - -def weebify(update: Update, context: CallbackContext): - args = context.args - message = update.effective_message - string = "" - - if message.reply_to_message: - string = message.reply_to_message.text.lower().replace(" ", " ") - - if args: - string = " ".join(args).lower() - - if not string: - deletion(update, context, message.reply_text("Usage is `/weebify `", parse_mode=ParseMode.MARKDOWN)) - return - - for normiecharacter in string: - if normiecharacter in normiefont: - weebycharacter = weebyfont[normiefont.index(normiecharacter)] - string = string.replace(normiecharacter, weebycharacter) - - if message.reply_to_message: - deletion(update, context, message.reply_to_message.reply_text(string)) - else: - deletion(update, context, message.reply_text(string)) - - def deletion(update: Update, context: CallbackContext, delmsg): chat = update.effective_chat cleartime = get_clearcmd(chat.id, "fun") @@ -335,11 +240,7 @@ def deletion(update: Update, context: CallbackContext, delmsg): DECIDE_HANDLER = DisableAbleCommandHandler("decide", decide, run_async=True) EIGHTBALL_HANDLER = DisableAbleCommandHandler("8ball", eightball, run_async=True) TABLE_HANDLER = DisableAbleCommandHandler("table", table, run_async=True) -SHOUT_HANDLER = DisableAbleCommandHandler("shout", shout, run_async=True) -WEEBIFY_HANDLER = DisableAbleCommandHandler("weebify", weebify, run_async=True) -dispatcher.add_handler(WEEBIFY_HANDLER) -dispatcher.add_handler(SHOUT_HANDLER) dispatcher.add_handler(SANITIZE_HANDLER) dispatcher.add_handler(RUNS_HANDLER) dispatcher.add_handler(SLAP_HANDLER) @@ -366,8 +267,6 @@ def deletion(update: Update, context: CallbackContext, delmsg): "table", "pat", "sanitize", - "shout", - "weebify", "8ball", ] __handlers__ = [ @@ -382,7 +281,5 @@ def deletion(update: Update, context: CallbackContext, delmsg): DECIDE_HANDLER, TABLE_HANDLER, SANITIZE_HANDLER, - SHOUT_HANDLER, - WEEBIFY_HANDLER, EIGHTBALL_HANDLER, ] diff --git a/AstrakoBot/modules/fun_strings.py b/AstrakoBot/modules/fun_strings.py index 02fced88ae..c090aabd25 100644 --- a/AstrakoBot/modules/fun_strings.py +++ b/AstrakoBot/modules/fun_strings.py @@ -20,9 +20,9 @@ ) GIFS = [ - "CgACAgQAAx0CSVUvGgAC7KpfWxMrgGyQs-GUUJgt-TSO8cOIDgACaAgAAlZD0VHT3Zynpr5nGxsE", - "CgACAgUAAx0CU_rCTAABAjdgX1s4NVaeCls6YaH3p43vgdCRwQIAAqsAA4P_MFUYQhyoR-kgpRsE", - "CgACAgUAAx0CU_rCTAABAjdSX1s3fq5iEJ64YeQLKI8cD7CSuSEAAlUBAAJu09hW5iqWB0hTPD4bBA", + "CgACAgUAAxkBAAM5Z9v5Sb4bJusBpEGzUII4mkJy8kkAAqsAA4P_MFVYEKwLUZWtHTYE", + "CgACAgUAAxkBAAM7Z9v5dv9WG5PlhxgWzj7Tj5l3TtUAAlUBAAJu09hWKztDjBmVh5k2BA", + "CgACAgQAAxkBAAM9Z9v5h7FGfqljNG2mBgqsdfq32h8AAmgIAAJWQ9FRSM-c1Iswcac2BA", ] SLAP_SAITAMA_TEMPLATES = ( diff --git a/AstrakoBot/modules/github.py b/AstrakoBot/modules/github.py index 079ff45bb0..991b364052 100755 --- a/AstrakoBot/modules/github.py +++ b/AstrakoBot/modules/github.py @@ -33,7 +33,7 @@ def getphh(index): - recentRelease = api.getReleaseData(api.getData("phhusson/treble_experimentations"), index) + recentRelease = api.getReleaseData(api.getData("TrebleDroid/treble_experimentations"), index) if recentRelease is None: return "The specified release could not be found" author = api.getAuthor(recentRelease) @@ -60,7 +60,7 @@ def getphh(index): # do not async def getData(url, index): if not api.getData(url): - return "Invalid / combo" + return "Invalid / combo.\nAre you sure this repository has a release file?" recentRelease = api.getReleaseData(api.getData(url), index) if recentRelease is None: return "The specified release could not be found" @@ -207,7 +207,7 @@ def delRepo(update: Update, context: CallbackContext): def listRepo(update: Update, context: CallbackContext): chat_id = update.effective_chat.id chat = update.effective_chat - chat_name = chat.title or chat.first or chat.username + chat_name = chat.title or chat.first_name or chat.username repo_list = sql.get_all_repos(str(chat_id)) msg = "*List of repo shotcuts in {}:*\n" des = "You can get repo shortcuts by using `/fetch repo`, or `&repo`.\n" diff --git a/AstrakoBot/modules/global_bans.py b/AstrakoBot/modules/global_bans.py index e2365ebc9a..b31ac68116 100644 --- a/AstrakoBot/modules/global_bans.py +++ b/AstrakoBot/modules/global_bans.py @@ -15,6 +15,7 @@ from telegram.utils.helpers import mention_html import AstrakoBot.modules.sql.global_bans_sql as sql +from AstrakoBot.modules.helper_funcs.admin_status import get_bot_member, user_is_admin from AstrakoBot.modules.sql.users_sql import get_user_com_chats from AstrakoBot import ( DEV_USERS, @@ -30,7 +31,6 @@ dispatcher, ) from AstrakoBot.modules.helper_funcs.chat_status import ( - is_user_admin, support_plus, user_admin, ) @@ -87,6 +87,12 @@ def gban(update: Update, context: CallbackContext): ) return + if not reason: + message.reply_text( + "You must provide a reason!" + ) + return + if int(user_id) in DEV_USERS: message.reply_text("This is a developer user\nI can't act against our own.") return @@ -208,7 +214,7 @@ def gban(update: Update, context: CallbackContext): continue try: - bot.kick_chat_member(chat_id, user_id) + bot.ban_chat_member(chat_id, user_id) gbanned_chats += 1 except BadRequest as excp: @@ -373,10 +379,17 @@ def ungban(update: Update, context: CallbackContext): if ungban_time > 60: ungban_time = round((ungban_time / 60), 2) - message.reply_text(f"Person has been un-gbanned. Took {ungban_time} min") + text = (f"Person has been un-gbanned. Took {ungban_time} min") else: - message.reply_text(f"Person has been un-gbanned. Took {ungban_time} sec") + text = (f"Person has been un-gbanned. Took {ungban_time} sec") + try: + message.reply_text(text) + except BadRequest: + context.bot.send_message( + chat_id=update.effective_chat.id, + text=text + ) @support_plus def gbanlist(update: Update, context: CallbackContext): @@ -414,20 +427,28 @@ def check_and_ban(update, user_id, should_message=True): sw_ban = None if sw_ban: - update.effective_chat.kick_member(user_id) + update.effective_chat.ban_member(user_id) if should_message: - update.effective_message.reply_text( + text = ( f"Alert: this user is globally banned.\n" f"*bans them from here*.\n" f"Appeal chat: {SPAMWATCH_SUPPORT_CHAT}\n" f"User ID: {sw_ban.id}\n" - f"Ban Reason: {html.escape(sw_ban.reason)}", - parse_mode=ParseMode.HTML, + f"Ban Reason: {html.escape(sw_ban.reason)}" ) + try: + update.effective_message.reply_text(text, parse_mode=ParseMode.HTML) + except BadRequest: + update.effective_chat.bot.send_message( + chat_id=update.effective_chat.id, + text=text, + parse_mode=ParseMode.HTML + ) + return if sql.is_user_gbanned(user_id): - update.effective_chat.kick_member(user_id) + update.effective_chat.ban_member(user_id) if should_message: text = ( f"Alert: this user is globally banned.\n" @@ -438,36 +459,38 @@ def check_and_ban(update, user_id, should_message=True): user = sql.get_gbanned_user(user_id) if user.reason: text += f"\nBan Reason: {html.escape(user.reason)}" - update.effective_message.reply_text(text, parse_mode=ParseMode.HTML) + try: + update.effective_message.reply_text(text, parse_mode=ParseMode.HTML) + except BadRequest: + update.effective_chat.bot.send_message( + chat_id=update.effective_chat.id, + text=text, + parse_mode=ParseMode.HTML + ) +def enforce_gban(update: Update, _: CallbackContext): + chat = update.effective_chat + if not get_bot_member(chat.id).can_restrict_members: + return -def enforce_gban(update: Update, context: CallbackContext): - # Not using @restrict handler to avoid spamming - just ignore if cant gban. - bot = context.bot - try: - restrict_permission = update.effective_chat.get_member( - bot.id - ).can_restrict_members - except Unauthorized: + if not sql.does_chat_gban(update.effective_chat.id): return - if sql.does_chat_gban(update.effective_chat.id) and restrict_permission: - user = update.effective_user - chat = update.effective_chat - msg = update.effective_message - if user and not is_user_admin(chat, user.id): - check_and_ban(update, user.id) - return + user = update.effective_user + msg = update.effective_message + + if user and not user_is_admin(chat, user.id): + check_and_ban(update, user.id) - if msg.new_chat_members: - new_members = update.effective_message.new_chat_members - for mem in new_members: - check_and_ban(update, mem.id) + if msg.new_chat_members: + new_members = update.effective_message.new_chat_members + for mem in new_members: + check_and_ban(update, mem.id) - if msg.reply_to_message: - user = msg.reply_to_message.from_user - if user and not is_user_admin(chat, user.id): - check_and_ban(update, user.id, should_message=False) + if msg.reply_to_message: + user = msg.reply_to_message.from_user + if user and not user_is_admin(chat, user.id): + check_and_ban(update, user.id, should_message=False) @user_admin diff --git a/AstrakoBot/modules/gtranslator.py b/AstrakoBot/modules/gtranslator.py index 3808860796..5e7390dca0 100644 --- a/AstrakoBot/modules/gtranslator.py +++ b/AstrakoBot/modules/gtranslator.py @@ -1,88 +1,162 @@ -from emoji import UNICODE_EMOJI -from google_trans_new import LANGUAGES, google_translator from telegram import ParseMode, Update from telegram.ext import CallbackContext, run_async +import requests +import urllib.parse from AstrakoBot import dispatcher from AstrakoBot.modules.disable import DisableAbleCommandHandler from AstrakoBot.modules.helper_funcs.misc import delete from AstrakoBot.modules.sql.clear_cmd_sql import get_clearcmd +languages = { + "af": "Afrikaans", + "sq": "Albanian", + "am": "Amharic", + "ar": "Arabic", + "hy": "Armenian", + "az": "Azerbaijani", + "eu": "Basque", + "be": "Belarusian", + "bn": "Bengali", + "bs": "Bosnian", + "bg": "Bulgarian", + "ca": "Catalan", + "ceb": "Cebuano", + "ny": "Chichewa", + "zh-cn": "Chinese", + "co": "Corsican", + "hr": "Croatian", + "cs": "Czech", + "da": "Danish", + "nl": "Dutch", + "en": "English", + "eo": "Esperanto", + "et": "Estonian", + "tl": "Filipino", + "fi": "Finnish", + "fr": "French", + "fy": "Frisian", + "gl": "Galician", + "ka": "Georgian", + "de": "German", + "el": "Greek", + "gu": "Gujarati", + "ht": "Haitian Creole", + "ha": "Hausa", + "haw": "Hawaiian", + "iw": "Hebrew", + "hi": "Hindi", + "hmn": "Hmong", + "hu": "Hungarian", + "is": "Icelandic", + "ig": "Igbo", + "id": "Indonesian", + "ga": "Irish", + "it": "Italian", + "ja": "Japanese", + "jw": "Javanese", + "kn": "Kannada", + "kk": "Kazakh", + "km": "Khmer", + "rw": "Kinyarwanda", + "ko": "Korean", + "ku": "Kurdish", + "ky": "Kyrgyz", + "lo": "Lao", + "la": "Latin", + "lv": "Latvian", + "lt": "Lithuanian", + "lb": "Luxembourgish", + "mk": "Macedonian", + "mg": "Malagasy", + "ms": "Malay", + "ml": "Malayalam", + "mt": "Maltese", + "mi": "Maori", + "mr": "Marathi", + "mn": "Mongolian", + "my": "Myanmar", + "ne": "Nepali", + "no": "Norwegian", + "or": "Odia", + "ps": "Pashto", + "fa": "Persian", + "pl": "Polish", + "pt": "Portuguese", + "pa": "Punjabi", + "ro": "Romanian", + "ru": "Russian", + "sm": "Samoan", + "gd": "Scots Gaelic", + "sr": "Serbian", + "st": "Sesotho", + "sn": "Shona", + "sd": "Sindhi", + "si": "Sinhala", + "sk": "Slovak", + "sl": "Slovenian", + "so": "Somali", + "es": "Spanish", + "su": "Sundanese", + "sw": "Swahili", + "sv": "Swedish", + "tg": "Tajik", + "ta": "Tamil", + "tt": "Tatar", + "te": "Telugu", + "th": "Thai", + "tr": "Turkish", + "tk": "Turkmen", + "uk": "Ukrainian", + "ur": "Urdu", + "ug": "Uyghur", + "uz": "Uzbek", + "vi": "Vietnamese", + "cy": "Welsh", + "xh": "Xhosa", + "yi": "Yiddish", + "yo": "Yoruba", + "zu": "Zulu" +} + + +def tr(text, srcLang, targetLang): + text = urllib.parse.quote_plus(text) + + # Try to get response at least 3 times + for _ in range(3): + try: + response = requests.get("https://translate.googleapis.com/translate_a/single?client=gtx&sl={}&tl={}&dt=t&q={}".format(srcLang, targetLang, text), timeout=4) + break + except (requests.exceptions.Timeout, requests.exceptions.ConnectionError): + pass + else: + return "" + + if response.status_code != 200: + return "" + data = response.json() + translated_text = "" + for item in data[0]: + translated_text += item[0] + return translated_text def totranslate(update: Update, context: CallbackContext): message = update.effective_message chat = update.effective_chat - problem_lang_code = [] + args = context.args + reply = message.reply_to_message text = "" - for key in LANGUAGES: - if "-" in key: - problem_lang_code.append(key) - - try: - if message.reply_to_message: - args = update.effective_message.text.split(None, 1) - if message.reply_to_message.text: - text = message.reply_to_message.text - elif message.reply_to_message.caption: - text = message.reply_to_message.caption - - try: - source_lang = args[1].split(None, 1)[0] - except (IndexError, AttributeError): - source_lang = "en" - - else: - args = update.effective_message.text.split(None, 2) - text = args[2] - source_lang = args[1] - - if source_lang.count("-") == 2: - for lang in problem_lang_code: - if lang in source_lang: - if source_lang.startswith(lang): - dest_lang = source_lang.rsplit("-", 1)[1] - source_lang = source_lang.rsplit("-", 1)[0] - else: - dest_lang = source_lang.split("-", 1)[1] - source_lang = source_lang.split("-", 1)[0] - elif source_lang.count("-") == 1: - for lang in problem_lang_code: - if lang in source_lang: - dest_lang = source_lang - source_lang = None - break - if dest_lang is None: - dest_lang = source_lang.split("-")[1] - source_lang = source_lang.split("-")[0] - else: - dest_lang = source_lang - source_lang = None - - exclude_list = UNICODE_EMOJI.keys() - for emoji in exclude_list: - if emoji in text: - text = text.replace(emoji, "") - - trl = google_translator() - if source_lang is None: - detection = trl.detect(text) - trans_str = trl.translate(text, lang_tgt=dest_lang) - delmsg = message.reply_text( - f"Translated from `{detection[0]}` to `{dest_lang}`:\n`{trans_str}`", - parse_mode=ParseMode.MARKDOWN, - ) - else: - trans_str = trl.translate(text, lang_tgt=dest_lang, lang_src=source_lang) - delmsg = message.reply_text( - f"Translated from `{source_lang}` to `{dest_lang}`:\n`{trans_str}`", - parse_mode=ParseMode.MARKDOWN, - ) - - deletion(update, context, delmsg) - - except IndexError: - delmsg = update.effective_message.reply_text( - "Reply to messages or write messages from other languages ​​for translating into the intended language\n\n" + if reply: + if reply.document or reply.video or reply.animation or reply.photo: + if reply.caption: + text = reply.caption + elif reply.text: + text = reply.text + + if not text: + delmsg = message.reply_text( + "Reply to messages from other languages for translating into the intended language\n\n" "Example: `/tr en-ml` to translate from English to Malayalam\n" "Or use: `/tr ml` for automatic detection and translating it into Malayalam.\n" "See [List of Language Codes](t.me/OnePunchSupport/12823) for a list of language codes.", @@ -90,11 +164,57 @@ def totranslate(update: Update, context: CallbackContext): disable_web_page_preview=True, ) deletion(update, context, delmsg) - except ValueError: - delmsg = update.effective_message.reply_text("The intended language is not found!") - deletion(update, context, delmsg) + return + if not args: + srcLang = "auto" + targetLang = "en" # Default to english if no arguments were given + elif "-" in args[0]: + try: + srcLang = args[0].split('-')[0] + except: + srcLang = "auto" + try: + targetLang = args[0].split('-')[1] + except: + message.reply_text("Well damn, that does not look like a target language") + return else: + try: + srcLang = args[0] + targetLang = args[1] + except: + targetLang = args[0] + srcLang = "auto" + + # Convert them to lowercase so we don't get issues + srcLang = srcLang.lower() + targetLang = targetLang.lower() + + + # If target language is 'english', set it to 'en' + for key, value in languages.items(): + if value.lower() == targetLang: + targetLang = key + break + + # If source language is 'english', set it to 'en' + for key, value in languages.items(): + if value.lower() == srcLang: + srcLang = key + break + + # Make sure language actually exists + if targetLang not in languages: + message.reply_text("That does not look like a valid language.") + return + + reply_msg = message.reply_text("Translating to {}...".format(languages[targetLang])) + translated = tr(text, srcLang, targetLang) + if not translated: + reply_msg.edit_text("Failed to translate to {}. Try again in a few seconds or try another target language!".format(languages[targetLang])) return + reply_msg.edit_text("Translated to {}:\n\n".format(languages[targetLang]) + translated) + return def deletion(update: Update, context: CallbackContext, delmsg): diff --git a/AstrakoBot/modules/helper_funcs/admin_status.py b/AstrakoBot/modules/helper_funcs/admin_status.py new file mode 100644 index 0000000000..1d9cdf2514 --- /dev/null +++ b/AstrakoBot/modules/helper_funcs/admin_status.py @@ -0,0 +1,102 @@ +from threading import RLock + +from cachetools import TTLCache + +from telegram import Chat, ChatMember, TelegramError, Update +from telegram.ext import CallbackContext, ChatMemberHandler +from telegram.error import Unauthorized + +from AstrakoBot import SUDO_USERS, dispatcher + +BOT_ADMIN_CACHE = TTLCache(maxsize = 512, ttl = 60 * 30) +USER_ADMIN_CACHE = TTLCache(maxsize = 512, ttl = 60 * 30) +RLOCK = RLock() + + +def get_bot_member(chat_id: int) -> ChatMember: + try: + return BOT_ADMIN_CACHE[chat_id] + except KeyError: + try: + mem = dispatcher.bot.getChatMember(chat_id, dispatcher.bot.id) + BOT_ADMIN_CACHE[chat_id] = mem + return mem + except: + bot_user = dispatcher.bot.get_me() + ghost = ChatMember( + user=bot_user, + status='member', + can_be_edited=False, + is_anonymous=False, + can_manage_chat=False, + can_delete_messages=False, + can_manage_video_chats=False, + can_restrict_members=False, + can_promote_members=False, + can_change_info=False, + can_invite_users=False, + can_post_messages=False, + can_edit_messages=False, + can_pin_messages=False, + can_manage_topics=False, + can_send_messages=False, + ) + return ghost + + +def user_is_admin(chat: Chat, user_id: int) -> bool: + if chat.type == "private" or user_id in SUDO_USERS: + return True + + member: ChatMember = get_mem_from_cache(user_id, chat.id) + + if not member: # not in cache so not an admin + return False + + return member.status in ["administrator", "creator"] # check if user is admin + + +def get_mem_from_cache(user_id: int, chat_id: int) -> ChatMember: + with RLOCK: + try: + for i in USER_ADMIN_CACHE[chat_id]: + if i.user.id == user_id: + return i + + except KeyError: + try: + admins = dispatcher.bot.getChatAdministrators(chat_id) + except Unauthorized: + return None + USER_ADMIN_CACHE[chat_id] = admins + for i in admins: + if i.user.id == user_id: + return i + + +def admincacheupdates(update: Update, _: CallbackContext): + try: + oldstat = update.chat_member.old_chat_member.status + newstat = update.chat_member.new_chat_member.status + except AttributeError: + return + if ( + oldstat == "administrator" + and newstat != "administrator" + or oldstat != "administrator" + and newstat == "administrator" + ): + + USER_ADMIN_CACHE[update.effective_chat.id] = update.effective_chat.get_administrators() + + +def botstatchanged(update: Update, _: CallbackContext): + if update.effective_chat.type != "private": + try: + BOT_ADMIN_CACHE[update.effective_chat.id] = update.effective_chat.get_member(dispatcher.bot.id) + except TelegramError: + pass + + +dispatcher.add_handler(ChatMemberHandler(botstatchanged, ChatMemberHandler.MY_CHAT_MEMBER, run_async=True), group=-20) +dispatcher.add_handler(ChatMemberHandler(admincacheupdates, ChatMemberHandler.CHAT_MEMBER, run_async=True), group=-21) diff --git a/AstrakoBot/modules/helper_funcs/chat_status.py b/AstrakoBot/modules/helper_funcs/chat_status.py index 288b4dd56d..1d2021b454 100644 --- a/AstrakoBot/modules/helper_funcs/chat_status.py +++ b/AstrakoBot/modules/helper_funcs/chat_status.py @@ -15,6 +15,8 @@ from telegram import Chat, ChatMember, ParseMode, Update from telegram.ext import CallbackContext +from telegram.error import Unauthorized +from AstrakoBot.modules.helper_funcs.admin_status import get_bot_member, user_is_admin # stores admemes in memory for 10 min. ADMIN_CACHE = TTLCache(maxsize=512, ttl=60 * 10, timer=perf_counter) @@ -33,45 +35,18 @@ def is_sudo_plus(chat: Chat, user_id: int, member: ChatMember = None) -> bool: return user_id in SUDO_USERS or user_id in DEV_USERS -def is_user_admin(chat: Chat, user_id: int, member: ChatMember = None) -> bool: - if ( - chat.type == "private" - or user_id in SUDO_USERS - or user_id in DEV_USERS - or chat.all_members_are_administrators - or user_id in [1087968824] - ): # Count telegram and Group Anonymous as admin - return True - if not member: - with THREAD_LOCK: - # try to fetch from cache first. - try: - return user_id in ADMIN_CACHE[chat.id] - except KeyError: - # keyerror happend means cache is deleted, - # so query bot api again and return user status - # while saving it in cache for future useage... - chat_admins = dispatcher.bot.getChatAdministrators(chat.id) - admin_list = [x.user.id for x in chat_admins] - ADMIN_CACHE[chat.id] = admin_list - - return user_id in admin_list - else: - return member.status in ("administrator", "creator") - - def is_bot_admin(chat: Chat, bot_id: int, bot_member: ChatMember = None) -> bool: if chat.type == "private" or chat.all_members_are_administrators: return True if not bot_member: - bot_member = chat.get_member(bot_id) + bot_member = get_bot_member(chat.id) return bot_member.status in ("administrator", "creator") def can_delete(chat: Chat, bot_id: int) -> bool: - return chat.get_member(bot_id).can_delete_messages + return get_bot_member(chat.id).can_delete_messages def is_user_ban_protected(chat: Chat, user_id: int, member: ChatMember = None) -> bool: @@ -135,15 +110,22 @@ def is_dev_plus_func(update: Update, context: CallbackContext, *args, **kwargs): update.effective_message.delete() except: pass - else: - update.effective_message.reply_text( - "This is a developer restricted command." - " You do not have permissions to run this." - ) return is_dev_plus_func +def sudo_plus_silent(func): + @wraps(func) + def is_sudo_plus_func(update: Update, context: CallbackContext, *args, **kwargs): + bot = context.bot + user = update.effective_user + chat = update.effective_chat + + if user and is_sudo_plus(chat, user.id): + return func(update, context, *args, **kwargs) + + return is_sudo_plus_func + def sudo_plus(func): @wraps(func) def is_sudo_plus_func(update: Update, context: CallbackContext, *args, **kwargs): @@ -160,10 +142,6 @@ def is_sudo_plus_func(update: Update, context: CallbackContext, *args, **kwargs) update.effective_message.delete() except: pass - else: - update.effective_message.reply_text( - "Who dis non-admin telling me what to do? You want a punch?" - ) return is_sudo_plus_func @@ -212,7 +190,7 @@ def is_admin(update: Update, context: CallbackContext, *args, **kwargs): user = update.effective_user chat = update.effective_chat - if user and is_user_admin(chat, user.id): + if user and user_is_admin(chat, user.id): return func(update, context, *args, **kwargs) elif not user: pass @@ -238,7 +216,7 @@ def is_not_admin_no_reply( user = update.effective_user chat = update.effective_chat - if user and is_user_admin(chat, user.id): + if user and user_is_admin(chat, user.id): return func(update, context, *args, **kwargs) elif not user: pass @@ -258,7 +236,7 @@ def is_not_admin(update: Update, context: CallbackContext, *args, **kwargs): user = update.effective_user chat = update.effective_chat - if user and not is_user_admin(chat, user.id): + if user and not user_is_admin(chat, user.id): return func(update, context, *args, **kwargs) elif not user: pass @@ -323,7 +301,7 @@ def pin_rights(update: Update, context: CallbackContext, *args, **kwargs): else: cant_pin = f"I can't pin messages in {update_chat_title}!\nMake sure I'm admin and can pin messages there." - if chat.get_member(bot.id).can_pin_messages: + if get_bot_member(chat.id).can_pin_messages: return func(update, context, *args, **kwargs) else: update.effective_message.reply_text(cant_pin, parse_mode=ParseMode.HTML) @@ -347,7 +325,7 @@ def promote_rights(update: Update, context: CallbackContext, *args, **kwargs): f"Make sure I'm admin there and can appoint new admins." ) - if chat.get_member(bot.id).can_promote_members: + if get_bot_member(chat.id).can_promote_members: return func(update, context, *args, **kwargs) else: update.effective_message.reply_text(cant_promote, parse_mode=ParseMode.HTML) @@ -368,7 +346,7 @@ def restrict_rights(update: Update, context: CallbackContext, *args, **kwargs): else: cant_restrict = f"I can't restrict people in {update_chat_title}!\nMake sure I'm admin there and can restrict users." - if chat.get_member(bot.id).can_restrict_members: + if get_bot_member(chat.id).can_restrict_members: return func(update, context, *args, **kwargs) else: update.effective_message.reply_text( @@ -401,6 +379,9 @@ def user_is_banhammer(update: Update, context: CallbackContext, *args, **kwargs) def connection_status(func): @wraps(func) def connected_status(update: Update, context: CallbackContext, *args, **kwargs): + if not update.effective_user: + return connected_status + conn = connected( context.bot, update, diff --git a/AstrakoBot/modules/helper_funcs/extraction.py b/AstrakoBot/modules/helper_funcs/extraction.py index d8b63fc199..0640b5acdd 100644 --- a/AstrakoBot/modules/helper_funcs/extraction.py +++ b/AstrakoBot/modules/helper_funcs/extraction.py @@ -12,6 +12,8 @@ def id_from_reply(message): return None, None user_id = prev_message.from_user.id res = message.text.split(None, 1) + if prev_message.sender_chat: + user_id = prev_message.sender_chat.id if len(res) < 2: return user_id, "" return user_id, res[1] diff --git a/AstrakoBot/modules/helper_funcs/filters.py b/AstrakoBot/modules/helper_funcs/filters.py index 035a8bec01..c892550ea3 100644 --- a/AstrakoBot/modules/helper_funcs/filters.py +++ b/AstrakoBot/modules/helper_funcs/filters.py @@ -45,3 +45,11 @@ def filter(self, message: Message): ) has_text = _HasText() + + class _IsAnonChannel(MessageFilter): + def filter(self, message: Message): + if (message.from_user and message.from_user.id == 136817688 ): + return True + return False + + is_anon_channel = _IsAnonChannel() diff --git a/AstrakoBot/modules/helper_funcs/msg_types.py b/AstrakoBot/modules/helper_funcs/msg_types.py index 446a53f60e..b8ac4c34fd 100644 --- a/AstrakoBot/modules/helper_funcs/msg_types.py +++ b/AstrakoBot/modules/helper_funcs/msg_types.py @@ -137,20 +137,19 @@ def get_welcome_type(msg: Message): data_type = Types.VIDEO_NOTE buttons = [] + argumen = None # determine what the contents of the filter are - text, image, sticker, etc - if args: - if msg.reply_to_message: - argumen = ( - msg.reply_to_message.caption if msg.reply_to_message.caption else "" - ) - offset = 0 # offset is no need since target was in reply - entities = msg.reply_to_message.parse_entities() - else: - argumen = args[1] - offset = len(argumen) - len( - msg.text - ) # set correct offset relative to command + notename - entities = msg.parse_entities() + if msg.reply_to_message: + argumen = msg.reply_to_message.caption or msg.reply_to_message.text or "" + offset = 0 # offset is no need since target was in reply + entities = msg.reply_to_message.parse_entities() + elif args and len(args) > 1: + argumen = args[1] + offset = len(argumen) - len( + msg.text + ) # set correct offset relative to command + notename + entities = msg.parse_entities() + if argumen: text, buttons = button_markdown_parser( argumen, entities=entities, offset=offset ) diff --git a/AstrakoBot/modules/helper_funcs/string_handling.py b/AstrakoBot/modules/helper_funcs/string_handling.py index 4164c7d241..2c6aa14b10 100644 --- a/AstrakoBot/modules/helper_funcs/string_handling.py +++ b/AstrakoBot/modules/helper_funcs/string_handling.py @@ -261,24 +261,37 @@ def extract_time(message, time_val): return "" if unit == "m": - bantime = int(time.time() + int(time_num) * 60) + bantime = int(time.time()) + int(time_num) * 60 elif unit == "h": - bantime = int(time.time() + int(time_num) * 60 * 60) + bantime = int(time.time()) + int(time_num) * 60 * 60 elif unit == "d": - bantime = int(time.time() + int(time_num) * 24 * 60 * 60) + bantime = int(time.time()) + int(time_num) * 24 * 60 * 60 else: # how even...? return "" return bantime else: message.reply_text( - "Invalid time type specified. Expected m,h, or d, got: {}".format( + "Invalid time type specified. Expected m, h, or d, got: {}".format( time_val[-1] ) ) return "" +def parse_to_seconds(time_str): + if time_str.isdigit(): + return int(time_str) or None + + match = re.match(r"\s*([\d.]+)\s*([a-z]*)\s*", time_str, re.IGNORECASE) + if len(time_str) > 50 or not match: + return None + + value, unit = float(match[1]), match[2].lower() + multipliers = {'d': 86400, 'h': 3600, 'm': 60, 's': 1} + return int(value * multipliers.get(unit[:1], 0)) or None + + def markdown_to_html(text): text = text.replace("*", "**") text = text.replace("`", "```") diff --git a/AstrakoBot/modules/linux.py b/AstrakoBot/modules/linux.py new file mode 100644 index 0000000000..340fea7c16 --- /dev/null +++ b/AstrakoBot/modules/linux.py @@ -0,0 +1,77 @@ +import requests +from AstrakoBot import dispatcher +from telegram import Update, ParseMode +from telegram.ext import CallbackContext, CommandHandler, run_async +from AstrakoBot.modules.sql.clear_cmd_sql import get_clearcmd +from AstrakoBot.modules.helper_funcs.misc import delete +from datetime import datetime +import re + +def linux_kernels(update: Update, context: CallbackContext): + chat = update.effective_chat + headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0"} + + try: + response = requests.get("https://www.kernel.org/releases.json", headers=headers) + response.raise_for_status() + data = response.json() + except Exception as e: + update.effective_message.reply_text(f"Error fetching kernel data: {str(e)}") + return + + releases = data.get("releases", []) + if not releases: + update.effective_message.reply_text("No kernel releases found.") + return + + message = "Linux Kernel Versions\n\n" + for release in releases[:10]: + version = release.get("version", "Unknown") + moniker = release.get("moniker", "").lower() + timestamp = release.get("released", {}).get("timestamp", 0) + source_url = release.get("gitweb", "#") + + category = "MAINLINE" + if release.get("iseol"): + category = "EOL" + elif "longterm" in moniker: + category = "LTS" + elif "stable" in moniker: + category = "STABLE" + elif "linux-next" in moniker: + category = "NEXT" + + message += ( + f"• {version}\n" + f" ā”” {category} " + f"({datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d')})\n\n" + ) + + message += "Source: kernel.org" + + try: + delmsg = update.effective_message.reply_text( + message, + parse_mode=ParseMode.HTML, + disable_web_page_preview=True + ) + except Exception as e: + update.effective_message.reply_text(f"Error formatting message: {str(e)}") + return + + cleartime = get_clearcmd(chat.id, "kernels") + if cleartime: + context.dispatcher.run_async(delete, delmsg, cleartime.time) + +__help__ = """ +*Available commands:*\n +*Linux Kernel:* +• `/kernels`: fetches linux kernels information\n +""" + +KERNELS_HANDLER = CommandHandler("kernels", linux_kernels, run_async=True) +dispatcher.add_handler(KERNELS_HANDLER) + +__mod_name__ = "Linux" +__command_list__ = ["kernels"] +__handlers__ = [KERNELS_HANDLER] diff --git a/AstrakoBot/modules/locks.py b/AstrakoBot/modules/locks.py index 17a9117f38..2d8c6cfebc 100644 --- a/AstrakoBot/modules/locks.py +++ b/AstrakoBot/modules/locks.py @@ -1,5 +1,6 @@ import html +import telegram.ext as tg from telegram import Message, Chat, ParseMode, MessageEntity, Update from telegram import TelegramError, ChatPermissions from telegram.error import BadRequest @@ -10,11 +11,11 @@ from alphabet_detector import AlphabetDetector import AstrakoBot.modules.sql.locks_sql as sql +from AstrakoBot.modules.helper_funcs.admin_status import user_is_admin from AstrakoBot import dispatcher, SUDO_USERS, LOGGER from AstrakoBot.modules.disable import DisableAbleCommandHandler from AstrakoBot.modules.helper_funcs.chat_status import ( can_delete, - is_user_admin, user_not_admin, is_bot_admin, user_admin, @@ -23,6 +24,7 @@ from AstrakoBot.modules.connection import connected from AstrakoBot.modules.sql.approve_sql import is_approved from AstrakoBot.modules.helper_funcs.alternate import send_message, typing_action +from AstrakoBot.modules.helper_funcs.filters import CustomFilters ad = AlphabetDetector() @@ -40,6 +42,7 @@ "game": Filters.game, "location": Filters.location, "egame": Filters.dice, + "anonchannel": CustomFilters.is_anon_channel, "rtl": "rtl", "button": "button", "inline": "inline", @@ -93,6 +96,25 @@ REST_GROUP = 2 +class CustomCommandHandler(tg.CommandHandler): + def __init__(self, command, callback, **kwargs): + super().__init__(command, callback, **kwargs) + + def check_update(self, update): + if super().check_update(update) and not ( + sql.is_restr_locked(update.effective_chat.id, 'messages') and not user_is_admin(update.effective_chat, + update.effective_user.id)): + args = update.effective_message.text.split()[1:] + filter_result = self.filters(update) + if filter_result: + return args, filter_result + else: + return False + + +CommandHandler = CustomCommandHandler + + # NOT ASYNC def restr_members( bot, chat_id, members, messages=False, media=False, other=False, previews=False @@ -100,15 +122,18 @@ def restr_members( for mem in members: if mem.user in SUDO_USERS: pass + elif mem.user == 777000 or mem.user == 1087968824: + pass try: bot.restrict_chat_member( chat_id, mem.user, + permissions=ChatPermissions( can_send_messages=messages, can_send_media_messages=media, can_send_other_messages=other, can_add_web_page_previews=previews, - ) + )) except TelegramError: pass @@ -218,6 +243,18 @@ def lock(update: Update, context: CallbackContext) -> str: ), ) + context.bot.restrict_chat_member(chat.id, int(777000), permissions=ChatPermissions( + can_send_messages=True, + can_send_media_messages=True, + can_send_other_messages=True, + can_add_web_page_previews=True)) + + context.bot.restrict_chat_member(chat.id, int(1087968824), permissions=ChatPermissions( + can_send_messages=True, + can_send_media_messages=True, + can_send_other_messages=True, + can_add_web_page_previews=True)) + send_message(update.effective_message, text, parse_mode="markdown") return ( "{}:" @@ -255,7 +292,7 @@ def unlock(update: Update, context: CallbackContext) -> str: chat = update.effective_chat user = update.effective_user message = update.effective_message - if is_user_admin(chat, message.from_user.id): + if user_is_admin(chat, message.from_user.id): if len(args) >= 1: ltype = args[0].lower() if ltype in LOCK_TYPES: @@ -426,7 +463,7 @@ def del_lockables(update: Update, context: CallbackContext): ) return - chat.kick_member(new_mem.id) + chat.ban_member(new_mem.id) send_message( update.effective_message, "Only admins are allowed to add bots in this chat! Get outta here.", @@ -469,6 +506,7 @@ def build_lock_message(chat_id): locklist.append("button = `{}`".format(locks.button)) locklist.append("egame = `{}`".format(locks.egame)) locklist.append("inline = `{}`".format(locks.inline)) + locklist.append("anonchannel = `{}`".format(locks.anonchannel)) permissions = dispatcher.bot.get_chat(chat_id).permissions permslist.append("messages = `{}`".format(permissions.can_send_messages)) permslist.append("media = `{}`".format(permissions.can_send_media_messages)) diff --git a/AstrakoBot/modules/log_channel.py b/AstrakoBot/modules/log_channel.py index 28a0b28ffb..4ff68871a1 100644 --- a/AstrakoBot/modules/log_channel.py +++ b/AstrakoBot/modules/log_channel.py @@ -39,9 +39,16 @@ def log_action( result += "\nEvent Stamp: {}".format( datetime.utcnow().strftime(datetime_fmt) ) + try: + if message and chat.type == chat.SUPERGROUP: + if chat.username: + result += f'\nLink: click here' + else: + cid = str(chat.id).replace("-100", '') + result += f'\nLink: click here' + except AttributeError: + result += '\nLink: No link for manual actions.' - if message.chat.type == chat.SUPERGROUP and message.chat.username: - result += f'\nLink: click here' log_chat = sql.get_chat_log_channel(chat.id) if log_chat: send_log(context, log_chat, chat.id, result) @@ -109,13 +116,16 @@ def logging(update: Update, context: CallbackContext): log_channel = sql.get_chat_log_channel(chat.id) if log_channel: - log_channel_info = bot.get_chat(log_channel) - message.reply_text( - f"This group has all it's logs sent to:" - f" {escape_markdown(log_channel_info.title)} (`{log_channel}`)", - parse_mode=ParseMode.MARKDOWN, - ) - + try: + log_channel_info = bot.get_chat(log_channel) + message.reply_text( + f"This group has all it's logs sent to:" + f" {escape_markdown(log_channel_info.title)} (`{log_channel}`)", + parse_mode=ParseMode.MARKDOWN, + ) + except Unauthorized: + sql.stop_chat_logging(chat.id) + message.reply_text("No log channel has been set for this group!") else: message.reply_text("No log channel has been set for this group!") diff --git a/AstrakoBot/modules/media.py b/AstrakoBot/modules/media.py index c8be181dc1..32dc955ba9 100644 --- a/AstrakoBot/modules/media.py +++ b/AstrakoBot/modules/media.py @@ -14,7 +14,7 @@ *Text to speech:* • `/tts `: convert text to speech\n *Youtube:* - • `/youtube`, `/yt` ``: download songs or videos from youtube in standar quality + • `/youtube`, `/yt` ` [type] [quality]`: download songs or videos from youtube. Quality limit is 720p; Type can be either video or audio. """ __mod_name__ = "Media" diff --git a/AstrakoBot/modules/misc.py b/AstrakoBot/modules/misc.py index 778d0e5a7d..239f95181f 100644 --- a/AstrakoBot/modules/misc.py +++ b/AstrakoBot/modules/misc.py @@ -90,7 +90,7 @@ def markdown_help(update: Update, context: CallbackContext): *Markdown:* • `/markdownhelp`*:* quick summary of how markdown works in telegram - can only be called in private chats\n *Paste:* - • `/paste`*:* saves replied content to `nekobin.com` and replies with a url\n + • `/paste`*:* saves replied content to `dpaste.com` and replies with a url\n *React:* • `/react`*:* reacts with a random reaction\n *Urban Dictonary:* diff --git a/AstrakoBot/modules/muting.py b/AstrakoBot/modules/muting.py index 09ab544ea4..0b8fae2be8 100644 --- a/AstrakoBot/modules/muting.py +++ b/AstrakoBot/modules/muting.py @@ -6,8 +6,10 @@ bot_admin, can_restrict, connection_status, - is_user_admin, user_admin, + user_can_ban, + can_delete, + is_user_ban_protected, ) from AstrakoBot.modules.helper_funcs.extraction import ( extract_user, @@ -15,6 +17,8 @@ ) from AstrakoBot.modules.helper_funcs.string_handling import extract_time from AstrakoBot.modules.log_channel import loggable +from AstrakoBot.modules.helper_funcs.admin_status import get_bot_member, user_is_admin + from telegram import Bot, Chat, ChatPermissions, ParseMode, Update from telegram.error import BadRequest from telegram.ext import CallbackContext, CommandHandler, run_async @@ -39,7 +43,7 @@ def check_user(user_id: int, bot: Bot, chat: Chat) -> Optional[str]: reply = "I'm not gonna MUTE myself, How high are you?" return reply - if is_user_admin(chat, user_id, member): + if is_user_ban_protected(chat, user_id): reply = "Can't. Find someone else to mute but not this one." return reply @@ -53,6 +57,8 @@ def check_user(user_id: int, bot: Bot, chat: Chat) -> Optional[str]: @connection_status @bot_admin @user_admin +@user_can_ban +@can_restrict @loggable def mute(update: Update, context: CallbackContext) -> str: bot = context.bot @@ -65,6 +71,12 @@ def mute(update: Update, context: CallbackContext) -> str: user_id, reason = extract_user_and_text(message, args) reply = check_user(user_id, bot, chat) + silent = False + if message.text.startswith("/s") or message.text.startswith("!s"): + silent = True + if not can_delete(chat, context.bot.id): + return "" + if reply: message.reply_text(reply) return "" @@ -84,18 +96,24 @@ def mute(update: Update, context: CallbackContext) -> str: if member.can_send_messages is None or member.can_send_messages: chat_permissions = ChatPermissions(can_send_messages=False) bot.restrict_chat_member(chat.id, user_id, chat_permissions) - reply = ( - f"ā•Mute Event\n" - f" • User: {mention_html(member.user.id, html.escape(member.user.first_name))}\n" - f" • Time: no expiration date" - ) - if reason: - reply += f"\n • Reason: {html.escape(reason)}" - bot.sendMessage(chat.id, reply, parse_mode=ParseMode.HTML) + if not silent: + reply = ( + f"ā•Mute Event\n" + f" • User: {mention_html(member.user.id, html.escape(member.user.first_name))}\n" + f" • Time: no expiration date" + ) + if reason: + reply += f"\n • Reason: {html.escape(reason)}" + bot.sendMessage(chat.id, reply, parse_mode=ParseMode.HTML) + else: + message.delete() return log else: - message.reply_text("This user is already muted!") + if not silent: + message.reply_text("This user is already muted!") + else: + message.delete() return "" @@ -103,6 +121,8 @@ def mute(update: Update, context: CallbackContext) -> str: @connection_status @bot_admin @user_admin +@user_can_ban +@can_restrict @loggable def unmute(update: Update, context: CallbackContext) -> str: bot, args = context.bot, context.args @@ -110,23 +130,33 @@ def unmute(update: Update, context: CallbackContext) -> str: user = update.effective_user message = update.effective_message + silent = False + if message.text.startswith("/s") or message.text.startswith("!s"): + silent = True + if not can_delete(chat, context.bot.id): + return "" + user_id = extract_user(message, args) if not user_id: - message.reply_text( - "You'll need to either give me a username to unmute, or reply to someone to be unmuted." - ) + if silent: + message.delete() + else: + message.reply_text( + "You'll need to either give me a username to unmute, or reply to someone to be unmuted." + ) return "" member = chat.get_member(int(user_id)) if member.status != "kicked" and member.status != "left": if ( - member.can_send_messages - and member.can_send_media_messages - and member.can_send_other_messages - and member.can_add_web_page_previews + member.can_send_messages is not False + and member.can_send_media_messages is not False + and member.can_send_other_messages is not False + and member.can_add_web_page_previews is not False ): - message.reply_text("This user already has the right to speak.") + if not silent: + message.reply_text("This user already has the right to speak.") else: chat_permissions = ChatPermissions( can_send_messages=True, @@ -142,11 +172,14 @@ def unmute(update: Update, context: CallbackContext) -> str: bot.restrict_chat_member(chat.id, int(user_id), chat_permissions) except BadRequest: pass - bot.sendMessage( - chat.id, - f"I shall allow {html.escape(member.user.first_name)} to text!", - parse_mode=ParseMode.HTML, - ) + if not silent: + bot.sendMessage( + chat.id, + f"I shall allow {html.escape(member.user.first_name)} to text!", + parse_mode=ParseMode.HTML, + ) + else: + message.delete() return ( f"{html.escape(chat.title)}:\n" f"#UNMUTE\n" @@ -154,11 +187,14 @@ def unmute(update: Update, context: CallbackContext) -> str: f"User: {mention_html(member.user.id, member.user.first_name)}" ) else: - message.reply_text( - "This user isn't even in the chat, unmuting them won't make them talk more than they " - "already do!" - ) + if not silent: + message.reply_text( + "This user isn't even in the chat, unmuting them won't make them talk more than they " + "already do!" + ) + if silent: + message.delete() return "" @@ -166,6 +202,7 @@ def unmute(update: Update, context: CallbackContext) -> str: @bot_admin @can_restrict @user_admin +@user_can_ban @loggable def temp_mute(update: Update, context: CallbackContext) -> str: bot, args = context.bot, context.args @@ -176,6 +213,12 @@ def temp_mute(update: Update, context: CallbackContext) -> str: user_id, reason = extract_user_and_text(message, args) reply = check_user(user_id, bot, chat) + silent = False + if message.text.startswith("/s") or message.text.startswith("!s"): + silent = True + if not can_delete(chat, context.bot.id): + return "" + if reply: message.reply_text(reply) return "" @@ -183,7 +226,10 @@ def temp_mute(update: Update, context: CallbackContext) -> str: member = chat.get_member(user_id) if not reason: - message.reply_text("You haven't specified a time to mute this user for!") + if not silent: + message.reply_text("You haven't specified a time to mute this user for!") + else: + message.delete() return "" split_reason = reason.split(None, 1) @@ -210,11 +256,11 @@ def temp_mute(update: Update, context: CallbackContext) -> str: log += f"\nReason: {reason}" try: - if member.can_send_messages is None or member.can_send_messages: - chat_permissions = ChatPermissions(can_send_messages=False) - bot.restrict_chat_member( - chat.id, user_id, chat_permissions, until_date=mutetime - ) + chat_permissions = ChatPermissions(can_send_messages=False) + bot.restrict_chat_member( + chat.id, user_id, chat_permissions, until_date=mutetime + ) + if not silent: reply = ( f"ā•Mute Event\n" f" • User: {mention_html(member.user.id, html.escape(member.user.first_name))}\n" @@ -223,14 +269,17 @@ def temp_mute(update: Update, context: CallbackContext) -> str: if reason: reply += f"\n • Reason: {html.escape(reason)}" bot.sendMessage(chat.id, reply, parse_mode=ParseMode.HTML) - return log else: - message.reply_text("This user is already muted.") + message.delete() + return log except BadRequest as excp: if excp.message == "Reply message not found": # Do not reply - message.reply_text(f"Muted for {time_val}!", quote=False) + if not silent: + message.reply_text(f"Muted for {time_val}!", quote=False) + else: + message.delete() return log else: LOGGER.warning(update) @@ -241,7 +290,10 @@ def temp_mute(update: Update, context: CallbackContext) -> str: chat.id, excp.message, ) - message.reply_text("Well damn, I can't mute that user.") + if not silent: + message.reply_text("Well damn, I can't mute that user.") + else: + message.delete() return "" @@ -253,9 +305,9 @@ def temp_mute(update: Update, context: CallbackContext) -> str: • `/unmute `*:* unmutes a user. Can also be used as a reply, muting the replied to user. """ -MUTE_HANDLER = CommandHandler("mute", mute, run_async=True) -UNMUTE_HANDLER = CommandHandler("unmute", unmute, run_async=True) -TEMPMUTE_HANDLER = CommandHandler(["tmute", "tempmute"], temp_mute, run_async=True) +MUTE_HANDLER = CommandHandler(["mute", "smute"], mute, run_async=True) +UNMUTE_HANDLER = CommandHandler(["unmute", "sunmute"], unmute, run_async=True) +TEMPMUTE_HANDLER = CommandHandler(["tmute", "tempmute", "stmute", "stempmute"], temp_mute, run_async=True) dispatcher.add_handler(MUTE_HANDLER) dispatcher.add_handler(UNMUTE_HANDLER) diff --git a/AstrakoBot/modules/notes.py b/AstrakoBot/modules/notes.py index cb99136854..7bde6bafbd 100644 --- a/AstrakoBot/modules/notes.py +++ b/AstrakoBot/modules/notes.py @@ -81,13 +81,12 @@ def get(update: Update, context: CallbackContext, notename, show_none=True, no_f ) except BadRequest as excp: if excp.message == "Message to forward not found": - message.reply_text( - "This message seems to have been lost - I'll remove it " - "from your notes list." - ) - sql.rm_note(note_chat_id, notename) - else: raise + message.reply_text( + "This message seems to have been lost - I'll remove it " + "from your notes list." + ) + sql.rm_note(note_chat_id, notename) else: try: bot.forward_message( @@ -95,15 +94,14 @@ def get(update: Update, context: CallbackContext, notename, show_none=True, no_f ) except BadRequest as excp: if excp.message == "Message to forward not found": - message.reply_text( - "Looks like the original sender of this note has deleted " - "their message - sorry! Get your bot admin to start using a " - "message dump to avoid this. I'll remove this note from " - "your saved notes." - ) - sql.rm_note(note_chat_id, notename) - else: raise + message.reply_text( + "Looks like the original sender of this note has deleted " + "their message - sorry! Get your bot admin to start using a " + "message dump to avoid this. I'll remove this note from " + "your saved notes." + ) + sql.rm_note(note_chat_id, notename) else: VALID_NOTE_FORMATTERS = [ "first", @@ -173,6 +171,8 @@ def get(update: Update, context: CallbackContext, notename, show_none=True, no_f try: setting = getprivatenotes(chat_id) if note.msgtype in (sql.Types.BUTTON_TEXT, sql.Types.TEXT): + text = re.sub(r'\n{3,}', '\n\n', text) + if setting: bot.send_message( user.id, @@ -309,7 +309,10 @@ def slash_get(update: Update, context: CallbackContext): def save(update: Update, context: CallbackContext): chat_id = update.effective_chat.id msg = update.effective_message # type: Optional[Message] - + m = msg.text.split(' ', 1) + if len(m) == 1: + msg.reply_text("Provide something to save") + return note_name, text, data_type, content, buttons = get_note_type(msg) note_name = note_name.lower() if data_type is None: diff --git a/AstrakoBot/modules/paste.py b/AstrakoBot/modules/paste.py index a95e263850..3dfd1c3d65 100644 --- a/AstrakoBot/modules/paste.py +++ b/AstrakoBot/modules/paste.py @@ -4,7 +4,6 @@ from telegram import ParseMode, Update from telegram.ext import CallbackContext, run_async - def paste(update: Update, context: CallbackContext): args = context.args message = update.effective_message @@ -19,16 +18,23 @@ def paste(update: Update, context: CallbackContext): message.reply_text("What am I supposed to do with this?") return - key = ( - requests.post("https://nekobin.com/api/documents", json={"content": data}) - .json() - .get("result") - .get("key") - ) - - url = f"https://nekobin.com/{key}" - - reply_text = f"Nekofied to *Nekobin* : {url}" + reply_text = "Dpaste failed!" + try: + response = requests.post( + "https://dpaste.com/api/v2/", + data={ + "content": data, + "syntax": "text", + "expiry_days": 7 + }, + headers={"User-Agent": "Mozilla/5.0"}, + timeout=10 + ) + response.raise_for_status() + url = response.text.strip() + reply_text = f"Dpasted: {url}" + except Exception as e: + reply_text = f"Dpaste failed: {e}" message.reply_text( reply_text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True diff --git a/AstrakoBot/modules/plet.py b/AstrakoBot/modules/plet.py index e8becb3355..2a669a6f28 100644 --- a/AstrakoBot/modules/plet.py +++ b/AstrakoBot/modules/plet.py @@ -24,10 +24,16 @@ def plet(update: Update, context: CallbackContext): chat = update.effective_chat message = update.effective_message - if not message.reply_to_message: + + msg = "" + if context.args: msg = message.text.split(None, 1)[1] - else: - msg = message.reply_to_message.text + elif message.reply_to_message: + msg = message.reply_to_message.caption or message.reply_to_message.text or "" + + if not msg: + message.reply_text("What should i plet?") + return # the processed photo becomes too long and unreadable + the telegram doesn't support any longer dimensions + you have the lulz. if (len(msg)) > 39: @@ -62,12 +68,17 @@ def plet(update: Update, context: CallbackContext): maxsize = 1024, 896 if image.size[0] > maxsize[0]: - image.thumbnail(maxsize, Image.ANTIALIAS) + attr = Image.ANTIALIAS if hasattr(Image, 'ANTIALIAS') else Image.Resampling.LANCZOS + image.thumbnail(maxsize, attr) # put processed image in a buffer and then upload cause async with BytesIO() as buffer: buffer.name = "image.png" - image.save(buffer, "PNG") + try: + image.save(buffer, "PNG") + except SystemError: + message.reply_text("Invalid characters detected. Try again with something readable.") + return buffer.seek(0) delmsg = context.bot.send_sticker(chat_id=message.chat_id, sticker=buffer) diff --git a/AstrakoBot/modules/purge.py b/AstrakoBot/modules/purge.py index d99b6ac489..5a480d9016 100644 --- a/AstrakoBot/modules/purge.py +++ b/AstrakoBot/modules/purge.py @@ -48,9 +48,6 @@ async def purge_messages(event): messages.append(event.reply_to_msg_id) for msg_id in range(message_id, delete_to + 1): messages.append(msg_id) - if len(messages) == 100: - await event.client.delete_messages(event.chat_id, messages) - messages = [] try: await event.client.delete_messages(event.chat_id, messages) diff --git a/AstrakoBot/modules/qr_code.py b/AstrakoBot/modules/qr_code.py new file mode 100644 index 0000000000..bed69f1902 --- /dev/null +++ b/AstrakoBot/modules/qr_code.py @@ -0,0 +1,122 @@ +import requests +from AstrakoBot import dispatcher +from telegram import Update, ParseMode +from telegram.ext import CallbackContext, CommandHandler, run_async +from AstrakoBot.modules.sql.clear_cmd_sql import get_clearcmd +from AstrakoBot.modules.helper_funcs.misc import delete +from datetime import datetime + +from urllib.parse import quote +from telegram import Update +from telegram.ext import CallbackContext +import requests +from io import BytesIO + +def qr_encode(update: Update, context: CallbackContext): + message = update.effective_message + data = None + + if message.reply_to_message: + replied_msg = message.reply_to_message + data = replied_msg.caption or replied_msg.text + if not data: + message.reply_text("Replied message has no text/caption!") + return + else: + if not context.args: + message.reply_text("Please provide text or reply to a message!\nExample: /qr_encode hello world") + return + data = ' '.join(context.args) + + try: + encoded_data = quote(data) + qr_url = f"https://api.qrserver.com/v1/create-qr-code/?data={encoded_data}" + + response = requests.get(qr_url) + response.raise_for_status() + + img_file = BytesIO(response.content) + img_file.name = "qrcode.png" + + message.reply_document( + document=img_file, + filename="qrcode.png" + ) + + except requests.exceptions.RequestException as e: + message.reply_text(f"Failed to generate QR code: {str(e)}") + except Exception as e: + message.reply_text(f"An error occurred: {str(e)}") + +def qr_decode(update: Update, context: CallbackContext): + message = update.effective_message + target = message.reply_to_message or message + + photo = target.photo[-1] if target.photo else None + document = target.document + + file = None + file_size = 0 + if photo: + file_size = photo.file_size + file = photo.get_file() + elif document and document.mime_type.startswith('image/'): + file_size = document.file_size + file = document.get_file() + else: + message.reply_text("Please reply to or send an image containing a QR code") + return + + if file_size > 1048576: + message.reply_text("Image too large! Max 1MB allowed") + return + + try: + img_data = BytesIO() + file.download(out=img_data) + img_data.seek(0) + + response = requests.post( + 'https://api.qrserver.com/v1/read-qr-code/', + files={'file': ('qrcode', img_data, 'application/octet-stream')} + ) + response.raise_for_status() + result = response.json() + + if result and isinstance(result, list): + symbol_list = result[0].get('symbol', [{}]) + if not symbol_list: + return message.reply_text("No QR symbol data found") + text = symbol_list[0].get('data') + if text: + allowed_controls = {'\t', '\n', '\r'} + if all(c.isprintable() or c in allowed_controls for c in text): + return message.reply_text(f"Decoded QR content:\n\n{text}") + else: + return message.reply_text("QR Code contains unprintable characters") + else: + return message.reply_text("QR Code contains no readable data") + else: + return message.reply_text("Invalid or empty QR Code response") + + except requests.exceptions.RequestException as e: + message.reply_text(f"API Error: {str(e)}") + except Exception as e: + message.reply_text(f"Decoding failed: {str(e)}") + +__help__ = """ +*Available commands:*\n +*QR Code:* +• `/qr`, `/qr_encode`: Encode text to QR Code\n +• `/qr_decode`: Decode QR Code to text\n +""" + +ENCODE_HANDLER = CommandHandler(["qr", "qr_encode"], qr_encode, run_async=True) +DECODE_HANDLER = CommandHandler("qr_decode", qr_decode, run_async=True) + +dispatcher.add_handler(ENCODE_HANDLER) +dispatcher.add_handler(DECODE_HANDLER) + +__mod_name__ = "QR Code" +__command_list__ = ["qr", "qr_encode", "qr_decode"] +__handlers__ = [ENCODE_HANDLER, DECODE_HANDLER] diff --git a/AstrakoBot/modules/quotly.py b/AstrakoBot/modules/quotly.py index b21fe1b9eb..28be9c80ed 100644 --- a/AstrakoBot/modules/quotly.py +++ b/AstrakoBot/modules/quotly.py @@ -1,5 +1,6 @@ from PIL import Image, ImageDraw, ImageFont, ImageOps from telethon.tl import types, functions +from telethon.errors.rpcerrorlist import UserNotParticipantError from fontTools.ttLib import TTFont from fontTools.unicode import Unicode import emoji @@ -26,29 +27,6 @@ async def process(msg, user, client, reply, replied=None): - if not os.path.isdir("resources"): - os.mkdir("resources", 0o755) - urllib.request.urlretrieve( - "https://github.com/erenmetesar/modules-repo/raw/master/Roboto-Regular.ttf", - "resources/Roboto-Regular.ttf", - ) - urllib.request.urlretrieve( - "https://github.com/erenmetesar/modules-repo/raw/master/Quivira.otf", - "resources/Quivira.otf", - ) - urllib.request.urlretrieve( - "https://github.com/erenmetesar/modules-repo/raw/master/Roboto-Medium.ttf", - "resources/Roboto-Medium.ttf", - ) - urllib.request.urlretrieve( - "https://github.com/erenmetesar/modules-repo/raw/master/DroidSansMono.ttf", - "resources/DroidSansMono.ttf", - ) - urllib.request.urlretrieve( - "https://github.com/erenmetesar/modules-repo/raw/master/Roboto-Italic.ttf", - "resources/Roboto-Italic.ttf", - ) - # Importing fonts and gettings the size of text font = ImageFont.truetype("resources/Roboto-Medium.ttf", 43, encoding="utf-16") font2 = ImageFont.truetype("resources/Roboto-Regular.ttf", 33, encoding="utf-16") @@ -90,13 +68,20 @@ async def process(msg, user, client, reply, replied=None): title = details.participant.rank if details.participant.rank else "Creator" elif isinstance(details.participant, types.ChannelParticipantAdmin): title = details.participant.rank if details.participant.rank else "Admin" + except UserNotParticipantError: + pass except TypeError: pass titlewidth = font2.getsize(title)[0] # Get user name - lname = "" if not user.last_name else user.last_name - tot = user.first_name + " " + lname + tot = ( + f"{user.first_name} {user.last_name}" if (user.first_name and user.last_name) else + user.first_name or + getattr(user, 'title', '') or + getattr(user, 'username', '') or + "Bot" + ) namewidth = fallback.getsize(tot)[0] + 10 @@ -143,8 +128,14 @@ async def process(msg, user, client, reply, replied=None): y = 80 if replied: # Creating a big canvas to gather all the elements - replname = "" if not replied.sender.last_name else replied.sender.last_name - reptot = replied.sender.first_name + " " + replname + reptot = ( + f"{replied.sender.first_name} {replied.sender.last_name}".strip() + if getattr(replied.sender, 'last_name', None) + else replied.sender.first_name or + getattr(replied.sender, 'title', '') or + getattr(replied.sender, 'username', '') or + "Bot" + ) replywidth = font2.getsize(reptot)[0] if reply.sticker: sticker = await reply.download_media() @@ -430,6 +421,7 @@ async def quotly(event): return reply = await event.get_reply_message() if reply is None: + await event.reply("What to quote?") return msg = reply.message repliedreply = await reply.get_reply_message() diff --git a/AstrakoBot/modules/remote_cmds.py b/AstrakoBot/modules/remote_cmds.py index 50402fbf6e..0c805532b4 100644 --- a/AstrakoBot/modules/remote_cmds.py +++ b/AstrakoBot/modules/remote_cmds.py @@ -7,6 +7,7 @@ ) from AstrakoBot.modules.helper_funcs.extraction import extract_user_and_text from AstrakoBot.modules.helper_funcs.filters import CustomFilters +from AstrakoBot.modules.helper_funcs.admin_status import get_bot_member from telegram import Update, ChatPermissions from telegram.error import BadRequest from telegram.ext import CallbackContext, CommandHandler, run_async @@ -119,7 +120,7 @@ def rban(update: Update, context: CallbackContext): if ( not is_bot_admin(chat, bot.id) - or not chat.get_member(bot.id).can_restrict_members + or not get_bot_member(chat.id).can_restrict_members ): message.reply_text( "I can't restrict people there! Make sure I'm admin and can ban users." @@ -144,7 +145,7 @@ def rban(update: Update, context: CallbackContext): return try: - chat.kick_member(user_id) + chat.ban_member(user_id) message.reply_text("Banned from chat!") except BadRequest as excp: if excp.message == "Reply message not found": @@ -201,7 +202,7 @@ def runban(update: Update, context: CallbackContext): if ( not is_bot_admin(chat, bot.id) - or not chat.get_member(bot.id).can_restrict_members + or not get_bot_member(chat.id).can_restrict_members ): message.reply_text( "I can't unrestrict people there! Make sure I'm admin and can unban users." @@ -285,7 +286,7 @@ def rkick(update: Update, context: CallbackContext): if ( not is_bot_admin(chat, bot.id) - or not chat.get_member(bot.id).can_restrict_members + or not get_bot_member(chat.id).can_restrict_members ): message.reply_text( "I can't restrict people there! Make sure I'm admin and can punch users." @@ -367,7 +368,7 @@ def rmute(update: Update, context: CallbackContext): if ( not is_bot_admin(chat, bot.id) - or not chat.get_member(bot.id).can_restrict_members + or not get_bot_member(chat.id).can_restrict_members ): message.reply_text( "I can't restrict people there! Make sure I'm admin and can mute users." @@ -451,7 +452,7 @@ def runmute(update: Update, context: CallbackContext): if ( not is_bot_admin(chat, bot.id) - or not chat.get_member(bot.id).can_restrict_members + or not get_bot_member(chat.id).can_restrict_members ): message.reply_text( "I can't unrestrict people there! Make sure I'm admin and can unban users." diff --git a/AstrakoBot/modules/reverse.py b/AstrakoBot/modules/reverse.py index 6dcb9cd7ed..db536c2073 100644 --- a/AstrakoBot/modules/reverse.py +++ b/AstrakoBot/modules/reverse.py @@ -114,7 +114,7 @@ def reverse(update: Update, context: CallbackContext): os.remove(imagename) match = ParseSauce(fetchUrl + "&hl=en") guess = match["best_guess"] - if match["override"] and not match["override"] == "": + if match["override"]: imgspage = match["override"] else: imgspage = match["similar_images"] diff --git a/AstrakoBot/modules/sed.py b/AstrakoBot/modules/sed.py index db43e204cb..b1d578100a 100644 --- a/AstrakoBot/modules/sed.py +++ b/AstrakoBot/modules/sed.py @@ -7,6 +7,7 @@ from AstrakoBot.modules.helper_funcs.regex_helper import infinite_loop_check from telegram import Update from telegram.ext import CallbackContext, Filters, run_async +from AstrakoBot.modules.helper_funcs.chat_status import sudo_plus_silent DELIMITERS = ("/", ":", "|", "_") @@ -56,7 +57,7 @@ def separate_sed(sed_string): flags = sed_string[counter:] return replace, replace_with, flags.lower() - +@sudo_plus_silent def sed(update: Update, context: CallbackContext): sed_result = separate_sed(update.effective_message.text) if sed_result and update.effective_message.reply_to_message: @@ -116,7 +117,7 @@ def sed(update: Update, context: CallbackContext): __help__ = """ - • `s//(/)`*:* Reply to a message with this to perform a sed operation on that message, replacing all \ + • `s//(/)` *(sudo users only):* Reply to a message with this to perform a sed operation on that message, replacing all \ occurrences of 'text1' with 'text2'. Flags are optional, and currently include 'i' for ignore case, 'g' for global, \ or nothing. Delimiters include `/`, `_`, `|`, and `:`. Text grouping is supported. The resulting message cannot be \ larger than {}. diff --git a/AstrakoBot/modules/shout.py b/AstrakoBot/modules/shout.py index 7aec7fe8bb..00d41801b8 100644 --- a/AstrakoBot/modules/shout.py +++ b/AstrakoBot/modules/shout.py @@ -6,7 +6,18 @@ def shout(update: Update, context: CallbackContext): args = context.args - text = " ".join(args) + message = update.effective_message + + text = "" + if args: + text = " ".join(args) + elif message.reply_to_message: + text = message.reply_to_message.caption or message.reply_to_message.text or "" + + if not text: + message.reply_text("What should i shout?") + return + result = [] result.append(" ".join([s for s in text])) for pos, symbol in enumerate(text[1:]): @@ -15,7 +26,7 @@ def shout(update: Update, context: CallbackContext): result[0] = text[0] result = "".join(result) msg = "```\n" + result + "```" - return update.effective_message.reply_text(msg, parse_mode="MARKDOWN") + return message.reply_text(msg, parse_mode="MARKDOWN") SHOUT_HANDLER = DisableAbleCommandHandler("shout", shout, run_async=True) diff --git a/AstrakoBot/modules/sql/cust_filters_sql.py b/AstrakoBot/modules/sql/cust_filters_sql.py index e42849ff71..63f346f88c 100644 --- a/AstrakoBot/modules/sql/cust_filters_sql.py +++ b/AstrakoBot/modules/sql/cust_filters_sql.py @@ -357,7 +357,7 @@ def __migrate_filters(): else: file_type = Types.TEXT - print(str(x.chat_id), x.keyword, x.reply, file_type.value) + #print(str(x.chat_id), x.keyword, x.reply, file_type.value) if file_type == Types.TEXT: filt = CustomFilters( str(x.chat_id), x.keyword, x.reply, file_type.value, None diff --git a/AstrakoBot/modules/sql/feds_sql.py b/AstrakoBot/modules/sql/feds_sql.py index b509515592..3b4c8c7bed 100644 --- a/AstrakoBot/modules/sql/feds_sql.py +++ b/AstrakoBot/modules/sql/feds_sql.py @@ -149,7 +149,7 @@ def get_user_fban(fed_id, user_id): def get_user_admin_fed_name(user_id): user_feds = [] for f in FEDERATION_BYFEDID: - if int(user_id) in ast.literal_eval(ast.literal_eval(FEDERATION_BYFEDID[f]["fusers"])["members"]): + if int(user_id) in list(set(ast.literal_eval(ast.literal_eval(FEDERATION_BYFEDID[f]["fusers"])["members"]))): user_feds.append(FEDERATION_BYFEDID[f]["fname"]) return user_feds @@ -165,7 +165,7 @@ def get_user_owner_fed_name(user_id): def get_user_admin_fed_full(user_id): user_feds = [] for f in FEDERATION_BYFEDID: - if int(user_id) in ast.literal_eval(ast.literal_eval(FEDERATION_BYFEDID[f]["fusers"])["members"]): + if int(user_id) in list(set(ast.literal_eval(ast.literal_eval(FEDERATION_BYFEDID[f]["fusers"])["members"]))): user_feds.append({"fed_id": f, "fed": FEDERATION_BYFEDID[f]}) return user_feds @@ -184,7 +184,7 @@ def get_user_fbanlist(user_id): fedname = [] for x in banlist: if banlist[x].get(user_id): - if user_name == "": + if not user_name: user_name = banlist[x][user_id].get("first_name") fedname.append([x, banlist[x][user_id].get("reason")]) return user_name, fedname @@ -327,7 +327,7 @@ def search_user_in_fed(fed_id, user_id): if getfed is None: return False getfed = ast.literal_eval(getfed["fusers"])["members"] - if user_id in ast.literal_eval(getfed): + if user_id in list(set(ast.literal_eval(getfed))): return True else: return False @@ -344,7 +344,7 @@ def user_demote_fed(fed_id, user_id): fed_log = getfed["flog"] # Temp set try: - members = ast.literal_eval(ast.literal_eval(getfed["fusers"])["members"]) + members = list(set(ast.literal_eval(ast.literal_eval(getfed["fusers"])["members"]))) except ValueError: return False members.remove(user_id) @@ -394,7 +394,7 @@ def user_join_fed(fed_id, user_id): fed_rules = getfed["frules"] fed_log = getfed["flog"] # Temp set - members = ast.literal_eval(ast.literal_eval(getfed["fusers"])["members"]) + members = list(set(ast.literal_eval(ast.literal_eval(getfed["fusers"])["members"]))) members.append(user_id) # Set user FEDERATION_BYOWNER[str(owner_id)]["fusers"] = str( @@ -456,7 +456,7 @@ def all_fed_users(fed_id): if getfed is None: return False fed_owner = ast.literal_eval(ast.literal_eval(getfed["fusers"])["owner"]) - fed_admins = ast.literal_eval(ast.literal_eval(getfed["fusers"])["members"]) + fed_admins = list(set(ast.literal_eval(ast.literal_eval(getfed["fusers"])["members"]))) fed_admins.append(fed_owner) return fed_admins @@ -464,7 +464,7 @@ def all_fed_users(fed_id): def all_fed_members(fed_id): with FEDS_LOCK: getfed = FEDERATION_BYFEDID.get(str(fed_id)) - fed_admins = ast.literal_eval(ast.literal_eval(getfed["fusers"])["members"]) + fed_admins = list(set(ast.literal_eval(ast.literal_eval(getfed["fusers"])["members"]))) return fed_admins @@ -557,11 +557,11 @@ def multi_fban_user( SESSION.add(r) counter += 1 - if str(str(counter)[-2:]) == "00": - print(user_id) - print(first_name) - print(reason) - print(counter) + #if str(str(counter)[-2:]) == "00": + #print(user_id) + #print(first_name) + #print(reason) + #print(counter) try: SESSION.commit() except: @@ -570,7 +570,7 @@ def multi_fban_user( finally: SESSION.commit() __load_all_feds_banned() - print("Done") + #print("Done") return counter @@ -717,7 +717,7 @@ def set_fed_log(fed_id, chat_id): ) SESSION.merge(fed) SESSION.commit() - print(fed_log) + #print(fed_log) return True diff --git a/AstrakoBot/modules/sql/locks_sql.py b/AstrakoBot/modules/sql/locks_sql.py index 124fdd6994..7d6c59bcf5 100644 --- a/AstrakoBot/modules/sql/locks_sql.py +++ b/AstrakoBot/modules/sql/locks_sql.py @@ -27,6 +27,7 @@ class Permissions(BASE): button = Column(Boolean, default=False) egame = Column(Boolean, default=False) inline = Column(Boolean, default=False) + anonchannel = Column(Boolean, default=False) def __init__(self, chat_id): self.chat_id = str(chat_id) # ensure string @@ -47,6 +48,7 @@ def __init__(self, chat_id): self.button = False self.egame = False self.inline = False + self.anonchannel = False def __repr__(self): return "" % self.chat_id @@ -145,6 +147,8 @@ def update_lock(chat_id, lock_type, locked): curr_perm.egame = locked elif lock_type == "inline": curr_perm.inline = locked + elif lock_type == 'anonchannel': + curr_perm.anonchannel = locked SESSION.add(curr_perm) SESSION.commit() @@ -214,6 +218,8 @@ def is_locked(chat_id, lock_type): return curr_perm.egame elif lock_type == "inline": return curr_perm.inline + elif lock_type == "anonchannel": + return curr_perm.anonchannel def is_restr_locked(chat_id, lock_type): diff --git a/AstrakoBot/modules/sql/users_sql.py b/AstrakoBot/modules/sql/users_sql.py index 9fb699161b..a4d6e919c0 100644 --- a/AstrakoBot/modules/sql/users_sql.py +++ b/AstrakoBot/modules/sql/users_sql.py @@ -218,7 +218,7 @@ def del_user(user_id): SESSION.commit() return True - ChatMembers.query.filter(ChatMembers.user == user_id).delete() + SESSION.query(ChatMembers).filter(ChatMembers.user == user_id).delete() SESSION.commit() SESSION.close() return False diff --git a/AstrakoBot/modules/stickers.py b/AstrakoBot/modules/stickers.py index 0075deba98..0f85a20e35 100644 --- a/AstrakoBot/modules/stickers.py +++ b/AstrakoBot/modules/stickers.py @@ -1,12 +1,15 @@ import os import math +from io import BytesIO +from urllib.error import HTTPError + import requests import urllib.request as urllib from PIL import Image from html import escape from bs4 import BeautifulSoup as bs -from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton +from telegram import Bot, ParseMode, InlineKeyboardMarkup, InlineKeyboardButton from telegram import TelegramError, Update from telegram.ext import run_async, CallbackContext from telegram.utils.helpers import mention_html @@ -17,6 +20,16 @@ combot_stickers_url = "https://combot.org/telegram/stickers?q=" +def get_sticker_count(bot: Bot, packname: str) -> int: + resp = bot._request.post( + f"{bot.base_url}/getStickerSet", + { + "name": packname, + }, + ) + return len(resp["stickers"]) + + def stickerid(update: Update, context: CallbackContext): msg = update.effective_message if msg.reply_to_message and msg.reply_to_message.sticker: @@ -39,248 +52,237 @@ def stickerid(update: Update, context: CallbackContext): def cb_sticker(update: Update, context: CallbackContext): msg = update.effective_message + stp = "" split = msg.text.split(" ", 1) if len(split) == 1: msg.reply_text("Provide some name to search for pack.") return text = requests.get(combot_stickers_url + split[1]).text soup = bs(text, "lxml") - results = soup.find_all("a", {"class": "sticker-pack__btn"}) - titles = soup.find_all("div", "sticker-pack__title") - if not results: - msg.reply_text("No results found :(.") - return - reply = f"Stickers for *{split[1]}*:" - for result, title in zip(results, titles): - link = result["href"] - reply += f"\n• [{title.get_text()}]({link})" - msg.reply_text(reply, parse_mode=ParseMode.MARKDOWN) - + results = soup.find_all("div", {"class": "sticker-pack__header"}) + for pack in results: + if pack.button: + title_ = (pack.find("div", {"class": "sticker-pack__title"})).text + link_ = (pack.a).get("href") + stp += f"\n• [{title_}]({link_})" + if not stp: + stp = "Stickers not found" + msg.reply_text(stp, parse_mode=ParseMode.MARKDOWN) def getsticker(update: Update, context: CallbackContext): - bot = context.bot msg = update.effective_message - chat_id = update.effective_chat.id if msg.reply_to_message and msg.reply_to_message.sticker: file_id = msg.reply_to_message.sticker.file_id + # Check if it's an animated file + is_animated = msg.reply_to_message.sticker.is_animated + bot = context.bot + # Get the file and put it into a memory buffer new_file = bot.get_file(file_id) - new_file.download("sticker.png") - bot.send_document(chat_id, document=open("sticker.png", "rb")) - os.remove("sticker.png") + sticker_data = new_file.download(out=BytesIO()) + # go back to the start of the buffer + sticker_data.seek(0) + filename = "animated_sticker.tgs.rename_me" if is_animated else "sticker.png" + chat_id = update.effective_chat.id + # Send the document + bot.send_document(chat_id, + document=sticker_data, + filename=filename, + disable_content_type_detection=True + ) else: update.effective_message.reply_text( "Please reply to a sticker for me to upload its PNG." ) - def kang(update: Update, context: CallbackContext): + ppref = "" msg = update.effective_message user = update.effective_user args = context.args - packnum = 0 - packname = "a" + str(user.id) + "_by_" + context.bot.username - packname_found = 0 - max_stickers = 120 - while packname_found == 0: - try: - stickerset = context.bot.get_sticker_set(packname) - if len(stickerset.stickers) >= max_stickers: - packnum += 1 - packname = ( - "a" - + str(packnum) - + "_" - + str(user.id) - + "_by_" - + context.bot.username - ) - else: - packname_found = 1 - except TelegramError as e: - if e.message == "Stickerset_invalid": - packname_found = 1 - kangsticker = "kangsticker.png" is_animated = False - file_id = "" + is_video = False + file_id = None + sticker_emoji = "šŸ¤”" + sticker_data = None + + # The kang syntax is as follows: + # /kang šŸ¤” + # /kang http://whatever.com/sticker.png šŸ¤” + # It can be animated or not. + + # first check if we're syntactically correct. + if not msg.reply_to_message and not args: + # this is quite a bit more difficult, we need to get all their packs managed by us. + packs = "" + # start with finding non-animated packs. + packnum = 0 + # Initial pack name for non-animated + packname = f"a{user.id}_by_{context.bot.username}" + # Max non-animated stickers in a pack + max_stickers = 120 + + # Find the packs + while True: + last_set = False + try: + if get_sticker_count(context.bot, packname) >= max_stickers: + packnum += 1 + if is_animated: + packname = f"animated{packnum}_{user.id}_by_{context.bot.username}" + ppref = "animated" + elif is_video: + packname = f"vid{packnum}_{user.id}_by_{context.bot.username}" + ppref = "vid" + else: + packname = f"a{packnum}_{user.id}_by_{context.bot.username}" + ppref = "" + else: + last_set = True + packs += f"[{ppref}pack{packnum if packnum != 0 else ''}](t.me/addstickers/{packname})\n" + except TelegramError as e: + if e.message == "Stickerset_invalid": + last_set = True + else: + #print(e) + break # something went wrong, leave the loop and send what we have. - if msg.reply_to_message: - if msg.reply_to_message.sticker: - if msg.reply_to_message.sticker.is_animated: + # If we're done checking bot animated and non-animated packs + # exit the loop and send our pack message. + if last_set and is_animated: + break + elif last_set: + # move to checking animated packs. Start with the first pack + packname = f"animated_{user.id}_by_{context.bot.username}" + # reset our counter + packnum = 0 + # Animated packs have a max of 50 stickers + max_stickers = 50 + # tell the loop we're looking at animated stickers now is_animated = True - file_id = msg.reply_to_message.sticker.file_id - elif msg.reply_to_message.photo: - file_id = msg.reply_to_message.photo[-1].file_id - elif msg.reply_to_message.document: - file_id = msg.reply_to_message.document.file_id + # if they have no packs, change our message + if not packs: + packs = "Looks like you don't have any packs! Please reply to a sticker, or image to kang it and create a new pack!" else: - msg.reply_text("Yea, I can't kang that.") + packs = "Please reply to a sticker, or image to kang it!\nOh, by the way, here are your packs:\n" + packs - kang_file = context.bot.get_file(file_id) - if not is_animated: - kang_file.download("kangsticker.png") + # Send our list as a reply + msg.reply_text(packs, parse_mode=ParseMode.MARKDOWN) + # Don't continue processing the command. + return + + # User sent /kang in reply to a message + if rep := msg.reply_to_message: + if rep.sticker: + is_animated = rep.sticker.is_animated + is_video = rep.sticker.is_video + file_id = rep.sticker.file_id + # also grab the emoji if the user wishes + if not args: + sticker_emoji = rep.sticker.emoji + elif rep.photo: + file_id = rep.photo[-1].file_id + elif rep.video: + file_id = rep.video.file_id + is_video = True + elif rep.animation: + file_id = rep.animation.file_id + is_video = True + elif doc := rep.document: + file_id = rep.document.file_id + if doc.mime_type == 'video/webm': + is_video = True else: - kang_file.download("kangsticker.tgs") + msg.reply_text("Yea, I can't steal that.") + return + # Check if they have an emoji specified. if args: - sticker_emoji = str(args[0]) - elif msg.reply_to_message.sticker and msg.reply_to_message.sticker.emoji: - sticker_emoji = msg.reply_to_message.sticker.emoji - else: - sticker_emoji = "šŸ¤”" + sticker_emoji = args[0] - if not is_animated: - try: - im = Image.open(kangsticker) - maxsize = (512, 512) - if (im.width and im.height) < 512: - size1 = im.width - size2 = im.height - if im.width > im.height: - scale = 512 / size1 - size1new = 512 - size2new = size2 * scale - else: - scale = 512 / size2 - size1new = size1 * scale - size2new = 512 - size1new = math.floor(size1new) - size2new = math.floor(size2new) - sizenew = (size1new, size2new) - im = im.resize(sizenew) - else: - im.thumbnail(maxsize) - if not msg.reply_to_message.sticker: - im.save(kangsticker, "PNG") - context.bot.add_sticker_to_set( - user_id=user.id, - name=packname, - png_sticker=open("kangsticker.png", "rb"), - emojis=sticker_emoji, - ) - msg.reply_text( - f"Sticker successfully added to [pack](t.me/addstickers/{packname})" - + f"\nEmoji is: {sticker_emoji}", - parse_mode=ParseMode.MARKDOWN, - ) - - except OSError as e: + # Download the data + kang_file = context.bot.get_file(file_id) + sticker_data = kang_file.download(out=BytesIO()) + # move to the front of the buffer. + sticker_data.seek(0) + else: # user sent /kang with url + url = args[0] + # set the emoji if they specify it. + if len(args) >= 2: + sticker_emoji = args[1] + # open the URL, downlaod the image and write to + # a buffer object we can use elsewhere. + sticker_data = BytesIO() + try: + resp = urllib.urlopen(url) + + # check the mime-type first, you can't kang a .html file. + mime = resp.getheader('Content-Type') + if mime not in ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'application/x-tgsticker']: msg.reply_text("I can only kang images m8.") - print(e) return - except TelegramError as e: - if e.message == "Stickerset_invalid": - makepack_internal( - update, - context, - msg, - user, - sticker_emoji, - packname, - packnum, - png_sticker=open("kangsticker.png", "rb"), - ) - elif e.message == "Sticker_png_dimensions": - im.save(kangsticker, "PNG") - context.bot.add_sticker_to_set( - user_id=user.id, - name=packname, - png_sticker=open("kangsticker.png", "rb"), - emojis=sticker_emoji, - ) - msg.reply_text( - f"Sticker successfully added to [pack](t.me/addstickers/{packname})" - + f"\nEmoji is: {sticker_emoji}", - parse_mode=ParseMode.MARKDOWN, - ) - elif e.message == "Invalid sticker emojis": - msg.reply_text("Invalid emoji(s).") - elif e.message == "Stickers_too_much": - msg.reply_text("Max packsize reached. Press F to pay respecc.") - elif e.message == "Internal Server Error: sticker set not found (500)": - msg.reply_text( - "Sticker successfully added to [pack](t.me/addstickers/%s)" - % packname - + "\n" - "Emoji is:" + " " + sticker_emoji, - parse_mode=ParseMode.MARKDOWN, - ) - print(e) + # check if it's an animated sticker type + if mime == "application/x-tgsticker": + is_animated = True + # write our sticker data to a buffer object + sticker_data.write(resp.read()) + # move to the front of the buffer. + sticker_data.seek(0) + except ValueError: + # If they gave an invalid URL + msg.reply_text("Yea, that's not a URL I can download from.") + return + except HTTPError as e: + # if we're not allowed there for some reason + msg.reply_text(f"Error downloading the file: {e.code} {e.msg}") + return - else: - packname = "animated" + str(user.id) + "_by_" + context.bot.username - packname_found = 0 - max_stickers = 50 - while packname_found == 0: - try: - stickerset = context.bot.get_sticker_set(packname) - if len(stickerset.stickers) >= max_stickers: - packnum += 1 - packname = ( - "animated" - + str(packnum) - + "_" - + str(user.id) - + "_by_" - + context.bot.username - ) - else: - packname_found = 1 - except TelegramError as e: - if e.message == "Stickerset_invalid": - packname_found = 1 - try: - context.bot.add_sticker_to_set( - user_id=user.id, - name=packname, - tgs_sticker=open("kangsticker.tgs", "rb"), - emojis=sticker_emoji, - ) - msg.reply_text( - f"Sticker successfully added to [pack](t.me/addstickers/{packname})" - + f"\nEmoji is: {sticker_emoji}", - parse_mode=ParseMode.MARKDOWN, - ) - except TelegramError as e: - if e.message == "Stickerset_invalid": - makepack_internal( - update, - context, - msg, - user, - sticker_emoji, - packname, - packnum, - tgs_sticker=open("kangsticker.tgs", "rb"), - ) - elif e.message == "Invalid sticker emojis": - msg.reply_text("Invalid emoji(s).") - elif e.message == "Internal Server Error: sticker set not found (500)": - msg.reply_text( - "Sticker successfully added to [pack](t.me/addstickers/%s)" - % packname - + "\n" - "Emoji is:" + " " + sticker_emoji, - parse_mode=ParseMode.MARKDOWN, - ) - print(e) - - elif args: + packnum = 0 + packname_found = False + invalid = False + + # now determine the pack name we should use by default + if is_animated: + packname = f"animated_{user.id}_by_{context.bot.username}" + max_stickers = 50 + elif is_video: + packname = f"vid_{user.id}_by_{context.bot.username}" + max_stickers = 50 + else: + packname = f"a{user.id}_by_{context.bot.username}" + max_stickers = 120 + + # Find if the pack is full already + while not packname_found: try: - try: - urlemoji = msg.text.split(" ") - png_sticker = urlemoji[1] - sticker_emoji = urlemoji[2] - except IndexError: - sticker_emoji = "šŸ¤”" - urllib.urlretrieve(png_sticker, kangsticker) - im = Image.open(kangsticker) - maxsize = (512, 512) + if get_sticker_count(context.bot, packname) >= max_stickers: + packnum += 1 + if is_animated: + packname = f"animated{packnum}_{user.id}_by_{context.bot.username}" + elif is_video: + packname = f"vid{packnum}_{user.id}_by_{context.bot.username}" + else: + packname = f"a{packnum}_{user.id}_by_{context.bot.username}" + else: + packname_found = True + except TelegramError as e: + if e.message == "Stickerset_invalid": + packname_found = True + # we will need to create the sticker pack + invalid = True + else: + raise + + # if the image isn't animated, ensure it's the right size/format with PIL + if not is_animated and not is_video: + # handle non-animated stickers. + try: + im = Image.open(sticker_data) if (im.width and im.height) < 512: size1 = im.width size2 = im.height - if im.width > im.height: + if size1 > size2: scale = 512 / size1 size1new = 512 size2new = size2 * scale @@ -293,85 +295,72 @@ def kang(update: Update, context: CallbackContext): sizenew = (size1new, size2new) im = im.resize(sizenew) else: + maxsize = (512, 512) im.thumbnail(maxsize) - im.save(kangsticker, "PNG") - msg.reply_photo(photo=open("kangsticker.png", "rb")) - context.bot.add_sticker_to_set( - user_id=user.id, - name=packname, - png_sticker=open("kangsticker.png", "rb"), - emojis=sticker_emoji, + # Saved the resized sticker in memory + sticker_data = BytesIO() + im.save(sticker_data, 'PNG') + # seek to start of the image data + sticker_data.seek(0) + except OSError as e: + msg.reply_text("I can only steal images m8.") + return + + # actually add the damn sticker to the pack, animated or not. + try: + # Add the sticker to the pack if it doesn't exist already + if invalid: + # Since Stickerset_invalid will also try to create a pack we might as + # well just reuse that code and avoid typing it all again. + raise TelegramError("Stickerset_invalid") + context.bot.add_sticker_to_set( + user_id=user.id, + name=packname, + tgs_sticker = sticker_data if is_animated else None, + webm_sticker = sticker_data if is_video else None, + png_sticker = sticker_data if not is_animated and not is_video else None, + emojis=sticker_emoji, + ) + msg.reply_text( + f"Sticker successfully added to [pack](t.me/addstickers/{packname})" + + f"\nEmoji is: {sticker_emoji}", + parse_mode=ParseMode.MARKDOWN, + ) + except TelegramError as e: + if e.message == "Stickerset_invalid": + # if we need to make a sticker pack, make one and make this the + # first sticker in the pack. + makepack_internal( + update, + context, + msg, + user, + sticker_emoji, + packname, + packnum, + tgs_sticker=sticker_data if is_animated else None, + webm_sticker=sticker_data if is_video else None, + png_sticker=sticker_data if not is_animated and not is_video else None, ) + elif e.message == "Stickers_too_much": + msg.reply_text("Max packsize reached. Press F to pay respecc.") + elif e.message == "Invalid sticker emojis": + msg.reply_text("I can't kang with that emoji!") + elif e.message == "Sticker_video_nowebm": msg.reply_text( - f"Sticker successfully added to [pack](t.me/addstickers/{packname})" - + f"\nEmoji is: {sticker_emoji}", + "This media format isn't supported, I need it in a webm format, " + "[see this guide](https://core.telegram.org/stickers/webm-vp9-encoding).", parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview = True, + ) + elif e.message == "Internal Server Error: sticker set not found (500)": + msg.reply_text( + f"Sticker successfully added to [pack](t.me/addstickers/{packname})\n" + + f"Emoji is: {sticker_emoji}", parse_mode=ParseMode.MARKDOWN ) - except OSError as e: - msg.reply_text("I can only kang images m8.") - print(e) - return - except TelegramError as e: - if e.message == "Stickerset_invalid": - makepack_internal( - update, - context, - msg, - user, - sticker_emoji, - packname, - packnum, - png_sticker=open("kangsticker.png", "rb"), - ) - elif e.message == "Sticker_png_dimensions": - im.save(kangsticker, "PNG") - context.bot.add_sticker_to_set( - user_id=user.id, - name=packname, - png_sticker=open("kangsticker.png", "rb"), - emojis=sticker_emoji, - ) - msg.reply_text( - "Sticker successfully added to [pack](t.me/addstickers/%s)" - % packname - + "\n" - + "Emoji is:" - + " " - + sticker_emoji, - parse_mode=ParseMode.MARKDOWN, - ) - elif e.message == "Invalid sticker emojis": - msg.reply_text("Invalid emoji(s).") - elif e.message == "Stickers_too_much": - msg.reply_text("Max packsize reached. Press F to pay respecc.") - elif e.message == "Internal Server Error: sticker set not found (500)": - msg.reply_text( - "Sticker successfully added to [pack](t.me/addstickers/%s)" - % packname - + "\n" - "Emoji is:" + " " + sticker_emoji, - parse_mode=ParseMode.MARKDOWN, - ) - print(e) - else: - packs = "Please reply to a sticker, or image to kang it!\nOh, by the way. here are your packs:\n" - if packnum > 0: - firstpackname = "a" + str(user.id) + "_by_" + context.bot.username - for i in range(0, packnum + 1): - if i == 0: - packs += f"[pack](t.me/addstickers/{firstpackname})\n" - else: - packs += f"[pack{i}](t.me/addstickers/{packname})\n" else: - packs += f"[pack](t.me/addstickers/{packname})" - msg.reply_text(packs, parse_mode=ParseMode.MARKDOWN) - try: - if os.path.isfile("kangsticker.png"): - os.remove("kangsticker.png") - elif os.path.isfile("kangsticker.tgs"): - os.remove("kangsticker.tgs") - except: - pass + msg.reply_text(f"Oops! looks like something happened that shouldn't happen! ({e.message})") + raise def makepack_internal( @@ -383,63 +372,68 @@ def makepack_internal( packname, packnum, png_sticker=None, + webm_sticker=None, tgs_sticker=None, ): - name = user.first_name - name = name[:50] + name = user.first_name[:50] try: extra_version = "" if packnum > 0: - extra_version = " " + str(packnum) - if png_sticker: - success = context.bot.create_new_sticker_set( - user.id, - packname, - f"{name}s kang pack" + extra_version, - png_sticker=png_sticker, - emojis=emoji, - ) - if tgs_sticker: - success = context.bot.create_new_sticker_set( - user.id, - packname, - f"{name}s animated kang pack" + extra_version, - tgs_sticker=tgs_sticker, - emojis=emoji, - ) + extra_version = f" {packnum}" + success = context.bot.create_new_sticker_set( + user.id, + packname, + f"{name}s {'animated ' if tgs_sticker else 'video ' if webm_sticker else ''}kang pack{extra_version}", + tgs_sticker=tgs_sticker or None, + webm_sticker=webm_sticker or None, + png_sticker=png_sticker or None, + emojis=emoji, + ) except TelegramError as e: - print(e) - if e.message == "Sticker set name is already occupied": + #print(e) + if e.message == 'Sticker set name is already occupied': msg.reply_text( - "Your pack can be found [here](t.me/addstickers/%s)" % packname, + 'Your pack can be found [here](t.me/addstickers/%s)' + % packname, parse_mode=ParseMode.MARKDOWN, ) - elif e.message in ("Peer_id_invalid", "bot was blocked by the user"): + + return + elif e.message in ('Peer_id_invalid', 'bot was blocked by the user'): msg.reply_text( - "Contact me in PM first.", + 'Contact me in PM first.', reply_markup=InlineKeyboardMarkup( [ [ InlineKeyboardButton( - text="Start", url=f"t.me/{context.bot.username}" + text='Start', + url=f't.me/{context.bot.username}', ) ] ] ), ) - elif e.message == "Internal Server Error: created sticker set not found (500)": + + return + elif ( + e.message + == 'Internal Server Error: created sticker set not found (500)' + ): + success = True + elif e.message == 'Sticker_video_nowebm': msg.reply_text( - "Sticker pack successfully created. Get it [here](t.me/addstickers/%s)" - % packname, + "This media format isn't supported, I need it in a webm format, " + "[see this guide](https://core.telegram.org/stickers/webm-vp9-encoding).", parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview = True, ) - return - + return + else: + success = False if success: msg.reply_text( - "Sticker pack successfully created. Get it [here](t.me/addstickers/%s)" - % packname, + f"Sticker pack successfully created. Get it [here](t.me/addstickers/{packname})", parse_mode=ParseMode.MARKDOWN, ) else: diff --git a/AstrakoBot/modules/super_users.py b/AstrakoBot/modules/super_users.py index 15b2d1b476..25482f2fbf 100644 --- a/AstrakoBot/modules/super_users.py +++ b/AstrakoBot/modules/super_users.py @@ -59,6 +59,11 @@ def addsudo(update: Update, context: CallbackContext) -> str: chat = update.effective_chat bot, args = context.bot, context.args user_id = extract_user(message, args) + + if not user_id: + message.reply_text("Invalid user!") + return "" + user_member = bot.getChat(user_id) rt = "" @@ -121,6 +126,11 @@ def addsupport( chat = update.effective_chat bot, args = context.bot, context.args user_id = extract_user(message, args) + + if not user_id: + message.reply_text("Invalid user!") + return "" + user_member = bot.getChat(user_id) rt = "" @@ -132,13 +142,21 @@ def addsupport( with open(ELEVATED_USERS_FILE, "r") as infile: data = json.load(infile) + if user_id in DEV_USERS: + message.reply_text("Huh? he is more than support!") + return "" + if user_id in SUDO_USERS: - rt += "Demoted from sudo to support user" - data["sudos"].remove(user_id) - SUDO_USERS.remove(user_id) + if user.id in DEV_USERS: + rt += "Demoted from sudo to support user" + data["sudos"].remove(user_id) + SUDO_USERS.remove(user_id) + else: + message.reply_text("This user is already sudo") + return "" if user_id in SUPPORT_USERS: - message.reply_text("This user is already sudo") + message.reply_text("This user is already support") return "" if user_id in WHITELIST_USERS: @@ -176,6 +194,11 @@ def addwhitelist(update: Update, context: CallbackContext) -> str: chat = update.effective_chat bot, args = context.bot, context.args user_id = extract_user(message, args) + + if not user_id: + message.reply_text("Invalid user!") + return "" + user_member = bot.getChat(user_id) rt = "" @@ -187,10 +210,18 @@ def addwhitelist(update: Update, context: CallbackContext) -> str: with open(ELEVATED_USERS_FILE, "r") as infile: data = json.load(infile) + if user_id in DEV_USERS: + message.reply_text("Huh? he is more than whitelist!") + return "" + if user_id in SUDO_USERS: - rt += "Demoted from sudo to whitelist user" - data["sudos"].remove(user_id) - SUDO_USERS.remove(user_id) + if user.id in DEV_USERS: + rt += "Demoted from sudo to whitelist user" + data["sudos"].remove(user_id) + SUDO_USERS.remove(user_id) + else: + message.reply_text("This user is already sudo") + return "" if user_id in SUPPORT_USERS: rt += "Demoted from support user to whitelist user" @@ -231,6 +262,11 @@ def removesudo(update: Update, context: CallbackContext) -> str: chat = update.effective_chat bot, args = context.bot, context.args user_id = extract_user(message, args) + + if not user_id: + message.reply_text("Invalid user!") + return "" + user_member = bot.getChat(user_id) reply = check_user_id(user_id, bot) @@ -277,6 +313,11 @@ def removesupport(update: Update, context: CallbackContext) -> str: chat = update.effective_chat bot, args = context.bot, context.args user_id = extract_user(message, args) + + if not user_id: + message.reply_text("Invalid user!") + return "" + user_member = bot.getChat(user_id) reply = check_user_id(user_id, bot) @@ -319,6 +360,11 @@ def removewhitelist(update: Update, context: CallbackContext) -> str: chat = update.effective_chat bot, args = context.bot, context.args user_id = extract_user(message, args) + + if not user_id: + message.reply_text("Invalid user!") + return "" + user_member = bot.getChat(user_id) reply = check_user_id(user_id, bot) @@ -357,14 +403,21 @@ def whitelistlist(update: Update, context: CallbackContext): bot = context.bot message = update.effective_message msg = "Whitelist users:\n" + zombies = [] for each_user in WHITELIST_USERS: user_id = int(each_user) try: user = bot.get_chat(user_id) - + if not user.first_name: + zombies.append(user_id) + continue msg += f"• {mention_html(user_id, html.escape(user.first_name))}\n" except TelegramError: pass + if zombies: + msg += "\nZombies:\n" + for user_id in zombies: + msg += f"• {mention_html(user_id, html.escape(str(user_id)))}\n" message.reply_text(msg, parse_mode=ParseMode.HTML) @@ -373,13 +426,21 @@ def supportlist(update: Update, context: CallbackContext): bot = context.bot message = update.effective_message msg = "Support users:\n" + zombies = [] for each_user in SUPPORT_USERS: user_id = int(each_user) try: user = bot.get_chat(user_id) + if not user.first_name: + zombies.append(user_id) + continue msg += f"• {mention_html(user_id, html.escape(user.first_name))}\n" except TelegramError: pass + if zombies: + msg += "\nZombies:\n" + for user_id in zombies: + msg += f"• {mention_html(user_id, html.escape(str(user_id)))}\n" message.reply_text(msg, parse_mode=ParseMode.HTML) @@ -389,13 +450,21 @@ def sudolist(update: Update, context: CallbackContext): message = update.effective_message true_sudo = list(set(SUDO_USERS) - set(DEV_USERS)) msg = "Sudo users:\n" + zombies = [] for each_user in true_sudo: user_id = int(each_user) try: user = bot.get_chat(user_id) + if not user.first_name: + zombies.append(user_id) + continue msg += f"• {mention_html(user_id, html.escape(user.first_name))}\n" except TelegramError: pass + if zombies: + msg += "\nZombies:\n" + for user_id in zombies: + msg += f"• {mention_html(user_id, html.escape(str(user_id)))}\n" message.reply_text(msg, parse_mode=ParseMode.HTML) @@ -405,13 +474,21 @@ def devlist(update: Update, context: CallbackContext): message = update.effective_message true_dev = list(set(DEV_USERS) - {OWNER_ID}) msg = "Developer users:\n" + zombies = [] for each_user in true_dev: user_id = int(each_user) try: user = bot.get_chat(user_id) + if not user.first_name: + zombies.append(user_id) + continue msg += f"• {mention_html(user_id, html.escape(user.first_name))}\n" except TelegramError: pass + if zombies: + msg += "\nZombies:\n" + for user_id in zombies: + msg += f"• {mention_html(user_id, html.escape(str(user_id)))}\n" message.reply_text(msg, parse_mode=ParseMode.HTML) diff --git a/AstrakoBot/modules/systools.py b/AstrakoBot/modules/systools.py index f195b89766..cb20897145 100644 --- a/AstrakoBot/modules/systools.py +++ b/AstrakoBot/modules/systools.py @@ -65,6 +65,16 @@ def get_size(bytes, suffix="B"): def convert(speed): return round(int(speed) / 1048576, 2) +def get_network_speed(): + old = psutil.net_io_counters() + + time.sleep(1) + + new = psutil.net_io_counters() + + upload = (new.bytes_sent - old.bytes_sent) + download = (new.bytes_recv - old.bytes_recv) + return upload, download @owner_plus def shell(update: Update, context: CallbackContext): @@ -122,21 +132,23 @@ def status(update: Update, context: CallbackContext): uname = platform.uname() msg += "*System information*\n" msg += f"OS: `{uname.system}`\n" - msg += f"Version: `{uname.version}`\n" + msg += f"Version: `{uname.version.split(' ')[0]}`\n" msg += f"Release: `{uname.release}`\n" msg += f"Processor: `{uname.processor}`\n" boot_time_timestamp = psutil.boot_time() bt = datetime.fromtimestamp(boot_time_timestamp) msg += f"Boot time: `{bt.day}/{bt.month}/{bt.year} - {bt.hour}:{bt.minute}:{bt.second}`\n" msg += f"CPU cores: `{psutil.cpu_count(logical=False)} physical, {psutil.cpu_count()} logical`\n" - msg += f"CPU freq: `{psutil.cpu_freq().current:.2f}Mhz`\n" + msg += f"CPU freq: `{psutil.cpu_freq().current:.2f}Mhz`\n" if psutil.cpu_freq() else "" msg += f"CPU usage: `{psutil.cpu_percent()}%`\n" ram = psutil.virtual_memory() - msg += f"RAM: `{get_size(ram.total)} - {get_size(ram.used)} used ({ram.percent}%)`\n" + msg += f"RAM: `{get_size(ram.total)} - {get_size(ram.used)} used ({round(ram.used / ram.total * 100, 2)}%)`\n" disk = psutil.disk_usage('/') msg += f"Disk usage: `{get_size(disk.total)} total - {get_size(disk.used)} used ({disk.percent}%)`\n" swap = psutil.swap_memory() msg += f"SWAP: `{get_size(swap.total)} - {get_size(swap.used)} used ({swap.percent}%)`\n" + upload, download = get_network_speed() + msg += f"Network: `⬇ {get_size(download)}/s | ⬆ {get_size(upload)}/s`\n" message.reply_text( text = msg, diff --git a/AstrakoBot/modules/tts.py b/AstrakoBot/modules/tts.py index b28cba4bde..e590fcf6d8 100644 --- a/AstrakoBot/modules/tts.py +++ b/AstrakoBot/modules/tts.py @@ -1,7 +1,9 @@ +import uuid import os from datetime import datetime from typing import List from gtts import gTTS +from gtts.tts import gTTSError from telegram import Update, ChatAction, ParseMode from AstrakoBot.modules.sql.clear_cmd_sql import get_clearcmd @@ -12,48 +14,53 @@ def tts(update: Update, context: CallbackContext): - args = context.args message = update.effective_message chat = update.effective_chat - delmsg = "" + args = context.args + delmsg = None + text = "" if message.reply_to_message: - delmsg = message.reply_to_message.text - - if args: - delmsg = " ".join(args).lower() - - current_time = datetime.strftime(datetime.now(), "%d.%m.%Y %H:%M:%S") - filename = datetime.now().strftime("%d%m%y-%H%M%S%f") - update.message.chat.send_action(ChatAction.RECORD_AUDIO) - lang = "ml" - tts = gTTS(delmsg, lang) - tts.save("k.mp3") - with open("k.mp3", "rb") as f: - linelist = list(f) - linecount = len(linelist) - if linecount == 1: - update.message.chat.send_action(ChatAction.RECORD_AUDIO) - lang = "en" - tts = gTTS(delmsg, lang) - tts.save("k.mp3") - with open("k.mp3", "rb") as speech: - delmsg = update.message.reply_voice(speech, quote=False) - - os.remove("k.mp3") - + text = message.reply_to_message.text or message.reply_to_message.caption or "" + elif args: + text = " ".join(args).lower() else: - delmsg = message.reply_text( - "Reply a message or give something like:\n`/tts `", - parse_mode = ParseMode.MARKDOWN + message.reply_text( + "Reply to a message or use: /tts ", + parse_mode=ParseMode.MARKDOWN ) + return + + if not text.strip(): + message.reply_text("Please provide valid text to convert to speech.") + return + + filename = f"tts_{uuid.uuid4()}.mp3" + + try: + message.chat.send_action(ChatAction.RECORD_AUDIO) + + tts = gTTS(text=text, lang='en', tld='com') + tts.save(filename) + + with open(filename, 'rb') as audio_file: + delmsg = message.reply_voice( + voice=audio_file, + quote=False + ) - cleartime = get_clearcmd(chat.id, "tts") + cleartime = get_clearcmd(chat.id, "tts") - if cleartime: - context.dispatcher.run_async(delete, delmsg, cleartime.time) + if cleartime: + context.dispatcher.run_async(delete, delmsg, cleartime.time) + except Exception as e: + message.reply_text(f"Failed to connect to the TTS service: {e}") + try: + os.remove(filename) + except: + pass TTS_HANDLER = DisableAbleCommandHandler("tts", tts, run_async=True) dispatcher.add_handler(TTS_HANDLER) diff --git a/AstrakoBot/modules/ud.py b/AstrakoBot/modules/ud.py index bbed60b121..d723a839f7 100644 --- a/AstrakoBot/modules/ud.py +++ b/AstrakoBot/modules/ud.py @@ -5,7 +5,7 @@ from AstrakoBot.modules.sql.clear_cmd_sql import get_clearcmd from telegram import ParseMode, Update from telegram.ext import CallbackContext, run_async - +import re def ud(update: Update, context: CallbackContext): message = update.effective_message @@ -18,7 +18,15 @@ def ud(update: Update, context: CallbackContext): reply_text = f'*{text}*\n\n{results["list"][0]["definition"]}\n\n_{results["list"][0]["example"]}_' except: reply_text = "No results found." - delmsg = message.reply_text(reply_text, parse_mode=ParseMode.MARKDOWN) + try: + delmsg = message.reply_text(reply_text, parse_mode=ParseMode.MARKDOWN) + except: + reply_text = re.sub(r'\*(.*?)\*', r'\1', reply_text) + plain_text = reply_text.split('\n') + for i in range(1, len(plain_text)): + plain_text[i] = plain_text[i].replace('_', '') + plain_text = '\n'.join(plain_text) + delmsg = message.reply_text(plain_text) cleartime = get_clearcmd(chat.id, "ud") diff --git a/AstrakoBot/modules/userinfo.py b/AstrakoBot/modules/userinfo.py index 147009b44b..e89e572555 100644 --- a/AstrakoBot/modules/userinfo.py +++ b/AstrakoBot/modules/userinfo.py @@ -3,15 +3,23 @@ import os import requests +from telegram.user import User from telethon.tl.functions.channels import GetFullChannelRequest from telethon.tl.types import ChannelParticipantsAdmins -from telethon import events +from telethon import events, types from telegram import MAX_MESSAGE_LENGTH, ParseMode, Update, MessageEntity from telegram.ext import CallbackContext, CommandHandler, Filters from telegram.ext.dispatcher import run_async from telegram.error import BadRequest from telegram.utils.helpers import escape_markdown, mention_html +from telethon.errors import ( + ChannelInvalidError, + ChannelPrivateError, + ChatAdminRequiredError, + PeerIdInvalidError, + UserNotParticipantError, +) from AstrakoBot import ( DEV_USERS, @@ -35,7 +43,6 @@ from AstrakoBot.modules.helper_funcs.misc import delete from AstrakoBot import telethn as AstrakoBotTelethonClient, SUDO_USERS, SUPPORT_USERS - def get_id(update: Update, context: CallbackContext): bot, args = context.bot, context.args message = update.effective_message @@ -58,12 +65,17 @@ def get_id(update: Update, context: CallbackContext): ) else: - - user = bot.get_chat(user_id) - msg.reply_text( - f"{html.escape(user.first_name)}'s id is {user.id}.", - parse_mode=ParseMode.HTML, - ) + try: + user = bot.get_chat(user_id) + msg.reply_text( + f"{html.escape(user.first_name)}'s id is {user.id}.", + parse_mode=ParseMode.HTML, + ) + except: + msg.reply_text( + f"Their id is {user_id}.", + parse_mode=ParseMode.HTML, + ) else: @@ -82,37 +94,130 @@ def get_id(update: Update, context: CallbackContext): events.NewMessage(pattern="/ginfo ", from_users=(SUDO_USERS or []) + (SUPPORT_USERS or [])) ) async def group_info(event) -> None: - chat = event.text.split(" ", 1)[1] + target_entity = None + entity_id_str = None + + if event.is_reply: + replied_msg = await event.get_reply_message() + if replied_msg and replied_msg.sender_id: + target_entity = replied_msg.sender_id + entity_id_str = str(target_entity) + else: + await event.reply("Could not identify the user from the replied message.") + return + else: + parts = event.text.split(" ", 1) + if len(parts) > 1: + entity_id_str = parts[1].strip() + if entity_id_str.startswith('@'): + target_entity = entity_id_str + else: + try: + target_entity = int(entity_id_str) + except ValueError: + await event.reply("Invalid ID or username format.") + return + else: + target_entity = event.chat_id + entity_id_str = str(target_entity) + + if target_entity is None: + await event.reply("Could not determine the target entity.") + return + try: - entity = await event.client.get_entity(chat) - totallist = await event.client.get_participants( - entity, filter=ChannelParticipantsAdmins - ) - ch_full = await event.client(GetFullChannelRequest(channel=entity)) - except: + entity = await event.client.get_entity(target_entity) + except (ValueError, PeerIdInvalidError, ChannelInvalidError, ChannelPrivateError) as e: await event.reply( - "Can't for some reason, maybe it is a private one or that I am banned there." + f"Could not find or access the specified chat/channel (`{entity_id_str}`). " + "It might be invalid, private, or I might lack permissions." ) return - msg = f"**ID**: `{entity.id}`" - msg += f"\n**Title**: `{entity.title}`" - msg += f"\n**Datacenter**: `{entity.photo.dc_id}`" - msg += f"\n**Video PFP**: `{entity.photo.has_video}`" - msg += f"\n**Supergroup**: `{entity.megagroup}`" - msg += f"\n**Restricted**: `{entity.restricted}`" - msg += f"\n**Scam**: `{entity.scam}`" - msg += f"\n**Slowmode**: `{entity.slowmode_enabled}`" - if entity.username: - msg += f"\n**Username**: {entity.username}" - msg += "\n\n**Member Stats:**" - msg += f"\n`Admins:` `{len(totallist)}`" - msg += f"\n`Users`: `{totallist.total}`" - msg += "\n\n**Admins List:**" - for x in totallist: - msg += f"\n• [{x.id}](tg://user?id={x.id})" - msg += f"\n\n**Description**:\n`{ch_full.full_chat.about}`" - await event.reply(msg) + except Exception as e: + await event.reply(f"An unexpected error occurred while fetching info for `{entity_id_str}`.") + return + msg = f"**Info for:** `{entity_id_str}`\n" + msg += f"**Type:** `{type(entity).__name__}`\n" + msg += f"**ID:** `{entity.id}`\n" + + if isinstance(entity, (types.Chat, types.Channel)): + msg += f"**Title:** `{entity.title}`\n" + if hasattr(entity, 'username') and entity.username: + msg += f"**Username:** @{entity.username}\n" + else: + msg += f"**Username:** `None`\n" + + if hasattr(entity.photo, 'dc_id'): + msg += f"**Photo DC:** `{entity.photo.dc_id}`\n" + if hasattr(entity.photo, 'has_video'): + msg += f"**Video PFP:** `{entity.photo.has_video}`\n" + + msg += f"**Scam:** `{getattr(entity, 'scam', 'N/A')}`\n" + msg += f"**Restricted:** `{getattr(entity, 'restricted', 'N/A')}`\n" + if getattr(entity, 'restriction_reason', None): + msg += f"**Restriction Reason:** `{entity.restriction_reason}`\n" + + if isinstance(entity, types.Channel): + msg += f"**Supergroup:** `{entity.megagroup}`\n" + msg += f"**Broadcast Channel:** `{entity.broadcast}`\n" + msg += f"**Verified:** `{entity.verified}`\n" + msg += f"**Gigagroup:** `{getattr(entity, 'gigagroup', 'N/A')}`\n" + msg += f"**Slowmode Enabled:** `{entity.slowmode_enabled}`\n" + + full_chat_info = None + admin_list = [] + participant_count = "N/A" + admin_count = "N/A" + about = "N/A" + + try: + if isinstance(entity, types.Channel): + full_chat_info = await event.client(GetFullChannelRequest(channel=entity)) + about = full_chat_info.full_chat.about + participant_count = getattr(full_chat_info.full_chat, 'participants_count', 'N/A') + elif isinstance(entity, types.Chat): + full_chat_info = await event.client(GetFullChatRequest(chat_id=entity.id)) + about = getattr(full_chat_info.full_chat, 'about', 'N/A') + if hasattr(full_chat_info, 'users'): + participant_count = len(full_chat_info.users) + + try: + admins = await event.client.get_participants( + entity, filter=ChannelParticipantsAdmins + ) + admin_count = len(admins) if admins else 0 # Handle None case + admin_list = [f"• [{admin.id}](tg://user?id={admin.id})" for admin in admins] + except ChatAdminRequiredError: + admin_list.append("_(Admin permissions required to list)_") + admin_count = "N/A (No Perms)" + except UserNotParticipantError: + admin_list.append("_(Bot is not in this chat/channel)_") + admin_count = "N/A (Not Participant)" + except Exception as e: + admin_list.append("_(Could not retrieve admin list)_") + admin_count = "N/A (Error)" + + + msg += "\n**Stats:**\n" + msg += f"`Participants:` `{participant_count}`\n" + msg += f"`Admins:` `{admin_count}`\n" + + if admin_list: + msg += "\n**Admins List:**" + msg += "\n" + "\n".join(admin_list) # Join list items with newlines + + msg += f"\n\n**Description:**\n`{about}`" + + except (ChatAdminRequiredError, UserNotParticipantError) as e: + msg += "\n\n**(Could not retrieve full details like description or participant counts due to permissions or bot not being a participant)**" + except Exception as e: + msg += "\n\n**(An error occurred while retrieving full details)**" + + else: + msg += "\n**(Unsupported entity type)**" + + await event.reply(msg, link_preview=False) def gifid(update: Update, context: CallbackContext): msg = update.effective_message @@ -131,11 +236,27 @@ def info(update: Update, context: CallbackContext): chat = update.effective_chat user_id = extract_user(update.effective_message, args) - if user_id: + if user_id and int(user_id) != 777000 and int(user_id) != 1087968824: user = bot.get_chat(user_id) + elif user_id and int(user_id) == 777000: + message.reply_text( + "This is Telegram. Unless you manually entered this reserved account's ID, it is likely a old broadcast from a linked channel." + ) + return + + elif user_id and int(user_id) == 1087968824: + message.reply_text( + "This is Group Anonymous Bot. Unless you manually entered this reserved account's ID, it is likely a broadcast from a linked channel or anonymously sent message." + ) + return + elif not message.reply_to_message and not args: - user = message.from_user + user = ( + message.sender_chat + if message.sender_chat is not None + else message.from_user + ) elif not message.reply_to_message and ( not args @@ -157,128 +278,163 @@ def info(update: Update, context: CallbackContext): else: return + + rep = message.reply_text("Appraising...", parse_mode=ParseMode.HTML) + + if hasattr(user, 'type') and user.type != "private": + text = ( + f"Chat Info: " + f"\nID: {user.id}" + f"\nTitle: {user.title}" + ) + if user.username: + text += f"\nUsername: @{html.escape(user.username)}" + text += f"\nChat Type: {user.type.capitalize()}" + + if INFOPIC: + try: + profile = bot.getChat(user.id).photo + _file = bot.get_file(profile["big_file_id"]) + _file.download(f"{user.id}.png") + + delmsg = message.reply_document( + document=open(f"{user.id}.png", "rb"), + caption=(text), + parse_mode=ParseMode.HTML, + ) - rep = message.reply_text("Appraising...", parse_mode=ParseMode.HTML) + os.remove(f"{user.id}.png") + # Incase chat don't have profile pic, send normal text + except: + delmsg = message.reply_text( + text, parse_mode=ParseMode.HTML, disable_web_page_preview=True + ) - text = ( - f"User info:\n" - f"ID: {user.id}\n" - f"First Name: {html.escape(user.first_name)}" - ) + else: + delmsg = message.reply_text( + text, parse_mode=ParseMode.HTML, disable_web_page_preview=True + ) - if user.last_name: - text += f"\nLast Name: {html.escape(user.last_name)}" + else: + text = ( + f"User info:\n" + f"ID: {user.id}\n" + f"First Name: {mention_html(user.id, user.first_name or 'None')}" + ) - if user.username: - text += f"\nUsername: @{html.escape(user.username)}" + if user.last_name: + text += f"\nLast Name: {html.escape(user.last_name)}" - text += f"\nPermalink: {mention_html(user.id, 'link')}" + if user.username: + text += f"\nUsername: @{html.escape(user.username)}" - if chat.type != "private" and user_id != bot.id: - _stext = "\nPresence: {}" + text += f"\nPermalink: {mention_html(user.id, 'link')}" - afk_st = is_afk(user.id) - if afk_st: - text += _stext.format("AFK") - else: - status = status = bot.get_chat_member(chat.id, user.id).status - if status: - if status == "left": - text += _stext.format("Not here") - if status == "kicked": - text += _stext.format("Banned") - elif status == "member": - text += _stext.format("Detected") - elif status in {"administrator", "creator"}: - text += _stext.format("Admin") + if chat.type != "private" and user_id != bot.id: + _stext = "\nPresence: {}" - try: - spamwtc = sw.get_ban(int(user.id)) - if spamwtc: - text += "\n\nThis person is Spamwatched!" - text += f"\nReason:
{spamwtc.reason}
" - text += "\nAppeal at @SpamWatchSupport" - else: - pass - except: - pass # don't crash if api is down somehow... - - disaster_level_present = False - - if user.id == OWNER_ID: - text += "\n\nUser level: god" - disaster_level_present = True - elif user.id in DEV_USERS: - text += "\n\nUser level: developer" - disaster_level_present = True - elif user.id in SUDO_USERS: - text += "\n\nUser level: sudo" - disaster_level_present = True - elif user.id in SUPPORT_USERS: - text += "\n\nUser level: support" - disaster_level_present = True - elif user.id in WHITELIST_USERS: - text += "\n\nUser level: whitelist" - disaster_level_present = True - - # if disaster_level_present: - # text += ' [?]'.format( - # bot.username) + afk_st = is_afk(user.id) + if afk_st: + text += _stext.format("AFK") + else: + status = bot.get_chat_member(chat.id, user.id).status + if status: + if status == "left": + text += _stext.format("Not here") + if status == "kicked": + text += _stext.format("Banned") + elif status == "member": + text += _stext.format("Detected") + elif status in {"administrator", "creator"}: + text += _stext.format("Admin") - try: - user_member = chat.get_member(user.id) - if user_member.status == "administrator": - result = requests.post( - f"https://api.telegram.org/bot{TOKEN}/getChatMember?chat_id={chat.id}&user_id={user.id}" - ) - result = result.json()["result"] - if "custom_title" in result.keys(): - custom_title = result["custom_title"] - text += f"\n\nTitle:\n{custom_title}" - except BadRequest: - pass - - for mod in USER_INFO: try: - mod_info = mod.__user_info__(user.id).strip() - except TypeError: - mod_info = mod.__user_info__(user.id, chat.id).strip() - if mod_info: - text += "\n\n" + mod_info + spamwtc = sw.get_ban(int(user.id)) + if spamwtc: + text += "\n\nThis person is Spamwatched!" + text += f"\nReason:
{spamwtc.reason}
" + text += "\nAppeal at @SpamWatchSupport" + else: + pass + except: + pass # don't crash if api is down somehow... + + disaster_level_present = False + + if user.id == OWNER_ID: + text += "\n\nUser level: god" + disaster_level_present = True + elif user.id in DEV_USERS: + text += "\n\nUser level: developer" + disaster_level_present = True + elif user.id in SUDO_USERS: + text += "\n\nUser level: sudo" + disaster_level_present = True + elif user.id in SUPPORT_USERS: + text += "\n\nUser level: support" + disaster_level_present = True + elif user.id in WHITELIST_USERS: + text += "\n\nUser level: whitelist" + disaster_level_present = True + + # if disaster_level_present: + # text += ' [?]'.format( + # bot.username) - if INFOPIC: try: - profile = context.bot.get_user_profile_photos(user.id).photos[0][-1] - _file = bot.get_file(profile["file_id"]) - _file.download(f"{user.id}.png") + user_member = chat.get_member(user.id) + if user_member.status == "administrator": + result = requests.post( + f"https://api.telegram.org/bot{TOKEN}/getChatMember?chat_id={chat.id}&user_id={user.id}" + ) + result = result.json()["result"] + if "custom_title" in result.keys(): + custom_title = result["custom_title"] + text += f"\n\nTitle:\n{custom_title}" + except BadRequest: + pass - delmsg = message.reply_document( - document=open(f"{user.id}.png", "rb"), - caption=(text), - parse_mode=ParseMode.HTML, - ) + for mod in USER_INFO: + try: + mod_info = mod.__user_info__(user.id).strip() + except TypeError: + mod_info = mod.__user_info__(user.id, chat.id).strip() + if mod_info: + text += "\n\n" + mod_info + + text += "\n\n" + biome(user.id) + + if INFOPIC: + try: + profile = context.bot.get_user_profile_photos(user.id).photos[0][-1] + _file = bot.get_file(profile["file_id"]) + _file.download(f"{user.id}.png") + + delmsg = message.reply_document( + document=open(f"{user.id}.png", "rb"), + caption=(text), + parse_mode=ParseMode.HTML, + ) - os.remove(f"{user.id}.png") - # Incase user don't have profile pic, send normal text - except IndexError: + os.remove(f"{user.id}.png") + # Incase user don't have profile pic, send normal text + except IndexError: + delmsg = message.reply_text( + text, parse_mode=ParseMode.HTML, disable_web_page_preview=True + ) + + else: delmsg = message.reply_text( text, parse_mode=ParseMode.HTML, disable_web_page_preview=True - ) - - else: - delmsg = message.reply_text( - text, parse_mode=ParseMode.HTML, disable_web_page_preview=True - ) - + ) + rep.delete() - - + cleartime = get_clearcmd(chat.id, "info") - + if cleartime: context.dispatcher.run_async(delete, delmsg, cleartime.time) - def about_me(update: Update, context: CallbackContext): bot, args = context.bot, context.args message = update.effective_message @@ -434,7 +590,7 @@ def gdpr(update: Update, context: CallbackContext): parse_mode=ParseMode.MARKDOWN) -def __user_info__(user_id): +def biome(user_id): bio = html.escape(sql.get_user_bio(user_id) or "") me = html.escape(sql.get_user_me_info(user_id) or "") result = "" @@ -478,7 +634,7 @@ def __gdpr__(user_id): """ SET_BIO_HANDLER = DisableAbleCommandHandler("setbio", set_about_bio, run_async=True) -GET_BIO_HANDLER = DisableAbleCommandHandler("bio", about_bio) +GET_BIO_HANDLER = DisableAbleCommandHandler("bio", about_bio, run_async=True) STATS_HANDLER = CommandHandler("stats", stats, run_async=True) ID_HANDLER = DisableAbleCommandHandler("id", get_id, run_async=True) diff --git a/AstrakoBot/modules/users.py b/AstrakoBot/modules/users.py index bdad0aff9a..f64c24e2b7 100644 --- a/AstrakoBot/modules/users.py +++ b/AstrakoBot/modules/users.py @@ -1,3 +1,4 @@ +import contextlib from io import BytesIO from time import sleep @@ -15,6 +16,7 @@ from AstrakoBot import DEV_USERS, LOGGER, OWNER_ID, dispatcher from AstrakoBot.modules.helper_funcs.chat_status import dev_plus, sudo_plus from AstrakoBot.modules.sql.users_sql import get_all_users +from AstrakoBot.modules.helper_funcs.admin_status import get_bot_member USERS_GROUP = 4 CHAT_GROUP = 5 @@ -99,23 +101,57 @@ def broadcast(update: Update, context: CallbackContext): ) -def log_user(update: Update, context: CallbackContext): +def log_user(update: Update, _: CallbackContext): chat = update.effective_chat msg = update.effective_message sql.update_user(msg.from_user.id, msg.from_user.username, chat.id, chat.title) - if msg.reply_to_message: + if rep := msg.reply_to_message: sql.update_user( - msg.reply_to_message.from_user.id, - msg.reply_to_message.from_user.username, + rep.from_user.id, + rep.from_user.username, chat.id, chat.title, ) + if rep.forward_from: + sql.update_user( + rep.forward_from.id, + rep.forward_from.username, + ) + + if rep.entities: + for entity in rep.entities: + if entity.type in ["text_mention", "mention"]: + with contextlib.suppress(AttributeError): + sql.update_user(entity.user.id, entity.user.username) + if rep.sender_chat and not rep.is_automatic_forward: + sql.update_user( + rep.sender_chat.id, + rep.sender_chat.username, + chat.id, + chat.title, + ) + if msg.forward_from: sql.update_user(msg.forward_from.id, msg.forward_from.username) + if msg.entities: + for entity in msg.entities: + if entity.type in ["text_mention", "mention"]: + with contextlib.suppress(AttributeError): + sql.update_user(entity.user.id, entity.user.username) + + if msg.new_chat_members: + for user in msg.new_chat_members: + if user.id == msg.from_user.id: # we already added that in the first place + continue + sql.update_user(user.id, user.username, chat.id, chat.title) + + if req := update.chat_join_request: + sql.update_user(req.from_user.id, req.from_user.username, chat.id, chat.title) + @sudo_plus def chats(update: Update, context: CallbackContext): @@ -146,9 +182,9 @@ def chats(update: Update, context: CallbackContext): def chat_checker(update: Update, context: CallbackContext): bot = context.bot try: - if update.effective_message.chat.get_member(bot.id).can_send_messages is False: + if get_bot_member(update.effective_chat.id).can_send_messages is False: bot.leaveChat(update.effective_message.chat.id) - except Unauthorized: + except: pass diff --git a/AstrakoBot/modules/wallpaper.py b/AstrakoBot/modules/wallpaper.py index 3b42e8e2b9..3004eaa4db 100644 --- a/AstrakoBot/modules/wallpaper.py +++ b/AstrakoBot/modules/wallpaper.py @@ -1,6 +1,6 @@ from random import randint -import requests as r +import requests from AstrakoBot import SUPPORT_CHAT, WALL_API, dispatcher from AstrakoBot.modules.disable import DisableAbleCommandHandler from AstrakoBot.modules.sql.clear_cmd_sql import get_clearcmd @@ -8,8 +8,7 @@ from telegram import Update from telegram.ext import CallbackContext, run_async -# Wallpapers module by @TheRealPhoenix using wall.alphacoders.com - +PIXABAY_API = WALL_API def wall(update: Update, context: CallbackContext): chat_id = update.effective_chat.id @@ -23,21 +22,20 @@ def wall(update: Update, context: CallbackContext): return else: caption = query - term = query.replace(" ", "%20") - json_rep = r.get( - f"https://wall.alphacoders.com/api2.0/get.php?auth={WALL_API}&method=search&term={term}" - ).json() - if not json_rep.get("success"): + term = query.replace(" ", "+") + response = requests.get(f"https://pixabay.com/api/?key={PIXABAY_API}&q={term}&image_type=photo&per_page=200") + + if response.status_code != 200: msg.reply_text(f"An error occurred! Report this @{SUPPORT_CHAT}") else: - wallpapers = json_rep.get("wallpapers") + data = response.json() + wallpapers = data.get("hits") if not wallpapers: msg.reply_text("No results found! Refine your search.") return else: - index = randint(0, len(wallpapers) - 1) # Choose random index - wallpaper = wallpapers[index] - wallpaper = wallpaper.get("url_image") + index = randint(0, len(wallpapers) - 1) if len(wallpapers) > 1 else 0 + wallpaper = wallpapers[index].get("largeImageURL") wallpaper = wallpaper.replace("\\", "") delmsg_preview = bot.send_photo( chat_id, diff --git a/AstrakoBot/modules/warns.py b/AstrakoBot/modules/warns.py index 2858d32d2b..8e93602887 100644 --- a/AstrakoBot/modules/warns.py +++ b/AstrakoBot/modules/warns.py @@ -8,10 +8,11 @@ from AstrakoBot.modules.helper_funcs.chat_status import ( bot_admin, can_restrict, - is_user_admin, user_admin, + user_can_ban, user_admin_no_reply, can_delete, + is_user_ban_protected, ) from AstrakoBot.modules.helper_funcs.extraction import ( extract_text, @@ -23,6 +24,7 @@ from AstrakoBot.modules.helper_funcs.string_handling import split_quotes from AstrakoBot.modules.log_channel import loggable from AstrakoBot.modules.sql import warns_sql as sql +from AstrakoBot.modules.helper_funcs.admin_status import user_is_admin from telegram import ( CallbackQuery, Chat, @@ -54,8 +56,8 @@ def warn( user: User, chat: Chat, reason: str, message: Message, warner: User = None ) -> str: - if is_user_admin(chat, user.id): - # message.reply_text("Damn admins, They are too far to be One Punched!") + if is_user_ban_protected(chat, user.id): + message.reply_text("Damn admins, They are too far to be One Punched!") return if user.id in WHITELIST_USERS: @@ -85,7 +87,7 @@ def warn( ) else: # ban - chat.kick_member(user.id) + chat.ban_member(user.id) reply = ( f"ā•Ban Event\n" f" • User: {mention_html(user.id, user.first_name)}\n" @@ -135,7 +137,7 @@ def warn( ) try: - message.reply_text(reply, reply_markup=keyboard, parse_mode=ParseMode.HTML) + message.reply_text(reply, reply_markup=keyboard, parse_mode=ParseMode.HTML, allow_sending_without_reply=True) except BadRequest as excp: if excp.message == "Reply message not found": # Do not reply @@ -179,6 +181,7 @@ def button(update: Update, context: CallbackContext) -> str: @user_admin +@user_can_ban @can_restrict @loggable def warn_user(update: Update, context: CallbackContext) -> str: @@ -194,6 +197,7 @@ def warn_user(update: Update, context: CallbackContext) -> str: if ( message.reply_to_message and message.reply_to_message.from_user.id == user_id + and not message.text.startswith("/d") ): return warn( message.reply_to_message.from_user, @@ -208,8 +212,34 @@ def warn_user(update: Update, context: CallbackContext) -> str: message.reply_text("That looks like an invalid User ID to me.") return "" +@user_admin +@user_can_ban +@bot_admin +@loggable +def rm_last_warn(update: Update, context: CallbackContext) -> str: + args = context.args + message: Optional[Message] = update.effective_message + chat: Optional[Chat] = update.effective_chat + user: Optional[User] = update.effective_user + + user_id = extract_user(message, args) + + if user_id: + sql.remove_warn(user_id, chat.id) + message.reply_text("Latest warning has been removed!") + unwarned = chat.get_member(user_id).user + return ( + f"{html.escape(chat.title)}:\n" + f"#UNWARN\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(unwarned.id, unwarned.first_name)}" + ) + else: + message.reply_text("No user has been designated!") + return "" @user_admin +@user_can_ban @bot_admin @loggable def reset_warns(update: Update, context: CallbackContext) -> str: @@ -266,6 +296,7 @@ def warns(update: Update, context: CallbackContext): # Dispatcher handler stop - do not async @user_admin +@user_can_ban def add_warn_filter(update: Update, context: CallbackContext): chat: Optional[Chat] = update.effective_chat msg: Optional[Message] = update.effective_message @@ -299,6 +330,7 @@ def add_warn_filter(update: Update, context: CallbackContext): @user_admin +@user_can_ban def remove_warn_filter(update: Update, context: CallbackContext): chat: Optional[Chat] = update.effective_chat msg: Optional[Message] = update.effective_message @@ -383,6 +415,7 @@ def reply_filter(update: Update, context: CallbackContext) -> str: @user_admin +@user_can_ban @loggable def set_warn_limit(update: Update, context: CallbackContext) -> str: args = context.args @@ -394,6 +427,8 @@ def set_warn_limit(update: Update, context: CallbackContext) -> str: if args[0].isdigit(): if int(args[0]) < 3: msg.reply_text("The minimum warn limit is 3!") + elif int(args[0]) > 9999: + msg.reply_text("Well damn, that's a too big value!") else: sql.set_warn_limit(chat.id, int(args[0])) msg.reply_text("Updated the warn limit to {}".format(args[0])) @@ -413,6 +448,7 @@ def set_warn_limit(update: Update, context: CallbackContext) -> str: @user_admin +@user_can_ban def set_warn_strength(update: Update, context: CallbackContext): args = context.args chat: Optional[Chat] = update.effective_chat @@ -490,6 +526,7 @@ def __chat_settings__(chat_id, user_id): *Admins only:* • `/warn `*:* warn a user. After 3 warns, the user will be banned from the group. Can also be used as a reply. • `/dwarn `*:* warn a user and delete the message. After 3 warns, the user will be banned from the group. Can also be used as a reply. + • `/rmwarn`, `/unwarn` ``*:* removes user's latest warning. Can also be used as a reply. • `/resetwarn `*:* reset the warns for a user. Can also be used as a reply. • `/addwarn `*:* set a warning filter on a certain keyword. If you want your keyword to \ be a sentence, encompass it with quotes, as such: `/addwarn "very angry" This is an angry user`. @@ -501,6 +538,7 @@ def __chat_settings__(chat_id, user_id): __mod_name__ = "Warnings" WARN_HANDLER = CommandHandler(["warn", "dwarn"], warn_user, filters=Filters.chat_type.groups, run_async=True) +UNWARN_HANDLER = CommandHandler(["unwarn", "rmwarn"], rm_last_warn, filters=Filters.chat_type.groups, run_async=True) RESET_WARN_HANDLER = CommandHandler( ["resetwarn", "resetwarns"], reset_warns, filters=Filters.chat_type.groups, run_async=True ) @@ -522,6 +560,7 @@ def __chat_settings__(chat_id, user_id): ) dispatcher.add_handler(WARN_HANDLER) +dispatcher.add_handler(UNWARN_HANDLER) dispatcher.add_handler(CALLBACK_QUERY_HANDLER) dispatcher.add_handler(RESET_WARN_HANDLER) dispatcher.add_handler(MYWARNS_HANDLER) diff --git a/AstrakoBot/modules/weather.py b/AstrakoBot/modules/weather.py index 4e03187538..be67ad869e 100644 --- a/AstrakoBot/modules/weather.py +++ b/AstrakoBot/modules/weather.py @@ -125,6 +125,33 @@ def sun(unix): ) return xx + ## AirQuality + air_url = f"http://api.openweathermap.org/data/2.5/air_pollution?lat={latitude}&lon={longitude}&appid={APPID}" + air_data = json.loads(get(air_url).text) + + into_dicts = air_data['list'][0] + air_qi = into_dicts['main'] + aqi = int(air_qi['aqi']) + + + ## Pollutant concentration + # airinfo = into_dicts['components'] + # components_co = airinfo["co"] + # components_no = airinfo["no"] + + + def air_qual(aqin): + if aqin == 1: + return "Good" + elif aqin == 2: + return "Fair" + elif aqin == 3: + return 'Moderate' + elif aqin == 4: + return 'Poor' + elif aqin == 5: + return "Very Poor" + msg = f"*{cityname}, {fullc_n}*\n" msg += f"`Longitude: {longitude}`\n" msg += f"`Latitude: {latitude}`\n\n" @@ -135,8 +162,9 @@ def sun(unix): msg += f"• **Humidity:** `{humidity}%`\n" msg += f"• **Wind:** `{kmph[0]} km/h`\n" msg += f"• **Sunrise**: `{sun(sunrise)}`\n" - msg += f"• **Sunset**: `{sun(sunset)}`" - + msg += f"• **Sunset**: `{sun(sunset)}`\n" + msg += f"• **Air Quality**: `{air_qual(aqi)}`" + else: msg = "Please specify a city or country" diff --git a/AstrakoBot/modules/weebify.py b/AstrakoBot/modules/weebify.py index 5018f5f0f0..c1d3f81588 100644 --- a/AstrakoBot/modules/weebify.py +++ b/AstrakoBot/modules/weebify.py @@ -67,7 +67,8 @@ def weebify(update: Update, context: CallbackContext): string = "" if message.reply_to_message: - string = message.reply_to_message.text.lower().replace(" ", " ") + text = message.reply_to_message.text or message.reply_to_message.caption or "" + string = text.lower().replace(" ", " ") if args: string = " ".join(args).lower() diff --git a/AstrakoBot/modules/welcome.py b/AstrakoBot/modules/welcome.py index fb64ad4c8f..4a8b2341e3 100644 --- a/AstrakoBot/modules/welcome.py +++ b/AstrakoBot/modules/welcome.py @@ -31,6 +31,7 @@ from AstrakoBot.modules.log_channel import loggable from AstrakoBot.modules.sql.clear_cmd_sql import get_clearcmd from AstrakoBot.modules.sql.global_bans_sql import is_user_gbanned +from AstrakoBot.modules.helper_funcs.admin_status import get_bot_member from telegram import ( ChatPermissions, InlineKeyboardButton, @@ -45,7 +46,7 @@ CommandHandler, Filters, MessageHandler, - run_async, + run_async, ChatMemberHandler, ) from telegram.utils.helpers import escape_markdown, mention_html, mention_markdown @@ -75,78 +76,104 @@ # do not async -def send(update, message, keyboard, backup_message): +def send(update, message, keyboard, backup_message, reply_to_message=None): + if not message: + return + chat = update.effective_chat - cleanserv = sql.clean_service(chat.id) - reply = update.message.message_id - # Clean service welcome - if cleanserv: - try: - dispatcher.bot.delete_message(chat.id, update.message.message_id) - except BadRequest: - pass - reply = False try: - msg = update.effective_message.reply_text( + msg = dispatcher.bot.send_message(chat.id, + message, + parse_mode=ParseMode.MARKDOWN, + reply_markup=keyboard, + disable_web_page_preview=True, + allow_sending_without_reply=True, + ) + except TypeError: + msg = dispatcher.bot.send_message(chat.id, message, parse_mode=ParseMode.MARKDOWN, reply_markup=keyboard, - reply_to_message_id=reply, + allow_sending_without_reply=True, ) except BadRequest as excp: if excp.message == "Reply message not found": - msg = update.effective_message.reply_text( + msg = dispatcher.bot.send_message(chat.id, message, parse_mode=ParseMode.MARKDOWN, reply_markup=keyboard, quote=False, + disable_web_page_preview=True, + allow_sending_without_reply=True, ) elif excp.message == "Button_url_invalid": - msg = update.effective_message.reply_text( + msg = dispatcher.bot.send_message(chat.id, markdown_parser( - backup_message + "\nNote: the current message has an invalid url " - "in one of its buttons. Please update." + backup_message + + "\nNote: the current message has an invalid url " + "in one of its buttons. Please update.", ), parse_mode=ParseMode.MARKDOWN, - reply_to_message_id=reply, + disable_web_page_preview=True, + allow_sending_without_reply=True, ) elif excp.message == "Unsupported url protocol": - msg = update.effective_message.reply_text( + msg = dispatcher.bot.send_message(chat.id, markdown_parser( - backup_message + "\nNote: the current message has buttons which " + backup_message + + "\nNote: the current message has buttons which " "use url protocols that are unsupported by " - "telegram. Please update." + "telegram. Please update.", ), parse_mode=ParseMode.MARKDOWN, - reply_to_message_id=reply, + disable_web_page_preview=True, + allow_sending_without_reply=True, ) elif excp.message == "Wrong url host": - msg = update.effective_message.reply_text( + msg = dispatcher.bot.send_message(chat.id, markdown_parser( - backup_message + "\nNote: the current message has some bad urls. " - "Please update." + backup_message + + "\nNote: the current message has some bad urls. " + "Please update.", ), parse_mode=ParseMode.MARKDOWN, - reply_to_message_id=reply, + disable_web_page_preview=True, + allow_sending_without_reply=True, ) LOGGER.warning(message) LOGGER.warning(keyboard) LOGGER.exception("Could not parse! got invalid url host errors") - elif excp.message == "Have no rights to send a message": + elif excp.message == "Have no rights to send a message" or excp.message == "Topic_closed": return else: - msg = update.effective_message.reply_text( + msg = dispatcher.bot.send_message(chat.id, markdown_parser( - backup_message + "\nNote: An error occured when sending the " - "custom message. Please update." + backup_message + + "\nNote: An error occured when sending the " + "custom message. Please update.", ), parse_mode=ParseMode.MARKDOWN, - reply_to_message_id=reply, + disable_web_page_preview=True, + allow_sending_without_reply=True, + ) + LOGGER.exception( + "An error occured when sending a custom message to %s", + chat.id, ) - LOGGER.exception("Error in welcome") return msg +def welcomeFilter(update: Update, context: CallbackContext): + if update.effective_chat.type != "group" and update.effective_chat.type != "supergroup": + return + if nm := update.chat_member.new_chat_member: + om = update.chat_member.old_chat_member + if nm.status == nm.MEMBER and (om.status == nm.KICKED or om.status == nm.LEFT): + return new_member(update, context) + if (nm.status == nm.KICKED or nm.status == nm.LEFT) and \ + (om.status == nm.MEMBER or om.status == nm.ADMINISTRATOR or om.status == nm.CREATOR): + return left_member(update, context) + @loggable def new_member(update: Update, context: CallbackContext): bot, job_queue = context.bot, context.job_queue @@ -157,270 +184,235 @@ def new_member(update: Update, context: CallbackContext): should_welc, cust_welcome, cust_content, welc_type = sql.get_welc_pref(chat.id) welc_mutes = sql.welcome_mutes(chat.id) human_checks = sql.get_human_checks(user.id, chat.id) + new_mem = update.chat_member.new_chat_member.user - new_members = update.effective_message.new_chat_members + if new_mem.id == bot.id and not AstrakoBot.ALLOW_CHATS: + with suppress(BadRequest): + dispatcher.bot.send_message(chat.id, f"Groups are disabled for {bot.first_name}, I'm outta here.") + bot.leave_chat(update.effective_chat.id) + return + + welcome_log = None + res = None + sent = None + should_mute = True + welcome_bool = True + media_wel = False + keyboard = None + backup_message = "" + reply = None - for new_mem in new_members: - if new_mem.id == bot.id and not AstrakoBot.ALLOW_CHATS: - with suppress(BadRequest): - update.effective_message.reply_text(f"Groups are disabled for {bot.first_name}, I'm outta here.") - bot.leave_chat(update.effective_chat.id) + if sw is not None: + sw_ban = sw.get_ban(new_mem.id) + if sw_ban: return - welcome_log = None - res = None - sent = None - should_mute = True - welcome_bool = True - media_wel = False + if is_user_gbanned(new_mem.id): + return - if sw is not None: - sw_ban = sw.get_ban(new_mem.id) - if sw_ban: - return + if should_welc: - if is_user_gbanned(new_mem.id): - return + # Give the owner a special welcome + if new_mem.id == OWNER_ID: + deletion(update, context, dispatcher.bot.send_message(chat.id, + "Oh, Genos? Let's get this moving." + )) + welcome_log = ( + f"{html.escape(chat.title)}\n" + f"#USER_JOINED\n" + f"Bot Owner just joined the group" + ) - if should_welc: + # Welcome Devs + elif new_mem.id in DEV_USERS: + deletion(update, context, dispatcher.bot.send_message(chat.id, + "Whoa! A developer user just joined!", + )) + welcome_log = ( + f"{html.escape(chat.title)}\n" + f"#USER_JOINED\n" + f"Bot Dev just joined the group" + ) - reply = update.message.message_id - cleanserv = sql.clean_service(chat.id) - # Clean service welcome - if cleanserv: - try: - dispatcher.bot.delete_message(chat.id, update.message.message_id) - except BadRequest: - pass - reply = False - - # Give the owner a special welcome - if new_mem.id == OWNER_ID: - deletion(update, context, update.effective_message.reply_text( - "Oh, Genos? Let's get this moving.", reply_to_message_id=reply - )) - welcome_log = ( - f"{html.escape(chat.title)}\n" - f"#USER_JOINED\n" - f"Bot Owner just joined the group" - ) - continue - - # Welcome Devs - elif new_mem.id in DEV_USERS: - deletion(update, context, update.effective_message.reply_text( - "Whoa! A developer user just joined!", - reply_to_message_id=reply, - )) - welcome_log = ( - f"{html.escape(chat.title)}\n" - f"#USER_JOINED\n" - f"Bot Dev just joined the group" - ) - continue - - # Welcome Sudos - elif new_mem.id in SUDO_USERS: - deletion(update, context, update.effective_message.reply_text( - "Huh! A sudo user just joined! Stay Alert!", - reply_to_message_id=reply, - )) - welcome_log = ( - f"{html.escape(chat.title)}\n" - f"#USER_JOINED\n" - f"Bot Sudo just joined the group" - ) - continue - - # Welcome Support - elif new_mem.id in SUPPORT_USERS: - deletion(update, context, update.effective_message.reply_text( - "Huh! A support user just joined!", - reply_to_message_id=reply, - )) - welcome_log = ( - f"{html.escape(chat.title)}\n" - f"#USER_JOINED\n" - f"Bot Support just joined the group" - ) - continue - - # Welcome Whitelisted - elif new_mem.id in WHITELIST_USERS: - deletion(update, context, update.effective_message.reply_text( - "Oof! A whitelist user just joined!", reply_to_message_id=reply - )) - welcome_log = ( - f"{html.escape(chat.title)}\n" - f"#USER_JOINED\n" - f"Bot whitelisted just joined the group" - ) - continue - - # Welcome yourself - elif new_mem.id == bot.id: - creator = None - for x in bot.bot.get_chat_administrators(update.effective_chat.id): - if x.status == "creator": - creator = x.user - break - if creator: - bot.send_message( - JOIN_LOGGER, - "#NEW_GROUP\nGroup name: {}\nID: {}\nCreator: {}".format( - html.escape(chat.title), chat.id, html.escape(creator) - ), - parse_mode=ParseMode.HTML, - ) - else: - bot.send_message( - JOIN_LOGGER, - "#NEW_GROUP\nGroup name: {}\nID: {}".format( - html.escape(chat.title), chat.id - ), - parse_mode=ParseMode.HTML, - ) - update.effective_message.reply_text( - "Hello everybody!", reply_to_message_id=reply - ) - continue + # Welcome Sudos + elif new_mem.id in SUDO_USERS: + deletion(update, context, dispatcher.bot.send_message(chat.id, + "Huh! A sudo user just joined! Stay Alert!", + )) + welcome_log = ( + f"{html.escape(chat.title)}\n" + f"#USER_JOINED\n" + f"Bot Sudo just joined the group" + ) - else: - buttons = sql.get_welc_buttons(chat.id) - keyb = build_keyboard(buttons) + # Welcome Support + elif new_mem.id in SUPPORT_USERS: + deletion(update, context, dispatcher.bot.send_message(chat.id, + "Huh! A support user just joined!", + )) + welcome_log = ( + f"{html.escape(chat.title)}\n" + f"#USER_JOINED\n" + f"Bot Support just joined the group" + ) - if welc_type not in (sql.Types.TEXT, sql.Types.BUTTON_TEXT): - media_wel = True - - first_name = ( - new_mem.first_name or "PersonWithNoName" - ) # edge case of empty name - occurs for some bugs. - - if cust_welcome: - if cust_welcome == sql.DEFAULT_WELCOME: - cust_welcome = random.choice( - sql.DEFAULT_WELCOME_MESSAGES - ).format(first=escape_markdown(first_name)) - - if new_mem.last_name: - fullname = escape_markdown(f"{first_name} {new_mem.last_name}") - else: - fullname = escape_markdown(first_name) - count = chat.get_member_count() - mention = mention_markdown(new_mem.id, escape_markdown(first_name)) - if new_mem.username: - username = "@" + escape_markdown(new_mem.username) - else: - username = mention - - valid_format = escape_invalid_curly_brackets( - cust_welcome, VALID_WELCOME_FORMATTERS - ) - res = valid_format.format( - first=escape_markdown(first_name), - last=escape_markdown(new_mem.last_name or first_name), - fullname=escape_markdown(fullname), - username=username, - mention=mention, - count=count, - chatname=escape_markdown(chat.title), - id=new_mem.id, - ) + # Welcome Whitelisted + elif new_mem.id in WHITELIST_USERS: + deletion(update, context, dispatcher.bot.send_message(chat.id, + "Oof! A whitelist user just joined!", + )) + welcome_log = ( + f"{html.escape(chat.title)}\n" + f"#USER_JOINED\n" + f"Bot whitelisted just joined the group" + ) + + # Welcome yourself + elif new_mem.id == bot.id: + dispatcher.bot.send_message(chat.id, + "Thanks for adding me! Join https://t.me/AstrakoBotSupport for support.", + disable_web_page_preview=True, + ) + + bot.send_message( + JOIN_LOGGER, + "#NEW_GROUP\nGroup name: {}\nID: {}".format( + html.escape(chat.title), chat.id + ), + parse_mode=ParseMode.HTML, + ) + else: + buttons = sql.get_welc_buttons(chat.id) + keyb = build_keyboard(buttons) + + if welc_type not in (sql.Types.TEXT, sql.Types.BUTTON_TEXT): + media_wel = True + + first_name = ( + new_mem.first_name or "PersonWithNoName" + ) # edge case of empty name - occurs for some bugs. + + if cust_welcome: + if cust_welcome == sql.DEFAULT_WELCOME: + cust_welcome = random.choice( + sql.DEFAULT_WELCOME_MESSAGES + ).format(first=escape_markdown(first_name)) + if new_mem.last_name: + fullname = escape_markdown(f"{first_name} {new_mem.last_name}") else: - res = random.choice(sql.DEFAULT_WELCOME_MESSAGES).format( - first=escape_markdown(first_name) - ) - keyb = [] + fullname = escape_markdown(first_name) + count = chat.get_member_count() + mention = mention_markdown(new_mem.id, escape_markdown(first_name)) + if new_mem.username: + username = "@" + escape_markdown(new_mem.username) + else: + username = mention - backup_message = random.choice(sql.DEFAULT_WELCOME_MESSAGES).format( + valid_format = escape_invalid_curly_brackets( + cust_welcome, VALID_WELCOME_FORMATTERS + ) + res = valid_format.format( + first=escape_markdown(first_name), + last=escape_markdown(new_mem.last_name or first_name), + fullname=escape_markdown(fullname), + username=username, + mention=mention, + count=count, + chatname=escape_markdown(chat.title), + id=new_mem.id, + ) + + else: + res = random.choice(sql.DEFAULT_WELCOME_MESSAGES).format( first=escape_markdown(first_name) ) - keyboard = InlineKeyboardMarkup(keyb) + keyb = [] - else: - welcome_bool = False - res = None - keyboard = None - backup_message = None - reply = None - - # User exceptions from welcomemutes - if ( - is_user_ban_protected(chat, new_mem.id, chat.get_member(new_mem.id)) - or human_checks - ): - should_mute = False - # Join welcome: soft mute - if new_mem.is_bot: - should_mute = False - - if user.id == new_mem.id: - if should_mute: - if welc_mutes == "soft": - bot.restrict_chat_member( - chat.id, - new_mem.id, - permissions=ChatPermissions( - can_send_messages=True, - can_send_media_messages=False, - can_send_other_messages=False, - can_invite_users=False, - can_pin_messages=False, - can_send_polls=False, - can_change_info=False, - can_add_web_page_previews=False, - ), - until_date=(int(time.time() + 24 * 60 * 60)), + backup_message = random.choice(sql.DEFAULT_WELCOME_MESSAGES).format( + first=escape_markdown(first_name) + ) + keyboard = InlineKeyboardMarkup(keyb) + else: + welcome_bool = False + backup_message = None + + # User exceptions from welcomemutes + if ( + is_user_ban_protected(chat, new_mem.id, chat.get_member(new_mem.id)) + or human_checks + ): + should_mute = False + # Join welcome: soft mute + if new_mem.is_bot: + should_mute = False + + if user.id == new_mem.id: + if should_mute: + if welc_mutes == "soft": + bot.restrict_chat_member( + chat.id, + new_mem.id, + permissions=ChatPermissions( + can_send_messages=True, + can_send_media_messages=False, + can_send_other_messages=False, + can_invite_users=False, + can_pin_messages=False, + can_send_polls=False, + can_change_info=False, + can_add_web_page_previews=False, + ), + until_date=(int(time.time() + 24 * 60 * 60)), + ) + if welc_mutes == "strong": + welcome_bool = False + if not media_wel: + VERIFIED_USER_WAITLIST.update( + { + new_mem.id: { + "should_welc": should_welc, + "media_wel": False, + "status": False, + "update": update, + "res": res, + "keyboard": keyboard, + "backup_message": backup_message, + } + } ) - if welc_mutes == "strong": - welcome_bool = False - if not media_wel: - VERIFIED_USER_WAITLIST.update( - { - new_mem.id: { - "should_welc": should_welc, - "media_wel": False, - "status": False, - "update": update, - "res": res, - "keyboard": keyboard, - "backup_message": backup_message, - } + else: + VERIFIED_USER_WAITLIST.update( + { + new_mem.id: { + "should_welc": should_welc, + "chat_id": chat.id, + "status": False, + "media_wel": True, + "cust_content": cust_content, + "welc_type": welc_type, + "res": res, + "keyboard": keyboard, } - ) - else: - VERIFIED_USER_WAITLIST.update( + } + ) + new_join_mem = f'{html.escape(new_mem.first_name)}' + message = dispatcher.bot.send_message(chat.id, + f"{new_join_mem}, click the button below to prove you're human.\nYou have 60 seconds.", + reply_markup=InlineKeyboardMarkup( + [ { - new_mem.id: { - "should_welc": should_welc, - "chat_id": chat.id, - "status": False, - "media_wel": True, - "cust_content": cust_content, - "welc_type": welc_type, - "res": res, - "keyboard": keyboard, - } + InlineKeyboardButton( + text="Yes, I'm human.", + callback_data=f"user_join_({new_mem.id})", + ) } - ) - new_join_mem = f'{html.escape(new_mem.first_name)}' - message = msg.reply_text( - f"{new_join_mem}, click the button below to prove you're human.\nYou have 60 seconds.", - reply_markup=InlineKeyboardMarkup( - [ - { - InlineKeyboardButton( - text="Yes, I'm human.", - callback_data=f"user_join_({new_mem.id})", - ) - } - ] - ), - parse_mode=ParseMode.HTML, - reply_to_message_id=reply, - ) + ] + ), + parse_mode=ParseMode.HTML, + ) + if get_bot_member(chat.id).can_restrict_members: bot.restrict_chat_member( chat.id, new_mem.id, @@ -435,62 +427,72 @@ def new_member(update: Update, context: CallbackContext): can_add_web_page_previews=False, ), ) - job_queue.run_once( - partial(check_not_bot, new_mem, chat.id, message.message_id), - 60, - name="welcomemute", - ) + job_queue.run_once( + partial(check_not_bot, new_mem, chat.id, message.message_id), + 60, + name="welcomemute", + ) - if welcome_bool: - if media_wel: + if welcome_bool: + if media_wel: + # Stickers have no caption, send separately + if welc_type == sql.Types.STICKER: + sent = ENUM_FUNC_MAP[welc_type]( + chat.id, + cust_content, + reply_markup=keyboard + ) and ENUM_FUNC_MAP[sql.Types.TEXT]( + chat.id, + res, + parse_mode="markdown" + ) + else: sent = ENUM_FUNC_MAP[welc_type]( chat.id, cust_content, caption=res, reply_markup=keyboard, - reply_to_message_id=reply, - parse_mode="markdown", + parse_mode="markdown" ) - else: - sent = send(update, res, keyboard, backup_message) - deletion(update, context, sent) - prev_welc = sql.get_clean_pref(chat.id) - if prev_welc: - try: - bot.delete_message(chat.id, prev_welc) - except BadRequest: - pass - - if sent: - sql.set_clean_welcome(chat.id, sent.message_id) + else: + sent = send(update, res, keyboard, backup_message) + deletion(update, context, sent) + prev_welc = sql.get_clean_pref(chat.id) + if prev_welc: + try: + bot.delete_message(chat.id, prev_welc) + except BadRequest: + pass - if welcome_log: - return welcome_log + if sent: + sql.set_clean_welcome(chat.id, sent.message_id) - if user.id == new_mem.id: - welcome_log = ( - f"{html.escape(chat.title)}\n" - f"#USER_JOINED\n" - f"User: {mention_html(user.id, user.first_name)}\n" - f"ID: {user.id}" - ) - elif new_mem.is_bot and user.id != new_mem.id: - welcome_log = ( - f"{html.escape(chat.title)}\n" - f"#BOT_ADDED\n" - f"Bot: {mention_html(new_mem.id, new_mem.first_name)}\n" - f"ID: {new_mem.id}" - ) - else: - welcome_log = ( - f"{html.escape(chat.title)}\n" - f"#USER_ADDED\n" - f"User: {mention_html(new_mem.id, new_mem.first_name)}\n" - f"ID: {new_mem.id}" - ) + if welcome_log: return welcome_log - return "" + if user.id == new_mem.id: + welcome_log = ( + f"{html.escape(chat.title)}\n" + f"#USER_JOINED\n" + f"User: {mention_html(user.id, user.first_name)}\n" + f"ID: {user.id}" + ) + elif new_mem.is_bot and user.id != new_mem.id: + welcome_log = ( + f"{html.escape(chat.title)}\n" + f"#BOT_ADDED\n" + f"Bot: {mention_html(new_mem.id, new_mem.first_name)}\n" + f"ID: {new_mem.id}" + ) + else: + welcome_log = ( + f"{html.escape(chat.title)}\n" + f"#USER_ADDED\n" + f"User: {mention_html(new_mem.id, new_mem.first_name)}\n" + f"ID: {new_mem.id}" + ) + return welcome_log + def check_not_bot(member, chat_id, message_id, context): @@ -514,6 +516,19 @@ def check_not_bot(member, chat_id, message_id, context): pass +def cleanServiceFilter(u: Update, _): + if u.effective_message.left_chat_member or u.effective_message.new_chat_members: + return handleCleanService(u) + + +def handleCleanService(update: Update): + if sql.clean_service(update.effective_chat.id): + try: + dispatcher.bot.delete_message(update.effective_chat.id, update.message.message_id) + except BadRequest: + pass + + def left_member(update: Update, context: CallbackContext): bot = context.bot chat = update.effective_chat @@ -524,17 +539,8 @@ def left_member(update: Update, context: CallbackContext): return if should_goodbye: - reply = update.message.message_id - cleanserv = sql.clean_service(chat.id) - # Clean service welcome - if cleanserv: - try: - dispatcher.bot.delete_message(chat.id, update.message.message_id) - except BadRequest: - pass - reply = False - left_mem = update.effective_message.left_chat_member + left_mem = update.chat_member.new_chat_member.user if left_mem: # Thingy for spamwatched users @@ -553,16 +559,15 @@ def left_member(update: Update, context: CallbackContext): # Give the owner a special goodbye if left_mem.id == OWNER_ID: - update.effective_message.reply_text( - "Oi! Genos! He left..", reply_to_message_id=reply + dispatcher.bot.send_message(chat.id, + "Oi! Genos! He left..", ) return # Give the devs a special goodbye elif left_mem.id in DEV_USERS: - update.effective_message.reply_text( + dispatcher.bot.send_message(chat.id, "See you later dev!", - reply_to_message_id=reply, ) return @@ -624,13 +629,29 @@ def left_member(update: Update, context: CallbackContext): deletion(update, context, delmsg) +def get_welcome_kwargs(welcome_type, chat, welcome_m, keyboard): + kwargs = { + 'reply_markup': keyboard, + } + + # Add caption (except for stickers) + if welcome_type != sql.Types.STICKER: + kwargs['caption'] = welcome_m + kwargs['parse_mode'] = ParseMode.MARKDOWN + + # Add web preview disable (only for supported types) + if welcome_type in {sql.Types.TEXT, sql.Types.PHOTO}: + kwargs['disable_web_page_preview'] = True + + return kwargs + @user_admin def welcome(update: Update, context: CallbackContext): args = context.args chat = update.effective_chat # if no args, show current replies. if not args or args[0].lower() == "noformat": - noformat = True + noformat = bool(args and args[0].lower() == "noformat") pref, welcome_m, cust_content, welcome_type = sql.get_welc_pref(chat.id) update.effective_message.reply_text( f"This chat has it's welcome setting set to: `{pref}`.\n" @@ -638,7 +659,7 @@ def welcome(update: Update, context: CallbackContext): parse_mode=ParseMode.MARKDOWN, ) - if welcome_type == sql.Types.BUTTON_TEXT or welcome_type == sql.Types.TEXT: + if welcome_type in [sql.Types.BUTTON_TEXT, sql.Types.TEXT]: buttons = sql.get_welc_buttons(chat.id) if noformat: welcome_m += revert_buttons(buttons) @@ -652,20 +673,17 @@ def welcome(update: Update, context: CallbackContext): else: buttons = sql.get_welc_buttons(chat.id) if noformat: - welcome_m += revert_buttons(buttons) - ENUM_FUNC_MAP[welcome_type](chat.id, cust_content, caption=welcome_m) + if welcome_m: + welcome_m += revert_buttons(buttons) + ENUM_FUNC_MAP[welcome_type](chat.id, cust_content, caption=welcome_m) + else: + ENUM_FUNC_MAP[welcome_type](chat.id, cust_content) else: keyb = build_keyboard(buttons) keyboard = InlineKeyboardMarkup(keyb) - ENUM_FUNC_MAP[welcome_type]( - chat.id, - cust_content, - caption=welcome_m, - reply_markup=keyboard, - parse_mode=ParseMode.MARKDOWN, - disable_web_page_preview=True, - ) + kwargs = get_welcome_kwargs(welcome_type, chat, welcome_m, keyboard) + ENUM_FUNC_MAP[welcome_type](chat.id, cust_content, **kwargs) elif len(args) >= 1: if args[0].lower() in ("on", "yes"): @@ -692,7 +710,7 @@ def goodbye(update: Update, context: CallbackContext): chat = update.effective_chat if not args or args[0] == "noformat": - noformat = True + noformat = bool(args and args[0].lower() == "noformat") pref, goodbye_m, goodbye_type = sql.get_gdbye_pref(chat.id) update.effective_message.reply_text( f"This chat has it's goodbye setting set to: `{pref}`.\n" @@ -712,14 +730,13 @@ def goodbye(update: Update, context: CallbackContext): send(update, goodbye_m, keyboard, sql.DEFAULT_GOODBYE) - else: - if noformat: - ENUM_FUNC_MAP[goodbye_type](chat.id, goodbye_m) + elif noformat: + ENUM_FUNC_MAP[goodbye_type](chat.id, goodbye_m) - else: - ENUM_FUNC_MAP[goodbye_type]( - chat.id, goodbye_m, parse_mode=ParseMode.MARKDOWN - ) + else: + ENUM_FUNC_MAP[goodbye_type]( + chat.id, goodbye_m, parse_mode=ParseMode.MARKDOWN + ) elif len(args) >= 1: if args[0].lower() in ("on", "yes"): @@ -968,33 +985,45 @@ def user_button(update: Update, context: CallbackContext): member_dict["status"] = True VERIFIED_USER_WAITLIST.update({user.id: member_dict}) query.answer(text="Yeet! You're a human, unmuted!") - bot.restrict_chat_member( - chat.id, - user.id, - permissions=ChatPermissions( - can_send_messages=True, - can_invite_users=True, - can_pin_messages=True, - can_send_polls=True, - can_change_info=True, - can_send_media_messages=True, - can_send_other_messages=True, - can_add_web_page_previews=True, - ), - ) + if get_bot_member(chat.id).can_restrict_members: + bot.restrict_chat_member( + chat.id, + user.id, + permissions=ChatPermissions( + can_send_messages=True, + can_invite_users=True, + can_pin_messages=True, + can_send_polls=True, + can_change_info=True, + can_send_media_messages=True, + can_send_other_messages=True, + can_add_web_page_previews=True, + ), + ) try: bot.deleteMessage(chat.id, message.message_id) except: pass if member_dict["should_welc"]: if member_dict["media_wel"]: - sent = ENUM_FUNC_MAP[member_dict["welc_type"]]( - member_dict["chat_id"], - member_dict["cust_content"], - caption=member_dict["res"], - reply_markup=member_dict["keyboard"], - parse_mode="markdown", - ) + if member_dict["welc_type"] == sql.Types.STICKER: + sent = ENUM_FUNC_MAP[member_dict["welc_type"]]( + member_dict["chat_id"], + member_dict["cust_content"], + reply_markup=member_dict["keyboard"], + ) and ENUM_FUNC_MAP[sql.Types.TEXT]( + member_dict["chat_id"], + member_dict["res"], + parse_mode="markdown", + ) + else: + sent = ENUM_FUNC_MAP[member_dict["welc_type"]]( + member_dict["chat_id"], + member_dict["cust_content"], + caption=member_dict["res"], + reply_markup=member_dict["keyboard"], + parse_mode="markdown", + ) else: sent = send( member_dict["update"], @@ -1118,8 +1147,15 @@ def __chat_settings__(chat_id, user_id): • `/welcomehelp`*:* view more formatting information for custom welcome/goodbye messages. """ -NEW_MEM_HANDLER = MessageHandler(Filters.status_update.new_chat_members, new_member, run_async=True) -LEFT_MEM_HANDLER = MessageHandler(Filters.status_update.left_chat_member, left_member, run_async=True) +dispatcher.add_handler( + ChatMemberHandler( + welcomeFilter, ChatMemberHandler.CHAT_MEMBER, run_async=True + ), group=-100) + +dispatcher.add_handler( + MessageHandler(Filters.chat_type.groups, cleanServiceFilter), group=100) + + WELC_PREF_HANDLER = CommandHandler("welcome", welcome, filters=Filters.chat_type.groups, run_async=True) GOODBYE_PREF_HANDLER = CommandHandler("goodbye", goodbye, filters=Filters.chat_type.groups, run_async=True) SET_WELCOME = CommandHandler("setwelcome", set_welcome, filters=Filters.chat_type.groups, run_async=True) @@ -1135,8 +1171,6 @@ def __chat_settings__(chat_id, user_id): WELCOME_MUTE_HELP = CommandHandler("welcomemutehelp", welcome_mute_help, run_async=True) BUTTON_VERIFY_HANDLER = CallbackQueryHandler(user_button, pattern=r"user_join_", run_async=True) -dispatcher.add_handler(NEW_MEM_HANDLER) -dispatcher.add_handler(LEFT_MEM_HANDLER) dispatcher.add_handler(WELC_PREF_HANDLER) dispatcher.add_handler(GOODBYE_PREF_HANDLER) dispatcher.add_handler(SET_WELCOME) @@ -1153,8 +1187,6 @@ def __chat_settings__(chat_id, user_id): __mod_name__ = "Welcomes/Goodbyes" __command_list__ = [] __handlers__ = [ - NEW_MEM_HANDLER, - LEFT_MEM_HANDLER, WELC_PREF_HANDLER, GOODBYE_PREF_HANDLER, SET_WELCOME, diff --git a/AstrakoBot/modules/wiki.py b/AstrakoBot/modules/wiki.py index bce0262180..d4b7b31726 100644 --- a/AstrakoBot/modules/wiki.py +++ b/AstrakoBot/modules/wiki.py @@ -28,7 +28,7 @@ def wiki(update: Update, context: CallbackContext): msg += f"Read more: https://en.wikipedia.org/wiki/{definition}" if len(msg) > 4000: with open("result.txt", "w") as f: - f.write(f"{result}\n\nUwU OwO OmO UmU") + f.write(f"{res}\n\nUwU OwO OmO UmU") with open("result.txt", "rb") as f: delmsg = context.bot.send_document( document=f, diff --git a/AstrakoBot/modules/youtube.py b/AstrakoBot/modules/youtube.py index e6765e0ac1..fc5b084bdb 100644 --- a/AstrakoBot/modules/youtube.py +++ b/AstrakoBot/modules/youtube.py @@ -1,173 +1,155 @@ -import os, glob, json - +import os +import json +import random +import string from datetime import datetime -from AstrakoBot.modules.sql.clear_cmd_sql import get_clearcmd from telegram import Bot, Update, ParseMode, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import CommandHandler, CallbackQueryHandler, CallbackContext, run_async +from AstrakoBot.modules.sql.clear_cmd_sql import get_clearcmd from AstrakoBot import dispatcher from AstrakoBot.modules.disable import DisableAbleCommandHandler from AstrakoBot.modules.helper_funcs.misc import delete -from youtubesearchpython import VideosSearch +import pytube +from moviepy.editor import * -from youtube_dl import YoutubeDL +def get_random(): + letters = string.ascii_lowercase + return ''.join(random.choice(letters) for i in range(10)) +def is_ytl(url): + return 'youtube.com/watch?v=' in url or 'youtu.be/' in url -def youtube(update: Update, context: CallbackContext): - bot = context.bot - message = update.effective_message - chat = update.effective_chat - yt = message.text[len("/youtube ") :] - if yt: - search = VideosSearch(yt, limit=1) - result = search.result() +def format_link(youtube_link): + if "youtu.be/" in youtube_link: + youtube_link = youtube_link.replace('youtu.be/', 'youtube.com/watch?v=') + if '&ab_channel' in youtube_link: + youtube_link = youtube_link.split('&ab_channel')[0] + return youtube_link + +def dyt_video(youtube_link, resolution, filename): + youtube_link = format_link(youtube_link) + youtube = pytube.YouTube(youtube_link) + # Try to get video length at least 5 times + for i in range(5): try: - url = result["result"][0]["link"] - title = result["result"][0]["title"] + video_length = youtube.length / 60 + break except: - return message.reply_text( - "Failed to find song or video", - ) - - buttons = [ - [ - InlineKeyboardButton("šŸŽµ", callback_data=f"youtube;audio;{url}"), - InlineKeyboardButton("šŸŽ„", callback_data=f"youtube;video;{url}"), - InlineKeyboardButton("🚫", callback_data=f"youtube;cancel;"""), - ] - ] - - msg = "*Preparing to upload file:*\n" - msg += f"`{title}`\n" - delmsg = message.reply_text( - msg, - parse_mode=ParseMode.MARKDOWN, - reply_markup = InlineKeyboardMarkup(buttons) - ) + if i == 4: + return "Could not get video length! Try again in a few seconds or try another video." - else: - delmsg = message.reply_text("Specify a song or video" - ) - cleartime = get_clearcmd(chat.id, "youtube") - - if cleartime: - context.dispatcher.run_async(delete, delmsg, cleartime.time) + if video_length > 10: # 10 minutes limit for video + return "Video is longer than 10 minutes! Try again with a shorter one." + + video_streams = youtube.streams + available_resolutions = [stream.resolution for stream in video_streams if stream.resolution is not None] + if resolution not in available_resolutions: + resolution = max(available_resolutions, key=lambda x: int(x[:-1])) + + video_stream = youtube.streams.filter(resolution=resolution, progressive=True).order_by('resolution').desc().first() + audio_stream = youtube.streams.filter(only_audio=True).order_by('abr').desc().first() + + video_file = video_stream.download(filename="{}.mp4".format(get_random())) + audio_file = audio_stream.download(filename="{}.mp3".format(get_random())) + + video_clip = VideoFileClip(video_file) + audio_clip = AudioFileClip(audio_file) + final_clip = video_clip.set_audio(audio_clip) + final_clip.write_videofile(filename, codec='libx264', audio_codec='aac') + + try: + os.remove(video_file) + os.remove(audio_file) + except: + pass + + return "" + +def dyt_audio(youtube_link, filename): + youtube_link = format_link(youtube_link) + youtube = pytube.YouTube(youtube_link) + + # Try to get video length at least 5 times, even with this, pytube may fail sometimes. + for i in range(5): + try: + video_length = youtube.length / 60 + break + except: + if i == 5: + return "Could not get video length! Try again in a few seconds or try another video." + if video_length > 30: # 30 minutes limit for audio + return "Audio is longer than 30 minutes! Try again with a shorter one." -def youtube_callback(update: Update, context: CallbackContext): - bot = context.bot + audio_stream = youtube.streams.filter(only_audio=True, abr="128kbps").first() + + try: + audio_stream.download(filename=filename) + except: + return "Unknown Error." + return "" + +def youtube(update: Update, context: CallbackContext): + message_id = update.message.message_id message = update.effective_message chat = update.effective_chat - query = update.callback_query - - media = query.data.split(";") - media_type = media[1] - media_url = media[2] - - if media_type == "audio": - deltext = message.edit_text("Processing song...") - opts = { - "format": "bestaudio/best", - "addmetadata": True, - "geo_bypass": True, - "nocheckcertificate": True, - "postprocessors": [ - { - "key": "FFmpegExtractAudio", - "preferredcodec": "mp3", - "preferredquality": "128", - } - ], - "outtmpl": "%(title)s.%(etx)s", - "quiet": True, - "logtostderr": False, - } - - codec = "mp3" - - with YoutubeDL(opts) as rip: - rip_data = rip.extract_info(media_url, download=False, process=False) - if int(rip_data['duration'] / 60) < 10: - try: - rip_data = rip.extract_info(media_url) - delmsg = bot.send_audio( - chat_id = chat.id, - audio = open(f"{rip_data['title']}.{codec}", "rb"), - duration = int(rip_data['duration']), - title = str(rip_data['title']), - parse_mode = ParseMode.HTML - ) - context.dispatcher.run_async(delete, deltext, 0) - except: - delmsg = message.edit_text( - "Song is too large for processing, or any other error happened. Try again later" - ) - else: - delmsg = message.edit_text( - "Song is too large for processing. Duration is limited to 10 minutes max" - ) - - elif media_type == "video": - deltext = message.edit_text("Processing video...") - opts = { - "format": "best", - "addmetadata": True, - "geo_bypass": True, - "nocheckcertificate": True, - "postprocessors": [ - { - "key": "FFmpegVideoConvertor", - "preferedformat": "mp4", - } - ], - "outtmpl": "%(title)s.mp4", - "quiet": True, - "logtostderr": False, - } - - codec = "mp4" - - with YoutubeDL(opts) as rip: - rip_data = rip.extract_info(media_url, download=False, process=False) - if int(rip_data['duration'] / 60) < 10: - try: - rip_data = rip.extract_info(media_url) - delmsg = bot.send_video( - chat_id = chat.id, - video = open(f"{rip_data['title']}.{codec}", "rb"), - duration = int(rip_data['duration']), - caption = rip_data['title'], - supports_streaming = True, - parse_mode = ParseMode.HTML - ) - context.dispatcher.run_async(delete, deltext, 0) - except: - delmsg = message.edit_text( - "Video is too large for processing, or any other error happened. Try again later" - ) - else: - delmsg = message.edit_text( - "Video is too large for processing. Duration is limited to 10 minutes max" - ) + args = context.args + chat_id = update.effective_chat.id + if not args or not is_ytl(args[0]): + message.reply_text("Specify a song or video!") + return + yt = args[0] + avail_res = ["720p", "480p", "360p", "240p", "144p"] # Limit to HD due to file size + types = ["video", "audio"] + type = "video" + res = avail_res[0] + for i in types: + if i in args: + type = i + break + for i in avail_res: + if i in args or i[:-1] in args: + res = i + if not res.endswith("p"): + res += "p" + break + + filename = get_random() + + if type == "video": + filename += ".mp4" + msg = message.reply_text("Downloading as video, Please wait...") + ret = dyt_video(yt, res, filename) + caption = "Type: mp4\nQuality: {}".format(res) + if not ret: + msg.edit_text("Uploading...") + with open(filename, "rb") as video: + context.bot.send_video(chat_id=chat_id, video=video, caption=caption, reply_to_message_id=message_id) + msg.delete() + else: + msg.edit_text(ret) + else: - delmsg = message.edit_text("Canceling...") - context.dispatcher.run_async(delete, delmsg, 1) + filename += ".mp3" + msg = message.reply_text("Downloading as mp3 audio, Please wait...") + ret = dyt_audio(yt, filename) + caption = "Type: mp3\nQuality: 128kbps".format(res) + if not ret: + msg.edit_text("Uploading...") + with open(filename, "rb") as audio: + context.bot.send_audio(chat_id=chat_id, audio=audio, caption=caption.format(type), reply_to_message_id=message_id) + msg.delete() + else: + msg.edit_text(ret) try: - os.remove(f"{rip_data['title']}.{codec}") + os.remove(filename) except Exception: pass - cleartime = get_clearcmd(chat.id, "youtube") - - if cleartime: - context.dispatcher.run_async(delete, delmsg, cleartime.time) - + return YOUTUBE_HANDLER = DisableAbleCommandHandler(["youtube", "yt"], youtube, run_async = True) -YOUTUBE_CALLBACKHANDLER = CallbackQueryHandler( - youtube_callback, pattern="youtube*", run_async=True -) dispatcher.add_handler(YOUTUBE_HANDLER) -dispatcher.add_handler(YOUTUBE_CALLBACKHANDLER) diff --git a/AstrakoBot/sample_config.py b/AstrakoBot/sample_config.py index cff0ed2aa0..b2a6a64133 100644 --- a/AstrakoBot/sample_config.py +++ b/AstrakoBot/sample_config.py @@ -3,6 +3,7 @@ import os + def get_user_list(config, key): with open("{}/AstrakoBot/{}".format(os.getcwd(), config), "r") as json_file: return json.load(json_file)[key] @@ -30,6 +31,7 @@ class Config(object): # RECOMMENDED SQLALCHEMY_DATABASE_URI = "something://somewhat:user@hosturl:port/databasename" # needed for any database modules + DB_NAME = "databasename" # needed for cron_jobs module, use same databasename from SQLALCHEMY_DATABASE_URI LOAD = [] NO_LOAD = ["rss", "cleaner", "connection", "math"] WEBHOOK = False @@ -59,16 +61,18 @@ class Config(object): BAN_STICKER = "" # banhammer marie sticker id, the bot will send this sticker before banning or kicking a user in chat. ALLOW_EXCL = True # Allow ! commands as well as / (Leave this to true so that blacklist can work) CASH_API_KEY = ( - "awoo" # Get your API key from https://www.alphavantage.co/support/#api-key + "awoo" # Get your API key from https://app.exchangerate-api.com/dashboard ) TIME_API_KEY = "awoo" # Get your API key from https://timezonedb.com/api WALL_API = ( - "awoo" # For wallpapers, get one from https://wall.alphacoders.com/api.php + "awoo" # For wallpapers, get one from https://pixabay.com/api/docs ) AI_API_KEY = "awoo" # For chatbot, get one from https://coffeehouse.intellivoid.net/dashboard BL_CHATS = [] # List of groups that you want blacklisted. SPAMMERS = None - + + BACKUP_PASS = "12345" # The password used for the cron backups zip + DROP_UPDATES = False # whether to drop the pending updates or not class Production(Config): LOGGER = True diff --git a/README.md b/README.md index 5bd687fd44..19c4c1d9d0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@

- +

diff --git a/requirements.txt b/requirements.txt index 0b456e4a9f..b17aed3c49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ future -emoji +emoji==1.7.0 lxml beautifulsoup4 requests -sqlalchemy -python-telegram-bot==13.9 +sqlalchemy==1.4.11 +python-telegram-bot==13.14 psycopg2-binary feedparser pynewtonmath @@ -23,12 +23,12 @@ speedtest-cli coffeehouse regex bleach -git+https://github.com/starry69/python-markdown2.git +markdown2 wikipedia telethon spamwatch alphabet_detector -pyrate-limiter +pyrate-limiter==2.8.1 covid datetime pytz @@ -40,10 +40,10 @@ hachoir pybase64 pySmartDL validators -youtube_dl -youtube-search-python gtts pretty_errors tswift PyYAML humanize +pytube +moviepy diff --git a/resources/DroidSansMono.ttf b/resources/DroidSansMono.ttf new file mode 100644 index 0000000000..b7bf5b4aa8 Binary files /dev/null and b/resources/DroidSansMono.ttf differ diff --git a/resources/Quivira.otf b/resources/Quivira.otf new file mode 100644 index 0000000000..8064cae278 Binary files /dev/null and b/resources/Quivira.otf differ diff --git a/resources/Roboto-Italic.ttf b/resources/Roboto-Italic.ttf new file mode 100644 index 0000000000..f0f33dbdfa Binary files /dev/null and b/resources/Roboto-Italic.ttf differ diff --git a/resources/Roboto-Medium.ttf b/resources/Roboto-Medium.ttf new file mode 100644 index 0000000000..8798341989 Binary files /dev/null and b/resources/Roboto-Medium.ttf differ diff --git a/resources/Roboto-Regular.ttf b/resources/Roboto-Regular.ttf new file mode 100644 index 0000000000..7d9a6c4c32 Binary files /dev/null and b/resources/Roboto-Regular.ttf differ diff --git a/run.sh b/run.sh new file mode 100755 index 0000000000..ce61ac3691 --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source venv/bin/activate +python3.10 -m AstrakoBot